feat: site languages, setup and site URL settings
This commit is contained in:
1 parent
0806733d5c
commit
d9158025d4
106 files changed
+10184
-56
No files matched your search
@@ -0,0 +1,157 @@
|
||||
import { vi } from 'vitest'
|
||||
import { defineComponent, h } from 'vue'
|
||||
import { flushPromises, mount, type VueWrapper } from '@vue/test-utils'
|
||||
import { createPinia, setActivePinia } from 'pinia'
|
||||
import { RouterView } from 'vue-router'
|
||||
import uiPlugin from '@nuxt/ui/vue-plugin'
|
||||
import UApp from '@nuxt/ui/components/App.vue'
|
||||
import router from '@/router'
|
||||
import { ApiError, useApi } from '@/composables/useApi'
|
||||
import { useSessionStore } from '@/stores/session'
|
||||
import { DESKTOP_WIDTH, setViewportWidth } from '@/components/__tests__/viewport'
|
||||
import { accessFor } from '@/test/access'
|
||||
|
||||
export interface Call {
|
||||
method: string
|
||||
path: string
|
||||
query: URLSearchParams
|
||||
body: Record<string, unknown> | undefined
|
||||
}
|
||||
|
||||
export type Route = unknown | ((call: Call) => unknown | Promise<unknown>)
|
||||
|
||||
export const kit: { wrapper: VueWrapper | null; calls: Call[] } = { wrapper: null, calls: [] }
|
||||
|
||||
export function failWith(status: number, body?: string): Route {
|
||||
return () => {
|
||||
throw new ApiError(status, undefined, body)
|
||||
}
|
||||
}
|
||||
|
||||
export function deferred<T>() {
|
||||
let resolve!: (value: T) => void
|
||||
const promise = new Promise<T>((done) => {
|
||||
resolve = done
|
||||
})
|
||||
return { promise, resolve }
|
||||
}
|
||||
|
||||
export const WRITE_FAILS = failWith(500, 'Internal server error')
|
||||
|
||||
export function callsTo(method: string, path: string) {
|
||||
return kit.calls.filter((call) => call.method === method && call.path === path)
|
||||
}
|
||||
|
||||
interface MountOptions {
|
||||
role?: string
|
||||
routes: Record<string, Route>
|
||||
width?: number
|
||||
}
|
||||
|
||||
export async function mountRoute(url: string, options: MountOptions) {
|
||||
setViewportWidth(options.width ?? DESKTOP_WIDTH)
|
||||
Element.prototype.scrollIntoView = vi.fn()
|
||||
setActivePinia(createPinia())
|
||||
kit.calls = []
|
||||
const session = useSessionStore()
|
||||
session.session = {
|
||||
verified: true,
|
||||
username: 'marta',
|
||||
id: 1,
|
||||
email: 'marta@renew.example',
|
||||
displayName: 'Marta Alves',
|
||||
access: accessFor(options.role ?? 'owner'),
|
||||
}
|
||||
session.status = 'ready'
|
||||
const request = vi.fn(async (path: string, init?: RequestInit) => {
|
||||
const [pathname = '', search = ''] = path.split('?')
|
||||
const call: Call = {
|
||||
method: init?.method ?? 'GET',
|
||||
path: pathname,
|
||||
query: new URLSearchParams(search),
|
||||
body: typeof init?.body === 'string' ? JSON.parse(init.body) : undefined,
|
||||
}
|
||||
kit.calls.push(call)
|
||||
const key = `${call.method} ${pathname}`
|
||||
if (!(key in options.routes)) {
|
||||
throw new Error(`Unexpected request: ${key}`)
|
||||
}
|
||||
const route = options.routes[key]
|
||||
return typeof route === 'function' ? route(call) : structuredClone(route)
|
||||
})
|
||||
vi.mocked(useApi).mockReturnValue({
|
||||
getMe: vi.fn().mockResolvedValue({ verified: true, username: 'marta', id: 1 }),
|
||||
request,
|
||||
requestPaged: vi.fn(),
|
||||
} as unknown as ReturnType<typeof useApi>)
|
||||
await router.push(url)
|
||||
kit.wrapper = mount(
|
||||
defineComponent({ render: () => h(UApp, null, { default: () => h(RouterView) }) }),
|
||||
{ attachTo: document.body, global: { plugins: [router, uiPlugin] } },
|
||||
)
|
||||
await settle()
|
||||
}
|
||||
|
||||
export async function settle() {
|
||||
for (let round = 0; round < 6; round++) {
|
||||
await flushPromises()
|
||||
}
|
||||
}
|
||||
|
||||
export async function unmountRoute() {
|
||||
kit.wrapper?.unmount()
|
||||
kit.wrapper = null
|
||||
await router.push('/login')
|
||||
document.body.innerHTML = ''
|
||||
}
|
||||
|
||||
export function text() {
|
||||
return (document.body.textContent ?? '').replace(/\s+/g, ' ').trim()
|
||||
}
|
||||
|
||||
export function dialogBox() {
|
||||
return document.body.querySelector<HTMLElement>('[role="dialog"], [role="alertdialog"]')
|
||||
}
|
||||
|
||||
export function dialogText() {
|
||||
return (dialogBox()?.textContent ?? '').replace(/\s+/g, ' ').trim()
|
||||
}
|
||||
|
||||
export function control(label: string | RegExp, root: ParentNode = document.body) {
|
||||
return Array.from(
|
||||
root.querySelectorAll<HTMLElement>('button, [role="menuitem"], a, [role="checkbox"]'),
|
||||
).find((element) => {
|
||||
const name = (element.getAttribute('aria-label') || element.textContent || '')
|
||||
.replace(/\s+/g, ' ')
|
||||
.trim()
|
||||
return typeof label === 'string' ? name === label : label.test(name)
|
||||
})
|
||||
}
|
||||
|
||||
export async function press(label: string | RegExp, root: ParentNode = document.body) {
|
||||
const target = control(label, root)
|
||||
if (!target) {
|
||||
throw new Error(`No control named ${String(label)}`)
|
||||
}
|
||||
target.click()
|
||||
await settle()
|
||||
}
|
||||
|
||||
export async function chooseAction(language: string, item: string) {
|
||||
await press(`Actions for ${language}`)
|
||||
await press(item)
|
||||
}
|
||||
|
||||
export async function typeInto(element: HTMLInputElement, value: string) {
|
||||
element.value = value
|
||||
element.dispatchEvent(new Event('input', { bubbles: true }))
|
||||
await settle()
|
||||
}
|
||||
|
||||
export function toastText() {
|
||||
return (
|
||||
document.body.querySelector(
|
||||
'[data-slot="viewport"], [role="region"][aria-label*="otification"]',
|
||||
)?.textContent ?? ''
|
||||
)
|
||||
}
|
||||
Reference in new issue
Block a user