Files
RenewCMS/web/src/views/__tests__/localizationKit.ts
T

161 lines
4.7 KiB
TypeScript

import { vi } from 'vitest'
import { defineComponent, h, type Component } 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 type { ApiErrorDetails } from '@/lib/apiError'
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, details?: ApiErrorDetails): Route {
return () => {
throw new ApiError(status, undefined, body, details)
}
}
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
userId?: number
root?: Component
}
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: options.userId ?? 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)
const root =
options.root ??
defineComponent({ render: () => h(UApp, null, { default: () => h(RouterView) }) })
kit.wrapper = mount(root, { 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 ?? ''
)
}