/** DOM rendering and interaction helpers for panel tests. */ import { act } from 'react' import { createRoot, type Root } from 'react-dom/client' import { expect, vi } from 'vitest' import { App } from '../../src/client/app.tsx' import { usePageForm } from '../../src/client/deployment.tsx' import { zh } from '../../src/client/locales.ts' import type { T } from '../../src/client/text.ts' import { server, usePanelServer } from './panel-server.ts' declare global { var IS_REACT_ACT_ENVIRONMENT: boolean } globalThis.IS_REACT_ACT_ENVIRONMENT = true export const translator = (dictionary: Record): T => ((key: string, params?: Record) => { const template = dictionary[key] ?? key return params ? template.replace(/\{(\w+)\}/g, (match, name: string) => name in params ? String(params[name]) : match) : template }) as T function Panel({ t }: { t: T }) { return { server.opened.push(id) }} deploymentForm={usePageForm(server.form)} /> } let root: Root | undefined let container: HTMLElement | undefined function unmount() { act(() => { root?.unmount() }) root = undefined container?.remove() } export const usePanel = () => usePanelServer(unmount) /** Flush pending fake-server responses and React state updates. */ export async function settle() { await vi.waitFor(async () => { await act(async () => { await new Promise(resolve => setTimeout(resolve, 0)) }) expect(server.pending).toBe(0) }) await act(async () => { await new Promise(resolve => setTimeout(resolve, 0)) }) } export async function mount(dictionary: Record = zh) { container = document.createElement('div'); document.body.append(container) root = createRoot(container) await act(async () => { root!.render() }) await settle() } export async function remount(dictionary: Record = zh) { unmount(); await mount(dictionary) } export const all = (selector: string) => [...document.querySelectorAll(selector)] const CONTROLS = 'button, [role="button"], [role="tab"], [role="switch"], a' export function find(name: string, selector = CONTROLS): HTMLElement { const match = all(selector).find(element => element.textContent?.trim() === name || element.getAttribute('aria-label') === name) if (!match) throw new Error(`No ${selector} named ${name}`) return match } export const tab = (name: string) => find(name, '[role="tab"]') export async function click(target: string | HTMLElement) { const element = typeof target === 'string' ? find(target) : target await act(async () => { element.click() }) await settle() } export async function type(element: HTMLInputElement | HTMLTextAreaElement | HTMLSelectElement, value: string) { const prototype = element instanceof HTMLTextAreaElement ? HTMLTextAreaElement.prototype : element instanceof HTMLSelectElement ? HTMLSelectElement.prototype : HTMLInputElement.prototype await act(async () => { Object.getOwnPropertyDescriptor(prototype, 'value')!.set!.call(element, value) element.dispatchEvent(new Event(element instanceof HTMLSelectElement ? 'change' : 'input', { bubbles: true })) }) await settle() } /** The page's text without its stylesheet. */ export const text = () => [...document.body.querySelectorAll('style')].reduce((all, style) => all.replace(style.textContent ?? '', ''), document.body.textContent ?? '') export function field(label: string): E { const labelled = (element: HTMLElement) => !!element.id && all('label').some(item => (item as HTMLLabelElement).htmlFor === element.id && item.textContent === label) const match = all('input, textarea, select').find(element => element.getAttribute('aria-label') === label || labelled(element)) if (!match) throw new Error(`No field ${label}`) return match as unknown as E } export const at = (route: object) => sessionStorage.setItem('dsh-superagent/route', JSON.stringify({ route, stack: [] }))