@fairfox/polly 0.75.1 → 0.75.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/src/actions/index.js +4 -2
- package/dist/src/actions/index.js.map +3 -3
- package/dist/src/polly-ui/index.css +8 -14
- package/dist/src/polly-ui/styles.css +8 -14
- package/dist/src/polly-ui/theme.css +5 -6
- package/dist/tools/test/src/browser/harness.d.ts +9 -1
- package/dist/tools/test/src/browser/index.d.ts +4 -3
- package/dist/tools/test/src/browser/index.js +6 -3
- package/dist/tools/test/src/browser/index.js.map +3 -3
- package/dist/tools/test/src/browser/run.js +17 -38
- package/dist/tools/test/src/browser/run.js.map +4 -4
- package/dist/tools/test/src/browser/runner-core.d.ts +9 -13
- package/package.json +1 -1
|
@@ -156,7 +156,7 @@ function parseActionData(element) {
|
|
|
156
156
|
return data;
|
|
157
157
|
}
|
|
158
158
|
function closeTopOverlay2() {
|
|
159
|
-
const overlays = document.querySelectorAll("[data-overlay-id]");
|
|
159
|
+
const overlays = document.querySelectorAll("[data-overlay-id]:popover-open, dialog[data-overlay-id][open]");
|
|
160
160
|
if (overlays.length === 0)
|
|
161
161
|
return;
|
|
162
162
|
const topOverlay2 = overlays[overlays.length - 1];
|
|
@@ -221,6 +221,8 @@ function installEventDelegation(onDispatch, options = {}) {
|
|
|
221
221
|
const clickedOverlay = event.target.closest("[data-overlay-id]");
|
|
222
222
|
if (clickedOverlay)
|
|
223
223
|
return;
|
|
224
|
+
if (event.target.closest("button[popovertarget]"))
|
|
225
|
+
return;
|
|
224
226
|
if (options.onOutsideOverlayClick) {
|
|
225
227
|
options.onOutsideOverlayClick();
|
|
226
228
|
} else {
|
|
@@ -522,4 +524,4 @@ export {
|
|
|
522
524
|
ACTION_EVENT_TYPES
|
|
523
525
|
};
|
|
524
526
|
|
|
525
|
-
//# debugId=
|
|
527
|
+
//# debugId=ABA512A41C3BA87D64756E2164756E21
|
|
@@ -4,12 +4,12 @@
|
|
|
4
4
|
"sourcesContent": [
|
|
5
5
|
"/**\n * Global error surface for action handlers.\n *\n * Actions that fail set the errorState signal via `submitError`; a `<Toast>`\n * component consumes that signal and renders a dismissable message. Handlers\n * that catch expected failures (validation, quota) may call `setError` directly.\n */\n\nimport { signal } from \"@preact/signals\";\n\nexport type ErrorSeverity = \"error\" | \"warning\" | \"info\";\n\nexport type ErrorEntry = {\n id: string;\n message: string;\n severity: ErrorSeverity;\n action?: string;\n createdAt: number;\n};\n\nexport const errorState = signal<ErrorEntry[]>([]);\n\nlet nextId = 0;\nfunction allocId(): string {\n nextId += 1;\n return `polly-err-${nextId}`;\n}\n\nexport function setError(\n message: string,\n opts: { severity?: ErrorSeverity; action?: string } = {}\n): string {\n const entry: ErrorEntry = {\n id: allocId(),\n message,\n severity: opts.severity ?? \"error\",\n action: opts.action,\n createdAt: Date.now(),\n };\n errorState.value = [...errorState.value, entry];\n return entry.id;\n}\n\nexport function clearError(id?: string): void {\n if (id === undefined) {\n errorState.value = [];\n return;\n }\n errorState.value = errorState.value.filter((e) => e.id !== id);\n}\n\n/**\n * Convenience wrapper for an action's catch block.\n * Logs the action name + error and surfaces a user-visible message.\n */\nexport function submitError(action: string, err: unknown): string {\n const message = err instanceof Error ? err.message : String(err);\n return setError(message, { action, severity: \"error\" });\n}\n",
|
|
6
6
|
"/**\n * Overlay registry.\n *\n * Tracks the stack of open overlays (modals, popovers, confirm dialogs) so\n * Escape can close the topmost and `data-overlay-id`-based DOM scans have\n * a mirror in memory. Each entry stores an id plus an optional close\n * callback; `closeTopOverlay` invokes the callback and pops the entry.\n */\n\nimport { signal } from \"@preact/signals\";\n\nexport type OverlayEntry = {\n id: string;\n onClose?: () => void;\n};\n\nconst stack = signal<OverlayEntry[]>([]);\n\n/** Current overlay stack as a read-only snapshot. */\nexport function overlayStack(): readonly OverlayEntry[] {\n return stack.value;\n}\n\n/** Is any overlay currently open? */\nexport function hasOpenOverlay(): boolean {\n return stack.value.length > 0;\n}\n\n/** The top of the stack, or undefined if empty. */\nexport function topOverlay(): OverlayEntry | undefined {\n const s = stack.value;\n return s[s.length - 1];\n}\n\n/** Push an overlay onto the stack. Returns the popped entry when closed. */\nexport function pushOverlay(entry: OverlayEntry): void {\n stack.value = [...stack.value, entry];\n}\n\n/** Pop a specific overlay by id (or the top if no id given). */\nexport function popOverlay(id?: string): OverlayEntry | undefined {\n const s = stack.value;\n if (s.length === 0) return undefined;\n if (id === undefined) {\n const top = s[s.length - 1];\n stack.value = s.slice(0, -1);\n return top;\n }\n const idx = s.findIndex((e) => e.id === id);\n if (idx === -1) return undefined;\n const entry = s[idx];\n stack.value = [...s.slice(0, idx), ...s.slice(idx + 1)];\n return entry;\n}\n\n/** Close the top overlay by calling its onClose and popping it. */\nexport function closeTopOverlay(): OverlayEntry | undefined {\n const top = topOverlay();\n if (!top) return undefined;\n top.onClose?.();\n return popOverlay(top.id);\n}\n\n/** Reset the stack. Intended for tests. */\nexport function resetOverlayStack(): void {\n stack.value = [];\n}\n",
|
|
7
|
-
"/**\n * Event delegation core.\n *\n * One document listener dispatches `data-action` events to typed handlers.\n * Walks up the DOM with `closest('[data-action]')`, parses `data-action-*`\n * attributes into a camelCase object, and hands the dispatch to the caller.\n *\n * Forms are skipped on click — a `<form data-action=\"...\">` responds to\n * submit only, so clicks on form children don't bubble into its action.\n * Escape closes the topmost overlay by calling the overlay registry.\n * Enter/Space on non-interactive elements with `data-action` fire the\n * action (Space is prevented to stop page scroll). Click outside any\n * `[data-overlay-id]` element also pops the top overlay.\n */\n\nimport { closeTopOverlay as closeTopRegistryOverlay } from \"./overlay.ts\";\n\n/** Elements that natively fire click on Enter/Space. */\nexport const INTERACTIVE_TAGS = new Set([\"BUTTON\", \"A\", \"INPUT\", \"SELECT\", \"TEXTAREA\"]);\n\n/** Event types that may trigger a data-action dispatch. */\nexport const ACTION_EVENT_TYPES = new Set([\"click\", \"submit\", \"change\", \"input\"]);\n\n/** Parsed action dispatch — what the runtime resolves before invoking a handler. */\nexport type ActionDispatch = {\n action: string;\n element: HTMLElement;\n event: Event;\n data: Record<string, string>;\n};\n\n/**\n * Parse `data-action-*` attributes into camelCase key-value pairs.\n *\n * `data-action-text-set-id=\"42\"` becomes `{ textSetId: \"42\" }`.\n */\nexport function parseActionData(element: HTMLElement): Record<string, string> {\n const data: Record<string, string> = {};\n for (const attr of Array.from(element.attributes)) {\n if (attr.name.startsWith(\"data-action-\")) {\n const key = attr.name\n .replace(\"data-action-\", \"\")\n .replace(/-([a-z])/g, (_m: string, letter: string) => letter.toUpperCase());\n data[key] = attr.value;\n }\n }\n return data;\n}\n\n/**\n * Close the topmost overlay by dispatching `overlay:close` on its element.\n * Overlays mark themselves with `data-overlay-id`.\n */\nexport function closeTopOverlay(): void {\n const overlays = document.querySelectorAll(\"[data-overlay-id]\");\n if (overlays.length === 0) return;\n const topOverlay = overlays[overlays.length - 1];\n if (!topOverlay) return;\n topOverlay.dispatchEvent(\n new CustomEvent(\"overlay:close\", {\n bubbles: true,\n detail: { id: topOverlay.getAttribute(\"data-overlay-id\") },\n })\n );\n}\n\n/**\n * Resolve a DOM event to an ActionDispatch, or null if no data-action matches.\n */\nexport function resolveAction(event: Event): ActionDispatch | null {\n const target = event.target;\n if (!(target instanceof Element)) return null;\n const actionElement = target.closest(\"[data-action]\");\n if (!(actionElement instanceof HTMLElement)) return null;\n if (event.type === \"click\" && actionElement.tagName === \"FORM\") return null;\n const action = actionElement.getAttribute(\"data-action\");\n if (!action) return null;\n return {\n action,\n element: actionElement,\n event,\n data: parseActionData(actionElement),\n };\n}\n\n/**\n * Install document-level listeners for the delegation system.\n * Returns a cleanup function that removes every listener it installed.\n */\nexport function installEventDelegation(\n onDispatch: (dispatch: ActionDispatch) => void,\n options: { onEscape?: () => void; onOutsideOverlayClick?: () => void } = {}\n): () => void {\n const handleActionEvent = (event: Event) => {\n const dispatch = resolveAction(event);\n if (dispatch) onDispatch(dispatch);\n };\n\n const handleKeyDown = (event: KeyboardEvent) => {\n if (event.key === \"Escape\") {\n if (options.onEscape) {\n options.onEscape();\n } else {\n closeTopRegistryOverlay();\n closeTopOverlay();\n }\n return;\n }\n if (event.key === \"Enter\" || event.key === \" \") {\n const target = event.target;\n if (!(target instanceof Element)) return;\n if (INTERACTIVE_TAGS.has(target.tagName)) return;\n const dispatch = resolveAction(event);\n if (dispatch) {\n event.preventDefault();\n onDispatch(dispatch);\n }\n }\n };\n\n const handleMouseDown = (event: MouseEvent) => {\n if (!(event.target instanceof Element)) return;\n const clickedOverlay = event.target.closest(\"[data-overlay-id]\");\n if (clickedOverlay) return;\n if (options.onOutsideOverlayClick) {\n options.onOutsideOverlayClick();\n } else {\n closeTopRegistryOverlay();\n closeTopOverlay();\n }\n };\n\n for (const eventType of ACTION_EVENT_TYPES) {\n document.addEventListener(eventType, handleActionEvent);\n }\n document.addEventListener(\"keydown\", handleKeyDown);\n document.addEventListener(\"mousedown\", handleMouseDown);\n\n return () => {\n for (const eventType of ACTION_EVENT_TYPES) {\n document.removeEventListener(eventType, handleActionEvent);\n }\n document.removeEventListener(\"keydown\", handleKeyDown);\n document.removeEventListener(\"mousedown\", handleMouseDown);\n };\n}\n",
|
|
7
|
+
"/**\n * Event delegation core.\n *\n * One document listener dispatches `data-action` events to typed handlers.\n * Walks up the DOM with `closest('[data-action]')`, parses `data-action-*`\n * attributes into a camelCase object, and hands the dispatch to the caller.\n *\n * Forms are skipped on click — a `<form data-action=\"...\">` responds to\n * submit only, so clicks on form children don't bubble into its action.\n * Escape closes the topmost overlay by calling the overlay registry.\n * Enter/Space on non-interactive elements with `data-action` fire the\n * action (Space is prevented to stop page scroll). Click outside any\n * `[data-overlay-id]` element also pops the top overlay.\n */\n\nimport { closeTopOverlay as closeTopRegistryOverlay } from \"./overlay.ts\";\n\n/** Elements that natively fire click on Enter/Space. */\nexport const INTERACTIVE_TAGS = new Set([\"BUTTON\", \"A\", \"INPUT\", \"SELECT\", \"TEXTAREA\"]);\n\n/** Event types that may trigger a data-action dispatch. */\nexport const ACTION_EVENT_TYPES = new Set([\"click\", \"submit\", \"change\", \"input\"]);\n\n/** Parsed action dispatch — what the runtime resolves before invoking a handler. */\nexport type ActionDispatch = {\n action: string;\n element: HTMLElement;\n event: Event;\n data: Record<string, string>;\n};\n\n/**\n * Parse `data-action-*` attributes into camelCase key-value pairs.\n *\n * `data-action-text-set-id=\"42\"` becomes `{ textSetId: \"42\" }`.\n */\nexport function parseActionData(element: HTMLElement): Record<string, string> {\n const data: Record<string, string> = {};\n for (const attr of Array.from(element.attributes)) {\n if (attr.name.startsWith(\"data-action-\")) {\n const key = attr.name\n .replace(\"data-action-\", \"\")\n .replace(/-([a-z])/g, (_m: string, letter: string) => letter.toUpperCase());\n data[key] = attr.value;\n }\n }\n return data;\n}\n\n/**\n * Close the topmost overlay by dispatching `overlay:close` on its element.\n * Overlays mark themselves with `data-overlay-id`.\n */\nexport function closeTopOverlay(): void {\n const overlays = document.querySelectorAll(\n \"[data-overlay-id]:popover-open, dialog[data-overlay-id][open]\"\n );\n if (overlays.length === 0) return;\n const topOverlay = overlays[overlays.length - 1];\n if (!topOverlay) return;\n topOverlay.dispatchEvent(\n new CustomEvent(\"overlay:close\", {\n bubbles: true,\n detail: { id: topOverlay.getAttribute(\"data-overlay-id\") },\n })\n );\n}\n\n/**\n * Resolve a DOM event to an ActionDispatch, or null if no data-action matches.\n */\nexport function resolveAction(event: Event): ActionDispatch | null {\n const target = event.target;\n if (!(target instanceof Element)) return null;\n const actionElement = target.closest(\"[data-action]\");\n if (!(actionElement instanceof HTMLElement)) return null;\n if (event.type === \"click\" && actionElement.tagName === \"FORM\") return null;\n const action = actionElement.getAttribute(\"data-action\");\n if (!action) return null;\n return {\n action,\n element: actionElement,\n event,\n data: parseActionData(actionElement),\n };\n}\n\n/**\n * Install document-level listeners for the delegation system.\n * Returns a cleanup function that removes every listener it installed.\n */\nexport function installEventDelegation(\n onDispatch: (dispatch: ActionDispatch) => void,\n options: { onEscape?: () => void; onOutsideOverlayClick?: () => void } = {}\n): () => void {\n const handleActionEvent = (event: Event) => {\n const dispatch = resolveAction(event);\n if (dispatch) onDispatch(dispatch);\n };\n\n const handleKeyDown = (event: KeyboardEvent) => {\n if (event.key === \"Escape\") {\n if (options.onEscape) {\n options.onEscape();\n } else {\n closeTopRegistryOverlay();\n closeTopOverlay();\n }\n return;\n }\n if (event.key === \"Enter\" || event.key === \" \") {\n const target = event.target;\n if (!(target instanceof Element)) return;\n if (INTERACTIVE_TAGS.has(target.tagName)) return;\n const dispatch = resolveAction(event);\n if (dispatch) {\n event.preventDefault();\n onDispatch(dispatch);\n }\n }\n };\n\n const handleMouseDown = (event: MouseEvent) => {\n if (!(event.target instanceof Element)) return;\n const clickedOverlay = event.target.closest(\"[data-overlay-id]\");\n if (clickedOverlay) return;\n if (event.target.closest(\"button[popovertarget]\")) return;\n if (options.onOutsideOverlayClick) {\n options.onOutsideOverlayClick();\n } else {\n closeTopRegistryOverlay();\n closeTopOverlay();\n }\n };\n\n for (const eventType of ACTION_EVENT_TYPES) {\n document.addEventListener(eventType, handleActionEvent);\n }\n document.addEventListener(\"keydown\", handleKeyDown);\n document.addEventListener(\"mousedown\", handleMouseDown);\n\n return () => {\n for (const eventType of ACTION_EVENT_TYPES) {\n document.removeEventListener(eventType, handleActionEvent);\n }\n document.removeEventListener(\"keydown\", handleKeyDown);\n document.removeEventListener(\"mousedown\", handleMouseDown);\n };\n}\n",
|
|
8
8
|
"/**\n * Form primitive.\n *\n * `createForm` returns a typed form store: per-field signals, an aggregated\n * values signal, open/close/submit methods, and three auto-registered action\n * handlers (`{name}:open`, `{name}:close`, `{name}:submit`) that callers spread\n * into their global action registry.\n *\n * Lifecycle:\n * open → resets fields to initialValues, applies onOpen overrides,\n * sets isOpen = true, records data-action-* data in openParams\n * typing → uncontrolled inputs keep their own state; controlled inputs\n * write fields.X.value directly\n * submit → reads FormData from the form element, merges into fields,\n * runs optional validate, calls user onSubmit with final values,\n * closes on success, sets errorState on failure\n * close → resets and sets isOpen = false\n * guard → autonomous effect; if guard() returns false while open, close()\n */\n\nimport { computed, effect, type ReadonlySignal, type Signal, signal } from \"@preact/signals\";\nimport { submitError } from \"./error.ts\";\nimport type { ActionRegistry } from \"./registry.ts\";\n\nfunction isFormElement(target: EventTarget | null): target is HTMLFormElement {\n if (!target) return false;\n if (typeof HTMLFormElement !== \"undefined\") {\n return target instanceof HTMLFormElement;\n }\n const t = target as unknown as { tagName?: unknown };\n return typeof t.tagName === \"string\" && t.tagName === \"FORM\";\n}\n\nexport type FormOpenContext<TStores> = {\n data: Record<string, string>;\n stores: TStores;\n};\n\nexport type FormSubmitContext<TValues, TStores> = {\n values: TValues;\n stores: TStores;\n};\n\nexport type FormConfig<TValues extends Record<string, string>, TStores> = {\n /** Used as action namespace: `{name}:open`, `{name}:close`, `{name}:submit`. */\n name: string;\n initialValues: TValues;\n onSubmit: (ctx: FormSubmitContext<TValues, TStores>) => void | Promise<void>;\n /** Invoked on open; return partial overrides to pre-populate fields. */\n onOpen?: (ctx: FormOpenContext<TStores>) => Partial<TValues> | undefined;\n /** Returns false while open → form auto-closes (entity-deletion guard). */\n guard?: (ctx: { stores: TStores }) => boolean;\n /** Synchronous validation. Returning keys blocks submit. */\n validate?: (values: TValues) => Partial<Record<keyof TValues, string>> | null;\n};\n\nexport type FormStore<TValues extends Record<string, string>, TStores> = {\n readonly name: string;\n readonly isOpen: ReadonlySignal<boolean>;\n readonly values: ReadonlySignal<TValues>;\n readonly fields: { [K in keyof TValues]: Signal<TValues[K]> };\n readonly errors: ReadonlySignal<Partial<Record<keyof TValues, string>>>;\n readonly isSubmitting: ReadonlySignal<boolean>;\n readonly openParams: ReadonlySignal<Record<string, string>>;\n open(override?: Partial<TValues>, params?: Record<string, string>): void;\n close(): void;\n submit(event?: Event): Promise<void>;\n /** Late-binds stores; required before guard/onOpen/onSubmit can access stores. */\n bindStores(getStores: () => TStores): void;\n /** Action handler entries. Spread into the user's ActionRegistry. */\n actions: ActionRegistry<TStores>;\n};\n\nexport function createForm<TValues extends Record<string, string>, TStores>(\n config: FormConfig<TValues, TStores>\n): FormStore<TValues, TStores> {\n const fieldKeys = Object.keys(config.initialValues) as unknown as (keyof TValues)[];\n\n const fields = {} as unknown as { [K in keyof TValues]: Signal<TValues[K]> };\n for (const key of fieldKeys) {\n fields[key] = signal(config.initialValues[key]);\n }\n\n const values = computed<TValues>(() => {\n const v = {} as unknown as TValues;\n for (const key of fieldKeys) {\n v[key] = fields[key].value;\n }\n return v;\n });\n\n const isOpen = signal(false);\n const isSubmitting = signal(false);\n const errors = signal<Partial<Record<keyof TValues, string>>>({});\n const openParams = signal<Record<string, string>>({});\n\n let getStoresRef: (() => TStores) | null = null;\n let disposeGuardEffect: (() => void) | null = null;\n\n function getStoresOrThrow(): TStores {\n if (!getStoresRef) {\n throw new Error(\n `Form \"${config.name}\" used before bindStores(). Call form.bindStores(() => yourStores) at app init.`\n );\n }\n return getStoresRef();\n }\n\n function resetToInitial(): void {\n for (const key of fieldKeys) {\n fields[key].value = config.initialValues[key];\n }\n errors.value = {};\n }\n\n // biome-ignore lint/complexity/noExcessiveCognitiveComplexity: open applies two layers of overrides plus optional onOpen — branchy by design.\n function open(override?: Partial<TValues>, params: Record<string, string> = {}): void {\n resetToInitial();\n openParams.value = params;\n if (config.onOpen && getStoresRef) {\n const onOpenOverride = config.onOpen({ data: params, stores: getStoresOrThrow() }) ?? {};\n for (const [k, v] of Object.entries(onOpenOverride)) {\n if (k in fields) {\n fields[k as keyof TValues].value = v as TValues[keyof TValues];\n }\n }\n }\n if (override) {\n for (const [k, v] of Object.entries(override)) {\n if (k in fields) {\n fields[k as keyof TValues].value = v as TValues[keyof TValues];\n }\n }\n }\n isOpen.value = true;\n }\n\n function close(): void {\n isOpen.value = false;\n resetToInitial();\n openParams.value = {};\n }\n\n // biome-ignore lint/complexity/noExcessiveCognitiveComplexity: submit funnels FormData, validate, onSubmit, errorState, and close — one-path-per-stage by design.\n async function submit(event?: Event): Promise<void> {\n if (event) event.preventDefault();\n if (event && isFormElement(event.target)) {\n const fd = new FormData(event.target);\n for (const key of fieldKeys) {\n const raw = fd.get(key as unknown as string);\n if (raw !== null) {\n fields[key].value = String(raw) as unknown as TValues[typeof key];\n }\n }\n }\n const current = values.value;\n const validationErrors = config.validate?.(current);\n if (validationErrors && Object.keys(validationErrors).length > 0) {\n errors.value = validationErrors;\n return;\n }\n errors.value = {};\n isSubmitting.value = true;\n try {\n await config.onSubmit({\n values: current,\n stores: getStoresOrThrow(),\n });\n close();\n } catch (err) {\n submitError(`${config.name}:submit`, err);\n } finally {\n isSubmitting.value = false;\n }\n }\n\n function bindStores(getStores: () => TStores): void {\n getStoresRef = getStores;\n disposeGuardEffect?.();\n const guardFn = config.guard;\n if (guardFn) {\n disposeGuardEffect = effect(() => {\n if (!isOpen.value) return;\n if (!guardFn({ stores: getStores() })) close();\n });\n }\n }\n\n const actions: ActionRegistry<TStores> = {\n [`${config.name}:open`]: ({ data }) => {\n open(undefined, data);\n },\n [`${config.name}:close`]: () => {\n close();\n },\n [`${config.name}:submit`]: async ({ event }) => {\n await submit(event);\n },\n };\n\n return {\n name: config.name,\n isOpen,\n values,\n fields,\n errors,\n isSubmitting,\n openParams,\n open,\n close,\n submit,\n bindStores,\n actions,\n };\n}\n\n/**\n * Compose many forms into a single set: merged actions, closeAll, openForm signal.\n */\nexport type FormSet<TStores> = {\n actions: ActionRegistry<TStores>;\n openForm: ReadonlySignal<string | null>;\n closeAll(): void;\n bindStores(getStores: () => TStores): void;\n};\n\nexport function createFormSet<TStores>(\n forms: readonly FormStore<Record<string, string>, TStores>[]\n): FormSet<TStores> {\n const merged: ActionRegistry<TStores> = {};\n for (const form of forms) {\n Object.assign(merged, form.actions);\n }\n\n const openForm = computed<string | null>(() => {\n for (const form of forms) {\n if (form.isOpen.value) return form.name;\n }\n return null;\n });\n\n return {\n actions: merged,\n openForm,\n closeAll() {\n for (const form of forms) form.close();\n },\n bindStores(getStores) {\n for (const form of forms) form.bindStores(getStores);\n },\n };\n}\n",
|
|
9
9
|
"/**\n * Store base + Preact context.\n *\n * `createStore` is a thin convention-enforcer: a typed bag of signals\n * plus methods. Apps use it to define domain stores whose only public\n * mutation surface is named methods (actions call them; components read\n * signals and dispatch actions). No cleverness — a named pattern more\n * than a library.\n *\n * `StoreProvider` + `useStores` wire the stores bag into Preact context\n * so components and the delegation runtime can reach it via a single hook.\n */\n\nimport type { ComponentChildren } from \"preact\";\nimport { createContext } from \"preact\";\nimport { useContext } from \"preact/hooks\";\n\n/**\n * Identity helper; exists so domain stores have a canonical creation\n * call even though the bag itself is a plain object. Centralising here\n * lets us hang additional behaviour off the call site later (debug\n * instrumentation, devtools) without a migration.\n */\nexport function createStore<T extends object>(init: T): T {\n return init;\n}\n\nconst StoreContext = createContext<unknown>(null);\n\nexport type StoreProviderProps<TStores> = {\n children: ComponentChildren;\n stores: TStores;\n};\n\nexport function StoreProvider<TStores>({ children, stores }: StoreProviderProps<TStores>) {\n return <StoreContext.Provider value={stores}>{children}</StoreContext.Provider>;\n}\n\nexport function useStores<TStores>(): TStores {\n const ctx = useContext(StoreContext) as unknown as TStores | null;\n if (ctx === null) {\n throw new Error(\"useStores must be used within a StoreProvider\");\n }\n return ctx;\n}\n",
|
|
10
10
|
"/**\n * Testing helpers for action handlers.\n *\n * Handlers are plain functions taking an ActionContext; these helpers\n * build the context pieces without jsdom. For full-DOM tests use the\n * browser harness at `@fairfox/polly/test/browser`.\n */\n\nimport type { ActionContext, ActionHandler, ActionRegistry } from \"./registry.ts\";\n\n/**\n * Build a mock element that satisfies ActionContext.element.\n * Only the surface a handler is likely to touch is populated.\n */\nexport function createMockElement(\n attrs: Record<string, string> = {},\n tagName = \"DIV\"\n): HTMLElement {\n const attrMap = new Map(Object.entries(attrs));\n const el = {\n tagName,\n nodeType: 1,\n getAttribute: (name: string) => attrMap.get(name) ?? null,\n setAttribute: (name: string, value: string) => {\n attrMap.set(name, value);\n },\n hasAttribute: (name: string) => attrMap.has(name),\n removeAttribute: (name: string) => {\n attrMap.delete(name);\n },\n attributes: Array.from(attrMap.entries()).map(([name, value]) => ({\n name,\n value,\n })),\n } as unknown as HTMLElement;\n return el;\n}\n\n/** Build a minimal submit-like event wrapping a `<form>` FormData payload. */\nexport function createMockSubmitEvent(form: HTMLFormElement | Record<string, string>): Event {\n let target: HTMLFormElement;\n if (typeof HTMLFormElement !== \"undefined\" && form instanceof HTMLFormElement) {\n target = form;\n } else {\n target = createMockFormElement(form as unknown as Record<string, string>);\n }\n let defaultPrevented = false;\n return {\n type: \"submit\",\n target,\n currentTarget: target,\n preventDefault() {\n defaultPrevented = true;\n },\n stopPropagation() {\n /* noop */\n },\n get defaultPrevented() {\n return defaultPrevented;\n },\n } as unknown as Event;\n}\n\nfunction createMockFormElement(fields: Record<string, string>): HTMLFormElement {\n return {\n nodeType: 1,\n tagName: \"FORM\",\n elements: Object.entries(fields).map(([name, value]) => ({\n name,\n value,\n })),\n } as unknown as HTMLFormElement;\n}\n\n/**\n * Shallow-merged partial stores. Callers typically pass signal-backed fakes.\n */\nexport function createMockStores<TStores extends object>(partial: Partial<TStores> = {}): TStores {\n return partial as unknown as TStores;\n}\n\n/**\n * Run a handler in isolation. Useful for unit-testing action logic without\n * wiring the full document event delegation.\n */\nexport async function runAction<TStores>(\n registry: ActionRegistry<TStores>,\n action: string,\n ctx: Partial<ActionContext<TStores>> & { stores: TStores }\n): Promise<void> {\n const handler: ActionHandler<TStores> | undefined = registry[action];\n if (!handler) {\n throw new Error(`No handler registered for action \"${action}\"`);\n }\n const fullCtx: ActionContext<TStores> = {\n stores: ctx.stores,\n event: ctx.event ?? new Event(\"click\"),\n element: ctx.element ?? createMockElement(),\n data: ctx.data ?? {},\n };\n await handler(fullCtx);\n}\n"
|
|
11
11
|
],
|
|
12
|
-
"mappings": "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
|
|
13
|
-
"debugId": "
|
|
12
|
+
"mappings": "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",
|
|
13
|
+
"debugId": "ABA512A41C3BA87D64756E2164756E21",
|
|
14
14
|
"names": []
|
|
15
15
|
}
|
|
@@ -575,9 +575,6 @@
|
|
|
575
575
|
}
|
|
576
576
|
|
|
577
577
|
.summary_sEhnPw {
|
|
578
|
-
display: flex;
|
|
579
|
-
align-items: center;
|
|
580
|
-
gap: var(--polly-space-sm);
|
|
581
578
|
padding: var(--polly-space-sm) var(--polly-space-md);
|
|
582
579
|
cursor: pointer;
|
|
583
580
|
font-family: inherit;
|
|
@@ -593,19 +590,16 @@
|
|
|
593
590
|
display: none;
|
|
594
591
|
}
|
|
595
592
|
|
|
596
|
-
.summary_sEhnPw:
|
|
597
|
-
content:
|
|
598
|
-
display: block;
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
transition: transform var(--polly-motion-fast)
|
|
602
|
-
flex: none;
|
|
603
|
-
margin-inline-start: auto;
|
|
604
|
-
line-height: 1;
|
|
593
|
+
.summary_sEhnPw:before {
|
|
594
|
+
content: "▶";
|
|
595
|
+
display: inline-block;
|
|
596
|
+
margin-right: var(--polly-space-sm);
|
|
597
|
+
font-size: var(--polly-text-xs);
|
|
598
|
+
transition: transform var(--polly-motion-fast);
|
|
605
599
|
}
|
|
606
600
|
|
|
607
|
-
.collapsible_sEhnPw[open] > .summary_sEhnPw:
|
|
608
|
-
transform: rotate(
|
|
601
|
+
.collapsible_sEhnPw[open] > .summary_sEhnPw:before {
|
|
602
|
+
transform: rotate(90deg);
|
|
609
603
|
}
|
|
610
604
|
|
|
611
605
|
.summary_sEhnPw:hover {
|
|
@@ -764,9 +764,6 @@
|
|
|
764
764
|
}
|
|
765
765
|
|
|
766
766
|
.summary_sEhnPw {
|
|
767
|
-
display: flex;
|
|
768
|
-
align-items: center;
|
|
769
|
-
gap: var(--polly-space-sm);
|
|
770
767
|
padding: var(--polly-space-sm) var(--polly-space-md);
|
|
771
768
|
cursor: pointer;
|
|
772
769
|
font-family: inherit;
|
|
@@ -782,19 +779,16 @@
|
|
|
782
779
|
display: none;
|
|
783
780
|
}
|
|
784
781
|
|
|
785
|
-
.summary_sEhnPw:
|
|
786
|
-
content:
|
|
787
|
-
display: block;
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
transition: transform var(--polly-motion-fast)
|
|
791
|
-
flex: none;
|
|
792
|
-
margin-inline-start: auto;
|
|
793
|
-
line-height: 1;
|
|
782
|
+
.summary_sEhnPw:before {
|
|
783
|
+
content: "▶";
|
|
784
|
+
display: inline-block;
|
|
785
|
+
margin-right: var(--polly-space-sm);
|
|
786
|
+
font-size: var(--polly-text-xs);
|
|
787
|
+
transition: transform var(--polly-motion-fast);
|
|
794
788
|
}
|
|
795
789
|
|
|
796
|
-
.collapsible_sEhnPw[open] > .summary_sEhnPw:
|
|
797
|
-
transform: rotate(
|
|
790
|
+
.collapsible_sEhnPw[open] > .summary_sEhnPw:before {
|
|
791
|
+
transform: rotate(90deg);
|
|
798
792
|
}
|
|
799
793
|
|
|
800
794
|
.summary_sEhnPw:hover {
|
|
@@ -80,13 +80,12 @@
|
|
|
80
80
|
--polly-control-height-md: 2.25rem;
|
|
81
81
|
--polly-control-height-lg: 2.75rem;
|
|
82
82
|
|
|
83
|
-
/* Disclosure caret
|
|
84
|
-
*
|
|
85
|
-
*
|
|
86
|
-
*
|
|
87
|
-
* turn instead of a half turn (polly#137). */
|
|
83
|
+
/* Disclosure caret for dropdown-style triggers (Select, ActionSelect).
|
|
84
|
+
* Collapsible runs its own left-side marker — that's a different
|
|
85
|
+
* affordance from a dropdown's right-side ▾ and shouldn't track
|
|
86
|
+
* these tokens (polly#137). */
|
|
88
87
|
--polly-caret-glyph: "\25BE";
|
|
89
|
-
--polly-caret-size: var(--polly-text-
|
|
88
|
+
--polly-caret-size: var(--polly-text-xl);
|
|
90
89
|
--polly-caret-color: var(--polly-text-muted);
|
|
91
90
|
|
|
92
91
|
/* Motion */
|
|
@@ -69,7 +69,15 @@ export declare function flush(ms?: number): Promise<void>;
|
|
|
69
69
|
*/
|
|
70
70
|
export declare function cleanup(container: Element): void;
|
|
71
71
|
/**
|
|
72
|
-
* Run all registered tests and
|
|
72
|
+
* Run all registered tests and report the tally to the Node-side runner.
|
|
73
|
+
*
|
|
74
|
+
* The runner injects `window.__pollyReport` (via Puppeteer's
|
|
75
|
+
* `page.exposeFunction`) before navigating, so reporting is a single
|
|
76
|
+
* push from the page to the runner — no polling, no waiting on a long-
|
|
77
|
+
* running CDP call from the Node side. Polled `page.evaluate` was the
|
|
78
|
+
* root cause of the intermittent `Runtime.callFunctionOn` stalls
|
|
79
|
+
* tracked in polly#138.
|
|
80
|
+
*
|
|
73
81
|
* Call this at the end of every .browser.ts test file.
|
|
74
82
|
*/
|
|
75
83
|
export declare function done(): Promise<void>;
|
|
@@ -3,9 +3,10 @@
|
|
|
3
3
|
* applications.
|
|
4
4
|
*
|
|
5
5
|
* Provides a lightweight describe/test/expect harness that runs inside a
|
|
6
|
-
* Puppeteer-launched browser tab. Results are
|
|
7
|
-
*
|
|
8
|
-
* module in their *.browser.ts
|
|
6
|
+
* Puppeteer-launched browser tab. Results are pushed back to the companion
|
|
7
|
+
* runner (run.ts) via a function the runner exposes on `window.__pollyReport`
|
|
8
|
+
* before navigating. Applications import this module in their *.browser.ts
|
|
9
|
+
* test files and call done() at the end.
|
|
9
10
|
*
|
|
10
11
|
* @example
|
|
11
12
|
* ```typescript
|
|
@@ -243,8 +243,11 @@ async function done() {
|
|
|
243
243
|
}
|
|
244
244
|
}
|
|
245
245
|
}
|
|
246
|
-
window["
|
|
247
|
-
|
|
246
|
+
const report = window["__pollyReport"];
|
|
247
|
+
if (typeof report !== "function") {
|
|
248
|
+
throw new Error("harness.done(): window.__pollyReport is not defined. " + "This harness must be driven by the Polly browser test runner.");
|
|
249
|
+
}
|
|
250
|
+
report(results);
|
|
248
251
|
}
|
|
249
252
|
async function waitFor(predicate, timeoutMs = 5000, intervalMs = 25) {
|
|
250
253
|
const deadline = Date.now() + timeoutMs;
|
|
@@ -265,4 +268,4 @@ export {
|
|
|
265
268
|
cleanup
|
|
266
269
|
};
|
|
267
270
|
|
|
268
|
-
//# debugId=
|
|
271
|
+
//# debugId=F2832DFB58875E3964756E2164756E21
|
|
@@ -2,9 +2,9 @@
|
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../tools/test/src/browser/harness.ts"],
|
|
4
4
|
"sourcesContent": [
|
|
5
|
-
"/**\n * Browser-side test harness for Polly applications.\n *\n * Provides describe/test/expect/done that run inside a Puppeteer-launched\n * browser tab and record results on window.__testResults for the Node-side\n * runner to collect. Matchers cover both value assertions and DOM element\n * assertions so that Preact component tests and WebRTC adapter tests use\n * the same harness.\n *\n * @example\n * ```typescript\n * import { describe, test, expect, done, flush, cleanup } from \"@fairfox/polly/test/browser\";\n *\n * const app = document.getElementById(\"app\")!;\n *\n * describe(\"my feature\", () => {\n * test(\"renders correctly\", async () => {\n * render(<MyComponent />, app);\n * await flush();\n * expect(app.querySelector(\"h1\")).toHaveTextContent(\"Hello\");\n * cleanup(app);\n * });\n * });\n *\n * done();\n * ```\n */\n\ninterface TestResult {\n name: string;\n passed: boolean;\n error?: string;\n}\n\nconst results: TestResult[] = [];\nconst suites: Array<{\n name: string;\n tests: Array<{ name: string; fn: () => Promise<void> | void }>;\n}> = [];\n\nexport function describe(name: string, fn: () => void): void {\n suites.push({ name, tests: [] });\n fn();\n}\n\nexport function test(name: string, fn: () => Promise<void> | void): void {\n const suite = suites[suites.length - 1];\n if (suite) {\n suite.tests.push({ name, fn });\n }\n}\n\nfunction assertElement(value: unknown): Element {\n if (!(value instanceof Element)) {\n throw new Error(`Expected an Element, got ${typeof value}: ${String(value)}`);\n }\n return value;\n}\n\nexport function expect<T>(actual: T) {\n return {\n // Value matchers\n toBe(expected: T) {\n if (actual !== expected) {\n throw new Error(`Expected ${JSON.stringify(expected)}, got ${JSON.stringify(actual)}`);\n }\n },\n toEqual(expected: T) {\n if (JSON.stringify(actual) !== JSON.stringify(expected)) {\n throw new Error(`Expected ${JSON.stringify(expected)}, got ${JSON.stringify(actual)}`);\n }\n },\n toContain(sub: string) {\n if (!String(actual).includes(sub)) {\n throw new Error(`Expected \"${String(actual)}\" to contain \"${sub}\"`);\n }\n },\n toBeTruthy() {\n if (!actual) throw new Error(`Expected truthy, got ${String(actual)}`);\n },\n toBeFalsy() {\n if (actual) throw new Error(`Expected falsy, got ${String(actual)}`);\n },\n toBeNull() {\n if (actual !== null) throw new Error(`Expected null, got ${String(actual)}`);\n },\n toBeDefined() {\n if (actual === undefined || actual === null) {\n throw new Error(`Expected value to be defined, got ${String(actual)}`);\n }\n },\n toBeUndefined() {\n if (actual !== undefined) {\n throw new Error(`Expected undefined, got ${JSON.stringify(actual)}`);\n }\n },\n toBeGreaterThan(expected: number) {\n if (typeof actual !== \"number\" || actual <= expected) {\n throw new Error(`Expected ${String(actual)} to be greater than ${expected}`);\n }\n },\n toHaveLength(expected: number) {\n const obj = actual;\n const len = obj && typeof obj === \"object\" && \"length\" in obj ? Number(obj.length) : -1;\n if (len !== expected) throw new Error(`Expected length ${expected}, got ${len}`);\n },\n toExist() {\n if (actual == null) throw new Error(`Expected value to exist, got ${String(actual)}`);\n },\n\n // DOM element matchers\n toHaveTextContent(expected: string) {\n const el = assertElement(actual);\n if (!el.textContent?.includes(expected)) {\n throw new Error(\n `Expected text content to include ${JSON.stringify(expected)}, got ${JSON.stringify(el.textContent)}`\n );\n }\n },\n toBeChecked() {\n const el = assertElement(actual);\n if (!(el instanceof HTMLInputElement) || !el.checked) {\n throw new Error(\"Expected element to be checked\");\n }\n },\n toBeDisabled() {\n const el = assertElement(actual);\n if (!el.hasAttribute(\"disabled\") && el.getAttribute(\"aria-disabled\") !== \"true\") {\n throw new Error(\"Expected element to be disabled\");\n }\n },\n toHaveValue(expected: string) {\n const el = assertElement(actual);\n const inputEl =\n el instanceof HTMLInputElement || el instanceof HTMLTextAreaElement ? el : null;\n if (!inputEl || inputEl.value !== expected) {\n throw new Error(\n `Expected value ${JSON.stringify(expected)}, got ${JSON.stringify(inputEl?.value ?? \"(not an input)\")}`\n );\n }\n },\n toHaveAttribute(name: string, value?: string) {\n const el = assertElement(actual);\n if (!el.hasAttribute(name)) {\n throw new Error(`Expected element to have attribute \"${name}\"`);\n }\n if (value !== undefined && el.getAttribute(name) !== value) {\n throw new Error(\n `Expected attribute \"${name}\" to be ${JSON.stringify(value)}, got ${JSON.stringify(el.getAttribute(name))}`\n );\n }\n },\n\n // .not variants\n not: {\n toBe(expected: T) {\n if (actual === expected) {\n throw new Error(`Expected value NOT to be ${JSON.stringify(expected)}`);\n }\n },\n toEqual(expected: T) {\n if (JSON.stringify(actual) === JSON.stringify(expected)) {\n throw new Error(`Expected value NOT to equal ${JSON.stringify(expected)}`);\n }\n },\n toContain(sub: string) {\n if (String(actual).includes(sub)) {\n throw new Error(`Expected \"${String(actual)}\" NOT to contain \"${sub}\"`);\n }\n },\n toBeNull() {\n if (actual === null) throw new Error(\"Expected value NOT to be null\");\n },\n toExist() {\n if (actual != null) throw new Error(`Expected value NOT to exist, got ${String(actual)}`);\n },\n toBeChecked() {\n const el = assertElement(actual);\n if (el instanceof HTMLInputElement && el.checked) {\n throw new Error(\"Expected element NOT to be checked\");\n }\n },\n toBeDisabled() {\n const el = assertElement(actual);\n if (el.hasAttribute(\"disabled\") || el.getAttribute(\"aria-disabled\") === \"true\") {\n throw new Error(\"Expected element NOT to be disabled\");\n }\n },\n toHaveAttribute(name: string) {\n const el = assertElement(actual);\n if (el.hasAttribute(name)) {\n throw new Error(`Expected element NOT to have attribute \"${name}\"`);\n }\n },\n },\n };\n}\n\n/**\n * Flush microtasks and pending DOM updates. Call after signal assignments\n * or render calls to give the reactive system and the browser a chance to\n * settle before asserting on the result.\n */\nexport function flush(ms = 50): Promise<void> {\n return new Promise((r) => setTimeout(r, ms));\n}\n\n/**\n * Clear a container's rendered content. Call at the end of each test to\n * prevent state leaking between tests. If you use Preact's render(), pass\n * the same container; the function calls render(null, container) if Preact\n * is available, otherwise sets innerHTML to \"\".\n */\nexport function cleanup(container: Element): void {\n container.innerHTML = \"\";\n}\n\n/**\n * Run all registered tests and
|
|
5
|
+
"/**\n * Browser-side test harness for Polly applications.\n *\n * Provides describe/test/expect/done that run inside a Puppeteer-launched\n * browser tab and record results on window.__testResults for the Node-side\n * runner to collect. Matchers cover both value assertions and DOM element\n * assertions so that Preact component tests and WebRTC adapter tests use\n * the same harness.\n *\n * @example\n * ```typescript\n * import { describe, test, expect, done, flush, cleanup } from \"@fairfox/polly/test/browser\";\n *\n * const app = document.getElementById(\"app\")!;\n *\n * describe(\"my feature\", () => {\n * test(\"renders correctly\", async () => {\n * render(<MyComponent />, app);\n * await flush();\n * expect(app.querySelector(\"h1\")).toHaveTextContent(\"Hello\");\n * cleanup(app);\n * });\n * });\n *\n * done();\n * ```\n */\n\ninterface TestResult {\n name: string;\n passed: boolean;\n error?: string;\n}\n\nconst results: TestResult[] = [];\nconst suites: Array<{\n name: string;\n tests: Array<{ name: string; fn: () => Promise<void> | void }>;\n}> = [];\n\nexport function describe(name: string, fn: () => void): void {\n suites.push({ name, tests: [] });\n fn();\n}\n\nexport function test(name: string, fn: () => Promise<void> | void): void {\n const suite = suites[suites.length - 1];\n if (suite) {\n suite.tests.push({ name, fn });\n }\n}\n\nfunction assertElement(value: unknown): Element {\n if (!(value instanceof Element)) {\n throw new Error(`Expected an Element, got ${typeof value}: ${String(value)}`);\n }\n return value;\n}\n\nexport function expect<T>(actual: T) {\n return {\n // Value matchers\n toBe(expected: T) {\n if (actual !== expected) {\n throw new Error(`Expected ${JSON.stringify(expected)}, got ${JSON.stringify(actual)}`);\n }\n },\n toEqual(expected: T) {\n if (JSON.stringify(actual) !== JSON.stringify(expected)) {\n throw new Error(`Expected ${JSON.stringify(expected)}, got ${JSON.stringify(actual)}`);\n }\n },\n toContain(sub: string) {\n if (!String(actual).includes(sub)) {\n throw new Error(`Expected \"${String(actual)}\" to contain \"${sub}\"`);\n }\n },\n toBeTruthy() {\n if (!actual) throw new Error(`Expected truthy, got ${String(actual)}`);\n },\n toBeFalsy() {\n if (actual) throw new Error(`Expected falsy, got ${String(actual)}`);\n },\n toBeNull() {\n if (actual !== null) throw new Error(`Expected null, got ${String(actual)}`);\n },\n toBeDefined() {\n if (actual === undefined || actual === null) {\n throw new Error(`Expected value to be defined, got ${String(actual)}`);\n }\n },\n toBeUndefined() {\n if (actual !== undefined) {\n throw new Error(`Expected undefined, got ${JSON.stringify(actual)}`);\n }\n },\n toBeGreaterThan(expected: number) {\n if (typeof actual !== \"number\" || actual <= expected) {\n throw new Error(`Expected ${String(actual)} to be greater than ${expected}`);\n }\n },\n toHaveLength(expected: number) {\n const obj = actual;\n const len = obj && typeof obj === \"object\" && \"length\" in obj ? Number(obj.length) : -1;\n if (len !== expected) throw new Error(`Expected length ${expected}, got ${len}`);\n },\n toExist() {\n if (actual == null) throw new Error(`Expected value to exist, got ${String(actual)}`);\n },\n\n // DOM element matchers\n toHaveTextContent(expected: string) {\n const el = assertElement(actual);\n if (!el.textContent?.includes(expected)) {\n throw new Error(\n `Expected text content to include ${JSON.stringify(expected)}, got ${JSON.stringify(el.textContent)}`\n );\n }\n },\n toBeChecked() {\n const el = assertElement(actual);\n if (!(el instanceof HTMLInputElement) || !el.checked) {\n throw new Error(\"Expected element to be checked\");\n }\n },\n toBeDisabled() {\n const el = assertElement(actual);\n if (!el.hasAttribute(\"disabled\") && el.getAttribute(\"aria-disabled\") !== \"true\") {\n throw new Error(\"Expected element to be disabled\");\n }\n },\n toHaveValue(expected: string) {\n const el = assertElement(actual);\n const inputEl =\n el instanceof HTMLInputElement || el instanceof HTMLTextAreaElement ? el : null;\n if (!inputEl || inputEl.value !== expected) {\n throw new Error(\n `Expected value ${JSON.stringify(expected)}, got ${JSON.stringify(inputEl?.value ?? \"(not an input)\")}`\n );\n }\n },\n toHaveAttribute(name: string, value?: string) {\n const el = assertElement(actual);\n if (!el.hasAttribute(name)) {\n throw new Error(`Expected element to have attribute \"${name}\"`);\n }\n if (value !== undefined && el.getAttribute(name) !== value) {\n throw new Error(\n `Expected attribute \"${name}\" to be ${JSON.stringify(value)}, got ${JSON.stringify(el.getAttribute(name))}`\n );\n }\n },\n\n // .not variants\n not: {\n toBe(expected: T) {\n if (actual === expected) {\n throw new Error(`Expected value NOT to be ${JSON.stringify(expected)}`);\n }\n },\n toEqual(expected: T) {\n if (JSON.stringify(actual) === JSON.stringify(expected)) {\n throw new Error(`Expected value NOT to equal ${JSON.stringify(expected)}`);\n }\n },\n toContain(sub: string) {\n if (String(actual).includes(sub)) {\n throw new Error(`Expected \"${String(actual)}\" NOT to contain \"${sub}\"`);\n }\n },\n toBeNull() {\n if (actual === null) throw new Error(\"Expected value NOT to be null\");\n },\n toExist() {\n if (actual != null) throw new Error(`Expected value NOT to exist, got ${String(actual)}`);\n },\n toBeChecked() {\n const el = assertElement(actual);\n if (el instanceof HTMLInputElement && el.checked) {\n throw new Error(\"Expected element NOT to be checked\");\n }\n },\n toBeDisabled() {\n const el = assertElement(actual);\n if (el.hasAttribute(\"disabled\") || el.getAttribute(\"aria-disabled\") === \"true\") {\n throw new Error(\"Expected element NOT to be disabled\");\n }\n },\n toHaveAttribute(name: string) {\n const el = assertElement(actual);\n if (el.hasAttribute(name)) {\n throw new Error(`Expected element NOT to have attribute \"${name}\"`);\n }\n },\n },\n };\n}\n\n/**\n * Flush microtasks and pending DOM updates. Call after signal assignments\n * or render calls to give the reactive system and the browser a chance to\n * settle before asserting on the result.\n */\nexport function flush(ms = 50): Promise<void> {\n return new Promise((r) => setTimeout(r, ms));\n}\n\n/**\n * Clear a container's rendered content. Call at the end of each test to\n * prevent state leaking between tests. If you use Preact's render(), pass\n * the same container; the function calls render(null, container) if Preact\n * is available, otherwise sets innerHTML to \"\".\n */\nexport function cleanup(container: Element): void {\n container.innerHTML = \"\";\n}\n\n/**\n * Run all registered tests and report the tally to the Node-side runner.\n *\n * The runner injects `window.__pollyReport` (via Puppeteer's\n * `page.exposeFunction`) before navigating, so reporting is a single\n * push from the page to the runner — no polling, no waiting on a long-\n * running CDP call from the Node side. Polled `page.evaluate` was the\n * root cause of the intermittent `Runtime.callFunctionOn` stalls\n * tracked in polly#138.\n *\n * Call this at the end of every .browser.ts test file.\n */\nexport async function done(): Promise<void> {\n for (const suite of suites) {\n for (const t of suite.tests) {\n const fullName = `${suite.name} > ${t.name}`;\n try {\n await t.fn();\n results.push({ name: fullName, passed: true });\n } catch (err) {\n results.push({\n name: fullName,\n passed: false,\n error: err instanceof Error ? err.message : String(err),\n });\n }\n }\n }\n\n const report = (window as unknown as Record<string, unknown>)[\"__pollyReport\"];\n if (typeof report !== \"function\") {\n throw new Error(\n \"harness.done(): window.__pollyReport is not defined. \" +\n \"This harness must be driven by the Polly browser test runner.\"\n );\n }\n (report as (r: TestResult[]) => void)(results);\n}\n\n/**\n * Wait until a predicate returns true, polling every intervalMs. Rejects\n * after timeoutMs.\n */\nexport async function waitFor(\n predicate: () => boolean | Promise<boolean>,\n timeoutMs = 5000,\n intervalMs = 25\n): Promise<void> {\n const deadline = Date.now() + timeoutMs;\n while (Date.now() < deadline) {\n if (await predicate()) return;\n await new Promise((r) => setTimeout(r, intervalMs));\n }\n throw new Error(`waitFor timed out after ${timeoutMs}ms`);\n}\n"
|
|
6
6
|
],
|
|
7
|
-
"mappings": "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
|
|
8
|
-
"debugId": "
|
|
7
|
+
"mappings": "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",
|
|
8
|
+
"debugId": "F2832DFB58875E3964756E2164756E21",
|
|
9
9
|
"names": []
|
|
10
10
|
}
|
|
@@ -147,9 +147,6 @@ function signalingServer(options = {}) {
|
|
|
147
147
|
}
|
|
148
148
|
|
|
149
149
|
// tools/test/src/browser/runner-core.ts
|
|
150
|
-
function isProtocolError(err) {
|
|
151
|
-
return err instanceof Error && err.name === "ProtocolError";
|
|
152
|
-
}
|
|
153
150
|
function errMessage(err) {
|
|
154
151
|
return err instanceof Error ? err.message : String(err);
|
|
155
152
|
}
|
|
@@ -165,18 +162,8 @@ async function runSuite(testFiles, runFile, options = {}) {
|
|
|
165
162
|
try {
|
|
166
163
|
result = await runFile(testFile);
|
|
167
164
|
} catch (err) {
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
try {
|
|
171
|
-
result = await runFile(testFile);
|
|
172
|
-
} catch (retryErr) {
|
|
173
|
-
log(` ❌ retry failed: ${errMessage(retryErr)}`);
|
|
174
|
-
result = { passed: 0, failed: 1 };
|
|
175
|
-
}
|
|
176
|
-
} else {
|
|
177
|
-
log(` ❌ ${errMessage(err)}`);
|
|
178
|
-
result = { passed: 0, failed: 1 };
|
|
179
|
-
}
|
|
165
|
+
log(` ❌ ${errMessage(err)}`);
|
|
166
|
+
result = { passed: 0, failed: 1 };
|
|
180
167
|
}
|
|
181
168
|
totalPassed += result.passed;
|
|
182
169
|
totalFailed += result.failed;
|
|
@@ -216,8 +203,7 @@ var signalingApp = new Elysia2().use(signalingServer({ path: "/polly/signaling"
|
|
|
216
203
|
console.log(`[browser-runner] signaling server on ws://127.0.0.1:${signalingPort}/polly/signaling`);
|
|
217
204
|
var browser = await puppeteer.launch({
|
|
218
205
|
headless,
|
|
219
|
-
args: ["--no-sandbox", "--disable-setuid-sandbox"]
|
|
220
|
-
protocolTimeout: 30000
|
|
206
|
+
args: ["--no-sandbox", "--disable-setuid-sandbox"]
|
|
221
207
|
});
|
|
222
208
|
async function runFile(testFile) {
|
|
223
209
|
const buildResult = await Bun.build({
|
|
@@ -256,34 +242,27 @@ async function runFile(testFile) {
|
|
|
256
242
|
});
|
|
257
243
|
let page;
|
|
258
244
|
try {
|
|
259
|
-
|
|
260
|
-
page
|
|
245
|
+
const newPage = await browser.newPage();
|
|
246
|
+
page = newPage;
|
|
247
|
+
newPage.on("console", (msg) => {
|
|
261
248
|
const text = msg.text();
|
|
262
249
|
if (text.includes("[test]")) {
|
|
263
250
|
console.log(` ${text}`);
|
|
264
251
|
}
|
|
265
252
|
});
|
|
266
|
-
|
|
267
|
-
|
|
253
|
+
const outcome = new Promise((resolve2, reject) => {
|
|
254
|
+
newPage.exposeFunction("__pollyReport", (results2) => {
|
|
255
|
+
resolve2(results2);
|
|
256
|
+
}).catch(reject);
|
|
257
|
+
newPage.on("pageerror", (err) => {
|
|
258
|
+
reject(err instanceof Error ? err : new Error(errMessage(err)));
|
|
259
|
+
});
|
|
268
260
|
});
|
|
269
|
-
await
|
|
270
|
-
const
|
|
271
|
-
const deadline = Date.now() + timeout;
|
|
272
|
-
let finished = false;
|
|
273
|
-
while (Date.now() < deadline) {
|
|
274
|
-
finished = await page.evaluate(() => window["__done"] === true);
|
|
275
|
-
if (finished)
|
|
276
|
-
break;
|
|
277
|
-
await new Promise((r) => setTimeout(r, 100));
|
|
278
|
-
}
|
|
279
|
-
if (!finished) {
|
|
280
|
-
console.log(` ❌ timed out after ${timeout}ms`);
|
|
281
|
-
return { passed: 0, failed: 1 };
|
|
282
|
-
}
|
|
283
|
-
const results = await page.evaluate(() => window["__testResults"]);
|
|
261
|
+
await newPage.goto(`http://127.0.0.1:${server.port}/`, { waitUntil: "domcontentloaded" });
|
|
262
|
+
const results = await outcome;
|
|
284
263
|
let passed = 0;
|
|
285
264
|
let failed = 0;
|
|
286
|
-
for (const r of results
|
|
265
|
+
for (const r of results) {
|
|
287
266
|
if (r.passed) {
|
|
288
267
|
console.log(` ✅ ${r.name}`);
|
|
289
268
|
passed += 1;
|
|
@@ -309,4 +288,4 @@ console.log(`
|
|
|
309
288
|
[browser-runner] ${totalPassed} passed, ${totalFailed} failed`);
|
|
310
289
|
process.exit(totalFailed > 0 ? 1 : 0);
|
|
311
290
|
|
|
312
|
-
//# debugId=
|
|
291
|
+
//# debugId=5455F136F17AAB2C64756E2164756E21
|
|
@@ -2,11 +2,11 @@
|
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../tools/test/src/browser/run.ts", "../src/elysia/signaling-server-plugin.ts", "../tools/test/src/browser/runner-core.ts"],
|
|
4
4
|
"sourcesContent": [
|
|
5
|
-
"#!/usr/bin/env bun\n\n/**\n * Browser test runner for Polly applications.\n *\n * Finds all *.browser.ts files in a given directory, bundles each with\n * Bun.build for the browser target (with an internal Automerge WASM fix),\n * serves the bundle on an ephemeral port, opens a Puppeteer page, and\n * polls window.__done for results. Prints pass/fail per test and exits\n * non-zero if any test failed.\n *\n * A signalling server for WebRTC tests starts automatically on a random\n * port. The URL is injected into the bundle via process.env.SIGNALING_URL.\n *\n * Usage (from project root):\n *\n * bun tools/test/src/browser/run.ts [testDir] [filter]\n *\n * Examples:\n *\n * bun tools/test/src/browser/run.ts tests/browser\n * bun tools/test/src/browser/run.ts tests/browser mesh-webrtc\n * HEADLESS=false bun tools/test/src/browser/run.ts tests/browser\n *\n * When invoked without a testDir, defaults to tests/browser relative to cwd.\n */\n\nimport { resolve } from \"node:path\";\nimport { type BunPlugin, Glob } from \"bun\";\nimport { Elysia } from \"elysia\";\nimport puppeteer, { type Page } from \"puppeteer\";\nimport { signalingServer } from \"../../../../src/elysia/signaling-server-plugin\";\nimport { errMessage, type FileTally, runSuite } from \"./runner-core\";\n\n// Automerge WASM fix\n// Bun.build's target: \"browser\" picks Automerge's fullfat_bundler.js which\n// does a static .wasm import that Bun can't wire up. Redirect to the\n// base64 variant which embeds the WASM as a string and self-initialises.\n\nconst automergeBase64Path = resolve(\n process.cwd(),\n \"node_modules/@automerge/automerge/dist/mjs/entrypoints/fullfat_base64.js\"\n);\n\nconst automergeBase64Plugin: BunPlugin = {\n name: \"automerge-base64\",\n setup(build) {\n build.onResolve({ filter: /^@automerge\\/automerge(\\/slim)?$/ }, () => {\n return { path: automergeBase64Path };\n });\n },\n};\n\n// Argument parsing\n\nconst testDir = resolve(process.cwd(), process.argv[2] ?? \"tests/browser\");\nconst filter = process.argv[3] ?? \"\";\nconst headless = process.env[\"HEADLESS\"] !== \"false\";\n\nconst glob = new Glob(\"**/*.browser.{ts,tsx}\");\nconst testFiles: string[] = [];\nfor await (const file of glob.scan({ cwd: testDir, absolute: true })) {\n if (file.includes(\"harness\")) continue;\n if (filter && !file.includes(filter)) continue;\n testFiles.push(file);\n}\n\nif (testFiles.length === 0) {\n console.log(`[browser-runner] no test files found${filter ? ` matching \"${filter}\"` : \"\"}`);\n process.exit(0);\n}\n\nconsole.log(`[browser-runner] found ${testFiles.length} test file(s)`);\n\n// Start server-side infrastructure\n\nconst signalingPort = 39000 + Math.floor(Math.random() * 1000);\nconst signalingApp = new Elysia()\n .use(signalingServer({ path: \"/polly/signaling\" }))\n .listen(signalingPort);\nconsole.log(`[browser-runner] signaling server on ws://127.0.0.1:${signalingPort}/polly/signaling`);\n\n// Launch browser\n//\n// protocolTimeout
|
|
5
|
+
"#!/usr/bin/env bun\n\n/**\n * Browser test runner for Polly applications.\n *\n * Finds all *.browser.ts files in a given directory, bundles each with\n * Bun.build for the browser target (with an internal Automerge WASM fix),\n * serves the bundle on an ephemeral port, opens a Puppeteer page, and\n * polls window.__done for results. Prints pass/fail per test and exits\n * non-zero if any test failed.\n *\n * A signalling server for WebRTC tests starts automatically on a random\n * port. The URL is injected into the bundle via process.env.SIGNALING_URL.\n *\n * Usage (from project root):\n *\n * bun tools/test/src/browser/run.ts [testDir] [filter]\n *\n * Examples:\n *\n * bun tools/test/src/browser/run.ts tests/browser\n * bun tools/test/src/browser/run.ts tests/browser mesh-webrtc\n * HEADLESS=false bun tools/test/src/browser/run.ts tests/browser\n *\n * When invoked without a testDir, defaults to tests/browser relative to cwd.\n */\n\nimport { resolve } from \"node:path\";\nimport { type BunPlugin, Glob } from \"bun\";\nimport { Elysia } from \"elysia\";\nimport puppeteer, { type Page } from \"puppeteer\";\nimport { signalingServer } from \"../../../../src/elysia/signaling-server-plugin\";\nimport { errMessage, type FileTally, runSuite } from \"./runner-core\";\n\n// Automerge WASM fix\n// Bun.build's target: \"browser\" picks Automerge's fullfat_bundler.js which\n// does a static .wasm import that Bun can't wire up. Redirect to the\n// base64 variant which embeds the WASM as a string and self-initialises.\n\nconst automergeBase64Path = resolve(\n process.cwd(),\n \"node_modules/@automerge/automerge/dist/mjs/entrypoints/fullfat_base64.js\"\n);\n\nconst automergeBase64Plugin: BunPlugin = {\n name: \"automerge-base64\",\n setup(build) {\n build.onResolve({ filter: /^@automerge\\/automerge(\\/slim)?$/ }, () => {\n return { path: automergeBase64Path };\n });\n },\n};\n\n// Argument parsing\n\nconst testDir = resolve(process.cwd(), process.argv[2] ?? \"tests/browser\");\nconst filter = process.argv[3] ?? \"\";\nconst headless = process.env[\"HEADLESS\"] !== \"false\";\n\nconst glob = new Glob(\"**/*.browser.{ts,tsx}\");\nconst testFiles: string[] = [];\nfor await (const file of glob.scan({ cwd: testDir, absolute: true })) {\n if (file.includes(\"harness\")) continue;\n if (filter && !file.includes(filter)) continue;\n testFiles.push(file);\n}\n\nif (testFiles.length === 0) {\n console.log(`[browser-runner] no test files found${filter ? ` matching \"${filter}\"` : \"\"}`);\n process.exit(0);\n}\n\nconsole.log(`[browser-runner] found ${testFiles.length} test file(s)`);\n\n// Start server-side infrastructure\n\nconst signalingPort = 39000 + Math.floor(Math.random() * 1000);\nconst signalingApp = new Elysia()\n .use(signalingServer({ path: \"/polly/signaling\" }))\n .listen(signalingPort);\nconsole.log(`[browser-runner] signaling server on ws://127.0.0.1:${signalingPort}/polly/signaling`);\n\n// Launch browser\n//\n// No `protocolTimeout` override: results arrive via `page.exposeFunction`\n// (a push from the page over CDP `Runtime.bindingCalled` events), not via\n// polled `page.evaluate` calls. Without a long-running `Runtime.callFunctionOn`\n// on the hot path there is no protocol round-trip for a busy renderer to\n// stall, so the timeout the previous polling design had to guard against\n// is no longer reachable (polly#138).\n\nconst browser = await puppeteer.launch({\n headless,\n args: [\"--no-sandbox\", \"--disable-setuid-sandbox\"],\n});\n\n/**\n * Build, serve, and run one test file on a fresh page. Returns its\n * pass/fail tally. Build failures are reported here (not thrown);\n * a page-level uncaught error propagates so the suite records the\n * file as failed. The page and server are always cleaned up first.\n */\ninterface TestResult {\n name: string;\n passed: boolean;\n error?: string;\n}\nasync function runFile(testFile: string): Promise<FileTally> {\n const buildResult = await Bun.build({\n entrypoints: [testFile],\n target: \"browser\",\n format: \"esm\",\n minify: false,\n sourcemap: \"inline\",\n plugins: [automergeBase64Plugin],\n define: {\n \"process.env.SIGNALING_URL\": JSON.stringify(\n `ws://127.0.0.1:${signalingPort}/polly/signaling`\n ),\n },\n });\n\n if (!buildResult.success) {\n console.log(\" ❌ build failed:\");\n for (const log of buildResult.logs) {\n console.log(` ${log}`);\n }\n return { passed: 0, failed: 1 };\n }\n\n const jsText = await buildResult.outputs[0]?.text();\n if (!jsText) {\n console.log(\" ❌ build produced no output\");\n return { passed: 0, failed: 1 };\n }\n\n const html = `<!DOCTYPE html>\n<html><head><meta charset=\"utf-8\"></head>\n<body>\n<script type=\"module\">${jsText}</script>\n</body></html>`;\n\n const server = Bun.serve({\n port: 0,\n fetch() {\n return new Response(html, { headers: { \"Content-Type\": \"text/html\" } });\n },\n });\n\n let page: Page | undefined;\n try {\n const newPage = await browser.newPage();\n page = newPage;\n newPage.on(\"console\", (msg) => {\n const text = msg.text();\n if (text.includes(\"[test]\")) {\n console.log(` ${text}`);\n }\n });\n\n // Push-based reporting (polly#138): the page calls back into Node via\n // `__pollyReport(results)` when its in-page suite has finished. We\n // bind that function and the page-error channel to a single Promise\n // before navigating, then `await` it — no `page.evaluate` polling\n // and no CDP timeout for a busy renderer to trip.\n const outcome = new Promise<TestResult[]>((resolve, reject) => {\n newPage\n .exposeFunction(\"__pollyReport\", (results: TestResult[]) => {\n resolve(results);\n })\n .catch(reject);\n newPage.on(\"pageerror\", (err: unknown) => {\n reject(err instanceof Error ? err : new Error(errMessage(err)));\n });\n });\n\n await newPage.goto(`http://127.0.0.1:${server.port}/`, { waitUntil: \"domcontentloaded\" });\n\n const results = await outcome;\n\n let passed = 0;\n let failed = 0;\n for (const r of results) {\n if (r.passed) {\n console.log(` ✅ ${r.name}`);\n passed += 1;\n } else {\n console.log(` ❌ ${r.name}: ${r.error}`);\n failed += 1;\n }\n }\n return { passed, failed };\n } finally {\n if (page) {\n await page.close().catch(() => {\n // ignore — page may already be gone after an uncaught error\n });\n }\n server.stop();\n }\n}\n\nconst { passed: totalPassed, failed: totalFailed } = await runSuite(testFiles, runFile, {\n label: (testFile) => testFile.replace(`${testDir}/`, \"\"),\n});\n\nawait browser.close();\n(signalingApp as unknown as { server?: { stop?: (f?: boolean) => void } }).server?.stop?.(true);\n\nconsole.log(`\\n[browser-runner] ${totalPassed} passed, ${totalFailed} failed`);\nprocess.exit(totalFailed > 0 ? 1 : 0);\n",
|
|
6
6
|
"// @ts-nocheck - Optional peer dependencies (elysia, @elysiajs/eden)\n/**\n * signalingServer — Phase 2 Elysia plugin that exposes a stateless\n * WebSocket route for SDP/ICE relay between $meshState peers.\n *\n * The mesh transport is a star-of-data-channels: peers establish direct\n * WebRTC connections to each other and exchange document operations\n * peer-to-peer once those channels are open. WebRTC connection setup\n * needs an out-of-band channel for SDP offer/answer and ICE candidate\n * exchange, and that channel is what this plugin provides. The plugin\n * does not own any document state, does not hold any encryption keys,\n * and never inspects the contents of the messages it relays. It is a\n * pure pub-sub by peer id.\n *\n * Once two peers have completed the SDP exchange and opened a direct\n * data channel, the signalling server is no longer on the critical\n * path — the peers talk directly. The signalling server's role is\n * therefore intermittent: peers connect to it only during the brief\n * windows when they are establishing or re-establishing connections.\n *\n * Wire protocol:\n *\n * Client → server (join):\n * { type: \"join\", peerId: \"peer-alice\" }\n *\n * Client → server (signal to another peer):\n * { type: \"signal\", peerId: \"peer-alice\", targetPeerId: \"peer-bob\",\n * payload: { ... } }\n *\n * Server → client (delivered signal):\n * { type: \"signal\", peerId: \"peer-alice\", targetPeerId: \"peer-bob\",\n * payload: { ... } }\n *\n * Server → client (notification of unknown target):\n * { type: \"error\", reason: \"unknown-target\", targetPeerId: \"...\" }\n *\n * The `payload` is opaque to the signalling server — typically it\n * carries an SDP offer, SDP answer, or ICE candidate. Applications can\n * also use the relay for any other peer-to-peer message that needs an\n * intermediary, such as the initial handshake of a pairing flow.\n *\n * @example\n * ```ts\n * import { Elysia } from \"elysia\";\n * import { signalingServer } from \"@fairfox/polly/elysia\";\n *\n * const app = new Elysia()\n * .use(signalingServer({ path: \"/polly/signaling\" }))\n * .listen(8080);\n * ```\n */\n\nimport { Elysia } from \"elysia\";\n\n/** A signalling message. The `type` discriminates between client-to-server\n * requests (join, signal), the error envelope, and the server-to-client\n * discovery frames (peers-present, peer-joined, peer-left) that let\n * peers learn about each other without polling. */\nexport type SignalingMessage =\n | {\n type: \"join\";\n /** The peer registering itself with the signalling server. */\n peerId: string;\n }\n | {\n type: \"signal\";\n /** The peer sending the signal. */\n peerId: string;\n /** The peer the signal is being relayed to. */\n targetPeerId: string;\n /** Opaque payload, typically SDP or ICE. */\n payload: unknown;\n }\n | {\n type: \"error\";\n reason: \"unknown-target\" | \"not-joined\" | \"malformed\";\n targetPeerId?: string;\n }\n | {\n /** Sent to a newcomer immediately after it joins, listing every\n * peer that was already joined at that moment. Empty for a lone\n * newcomer. */\n type: \"peers-present\";\n peerIds: string[];\n }\n | {\n /** Broadcast to every incumbent when a new peer joins. */\n type: \"peer-joined\";\n peerId: string;\n }\n | {\n /** Broadcast to every remaining incumbent when a joined peer\n * closes its socket. Never emitted for a connection that never\n * sent a join frame. */\n type: \"peer-left\";\n peerId: string;\n };\n\n/** A frame whose `type` is outside the built-in signalling vocabulary.\n * Consumers who pass an {@link SignalingServerOptions.onCustomFrame}\n * handler receive these on the server side; everything else — including\n * routing them to a specific peer, storing a session, or rejecting the\n * frame — is the consumer's call. Polly does not touch the body. */\nexport interface CustomSignalingFrame {\n type: string;\n [key: string]: unknown;\n}\n\n/** Minimal surface the custom-frame handler receives in place of the\n * Elysia-specific `ws` object so the plugin stays portable. Exposes the\n * `data` bag (used to stash the authenticated peerId under the existing\n * join handshake) and a `send` method. */\nexport interface CustomFrameSocket {\n data: Record<string, unknown>;\n send: (msg: unknown) => void;\n}\n\nexport interface SignalingServerOptions {\n /** WebSocket route path. Defaults to \"/polly/signaling\". */\n path?: string;\n /** Optional hook for frames whose `type` is outside the built-in\n * vocabulary. The plugin invokes it in place of returning a\n * `malformed` error, so consumers can layer their own application\n * protocol (pairing return tokens, presence pings, etc.) on the\n * existing socket. The `peerId` argument is the sender's\n * authenticated peer id from their prior `join` frame, or\n * `undefined` if they haven't joined yet. */\n onCustomFrame?: (\n socket: CustomFrameSocket,\n frame: CustomSignalingFrame,\n peerId: string | undefined\n ) => void;\n}\n\n/**\n * Construct the signalling-server Elysia plugin. The plugin keeps a\n * per-instance map of peer id → WebSocket connection so that incoming\n * \"signal\" messages can be routed to the right target socket. The map\n * is local to the plugin instance, not shared across processes; for\n * multi-instance deployments behind a load balancer, applications need\n * sticky connection routing or a shared backplane (Redis pub-sub or\n * similar). That is a follow-up.\n */\nexport function signalingServer(options: SignalingServerOptions = {}) {\n const path = options.path ?? \"/polly/signaling\";\n const onCustomFrame = options.onCustomFrame;\n // Per-peer-id map of joined sockets. The inverse mapping is stored\n // directly on ws.data (a mutable property bag that Elysia preserves\n // across message callbacks for a given connection); the webrtc-p2p-chat\n // example in examples/ confirms this pattern is stable under Bun.\n const peerSockets = new Map<string, { send: (msg: unknown) => void }>();\n\n // Intentionally unnamed — Elysia deduplicates plugins by name, and each\n // signalingServer() call needs its own closure-captured peer map.\n const parseMessage = (raw: unknown): SignalingMessage | undefined => {\n try {\n return typeof raw === \"string\" ? JSON.parse(raw) : (raw as unknown as SignalingMessage);\n } catch {\n return undefined;\n }\n };\n\n const handleJoin = (ws: unknown, peerId: string): void => {\n const newcomer = ws as unknown as { send: (m: unknown) => void };\n // Collect the peers that were already joined so we can (a) tell the\n // newcomer who is present and (b) tell each of them about the\n // newcomer. A rejoin with the same peerId replaces the prior entry\n // but is otherwise treated as a fresh arrival.\n const incumbents: Array<{ peerId: string; socket: { send: (m: unknown) => void } }> = [];\n for (const [existingPeerId, existingSocket] of peerSockets) {\n if (existingPeerId === peerId) continue;\n incumbents.push({ peerId: existingPeerId, socket: existingSocket });\n }\n peerSockets.set(peerId, newcomer);\n const wsWithData = ws as unknown as { data: Record<string, unknown> };\n wsWithData.data.peerId = peerId;\n\n newcomer.send({\n type: \"peers-present\",\n peerIds: incumbents.map((i) => i.peerId),\n } as unknown as SignalingMessage);\n\n for (const incumbent of incumbents) {\n try {\n incumbent.socket.send({ type: \"peer-joined\", peerId } as unknown as SignalingMessage);\n } catch {\n // If a send fails we leave the stale socket to its own close\n // handler to evict. Dropping here would open a window where\n // the next signal to this peer still thinks it's alive.\n }\n }\n };\n\n const sendUnknownTarget = (ws: unknown, targetPeerId: string): void => {\n (ws as unknown as { send: (m: unknown) => void }).send({\n type: \"error\",\n reason: \"unknown-target\",\n targetPeerId,\n } as unknown as SignalingMessage);\n };\n\n /** Look up a target socket and confirm it is still open. */\n const findOpenTarget = (targetPeerId: string): { send: (msg: unknown) => void } | undefined => {\n const target = peerSockets.get(targetPeerId);\n if (!target) return undefined;\n const readyState = (target as unknown as { readyState?: number }).readyState;\n const OPEN = 1;\n if (readyState !== undefined && readyState !== OPEN) {\n peerSockets.delete(targetPeerId);\n return undefined;\n }\n return target;\n };\n\n const handleSignal = (ws: unknown, msg: Extract<SignalingMessage, { type: \"signal\" }>): void => {\n const wsWithData = ws as unknown as {\n data: Record<string, unknown>;\n send: (m: unknown) => void;\n };\n const senderId = wsWithData.data.peerId as unknown as string | undefined;\n if (!senderId) {\n wsWithData.send({ type: \"error\", reason: \"not-joined\" } as unknown as SignalingMessage);\n return;\n }\n const target = findOpenTarget(msg.targetPeerId);\n if (!target) {\n sendUnknownTarget(ws, msg.targetPeerId);\n return;\n }\n const relayed: SignalingMessage = {\n type: \"signal\",\n peerId: senderId,\n targetPeerId: msg.targetPeerId,\n payload: msg.payload,\n };\n try {\n target.send(relayed);\n } catch {\n peerSockets.delete(msg.targetPeerId);\n sendUnknownTarget(ws, msg.targetPeerId);\n }\n };\n\n return new Elysia().ws(path, {\n message(ws, raw) {\n const msg = parseMessage(raw);\n if (!msg) {\n ws.send({ type: \"error\", reason: \"malformed\" } as unknown as SignalingMessage);\n return;\n }\n if (msg.type === \"join\") {\n handleJoin(ws, msg.peerId);\n return;\n }\n if (msg.type === \"signal\") {\n handleSignal(ws, msg);\n return;\n }\n // Unknown types route to the consumer's custom-frame hook when\n // one is configured. Without a hook they still fall through to\n // the `malformed` error — same behaviour as before this branch\n // existed.\n if (onCustomFrame !== undefined) {\n const wsWithData = ws as unknown as CustomFrameSocket;\n const senderId = wsWithData.data[\"peerId\"];\n const peerId = typeof senderId === \"string\" ? senderId : undefined;\n onCustomFrame(wsWithData, msg as unknown as CustomSignalingFrame, peerId);\n return;\n }\n ws.send({ type: \"error\", reason: \"malformed\" } as unknown as SignalingMessage);\n },\n\n close(ws) {\n const peerId = (ws.data as unknown as Record<string, unknown>).peerId as unknown as\n | string\n | undefined;\n if (!peerId) {\n // Connection that never sent a join — nothing to broadcast and\n // nothing to evict. A bystander coming and going leaves no trace.\n return;\n }\n // Only evict if the map still points at *this* socket. A stale\n // close after the same peerId rejoined on a new socket should not\n // take the fresh entry with it. The comparison uses the `data` bag\n // Elysia attaches to each connection because it is preserved across\n // message and close callbacks, unlike the `ws` wrapper object which\n // Elysia may or may not reuse.\n const mapped = peerSockets.get(peerId);\n const wsData = (ws as unknown as { data: Record<string, unknown> }).data;\n const mappedData = (mapped as unknown as { data?: Record<string, unknown> } | undefined)\n ?.data;\n if (mapped === undefined || mappedData !== wsData) {\n return;\n }\n peerSockets.delete(peerId);\n for (const [_incumbentId, incumbentSocket] of peerSockets) {\n try {\n incumbentSocket.send({ type: \"peer-left\", peerId } as unknown as SignalingMessage);\n } catch {\n // Incumbent socket is gone; its own close handler will tidy.\n }\n }\n },\n });\n}\n",
|
|
7
|
-
"/**\n * Suite orchestration for the browser test runner.\n *\n * Extracted from run.ts so the resilience guarantee — a
|
|
7
|
+
"/**\n * Suite orchestration for the browser test runner.\n *\n * Extracted from run.ts so the resilience guarantee — a failure in one\n * file never aborts the whole suite — is a pure function that can be\n * unit-tested without launching a browser. run.ts supplies the real\n * per-file `runFile`; the test suite supplies a fake one.\n */\n\n/** Pass/fail tally for one test file or for a whole suite. */\nexport interface FileTally {\n passed: number;\n failed: number;\n}\n\nexport function errMessage(err: unknown): string {\n return err instanceof Error ? err.message : String(err);\n}\n\n/**\n * Run every test file, isolating each file's failure.\n *\n * - Any thrown error from `runFile` fails only that file and is logged.\n * - A file that returns a tally with `failed > 0` (a genuine red test)\n * is reported as-is.\n *\n * The loop never rejects: a failure in one file can never abort the\n * suite, so the remaining files always execute and report.\n */\nexport async function runSuite(\n testFiles: string[],\n runFile: (testFile: string) => Promise<FileTally>,\n options: {\n label?: (testFile: string) => string;\n log?: (msg: string) => void;\n } = {}\n): Promise<FileTally> {\n const label = options.label ?? ((f) => f);\n const log = options.log ?? console.log;\n\n let totalPassed = 0;\n let totalFailed = 0;\n\n for (const testFile of testFiles) {\n log(`\\n[browser-runner] running ${label(testFile)}`);\n\n let result: FileTally;\n try {\n result = await runFile(testFile);\n } catch (err) {\n log(` ❌ ${errMessage(err)}`);\n result = { passed: 0, failed: 1 };\n }\n\n totalPassed += result.passed;\n totalFailed += result.failed;\n }\n\n return { passed: totalPassed, failed: totalFailed };\n}\n"
|
|
8
8
|
],
|
|
9
|
-
"mappings": "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
|
|
10
|
-
"debugId": "
|
|
9
|
+
"mappings": "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",
|
|
10
|
+
"debugId": "5455F136F17AAB2C64756E2164756E21",
|
|
11
11
|
"names": []
|
|
12
12
|
}
|
|
@@ -1,30 +1,26 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Suite orchestration for the browser test runner.
|
|
3
3
|
*
|
|
4
|
-
* Extracted from run.ts so the resilience guarantee — a
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
4
|
+
* Extracted from run.ts so the resilience guarantee — a failure in one
|
|
5
|
+
* file never aborts the whole suite — is a pure function that can be
|
|
6
|
+
* unit-tested without launching a browser. run.ts supplies the real
|
|
7
|
+
* per-file `runFile`; the test suite supplies a fake one.
|
|
8
8
|
*/
|
|
9
9
|
/** Pass/fail tally for one test file or for a whole suite. */
|
|
10
10
|
export interface FileTally {
|
|
11
11
|
passed: number;
|
|
12
12
|
failed: number;
|
|
13
13
|
}
|
|
14
|
-
/** A transient CDP timeout (renderer stall) — retryable, unlike a red test. */
|
|
15
|
-
export declare function isProtocolError(err: unknown): boolean;
|
|
16
14
|
export declare function errMessage(err: unknown): string;
|
|
17
15
|
/**
|
|
18
16
|
* Run every test file, isolating each file's failure.
|
|
19
17
|
*
|
|
20
|
-
* -
|
|
21
|
-
*
|
|
22
|
-
* -
|
|
23
|
-
* - A file that returns a tally with `failed > 0` (a genuine red test) is
|
|
24
|
-
* reported as-is and never retried.
|
|
18
|
+
* - Any thrown error from `runFile` fails only that file and is logged.
|
|
19
|
+
* - A file that returns a tally with `failed > 0` (a genuine red test)
|
|
20
|
+
* is reported as-is.
|
|
25
21
|
*
|
|
26
|
-
* The loop never rejects: a
|
|
27
|
-
* so remaining files always execute and report.
|
|
22
|
+
* The loop never rejects: a failure in one file can never abort the
|
|
23
|
+
* suite, so the remaining files always execute and report.
|
|
28
24
|
*/
|
|
29
25
|
export declare function runSuite(testFiles: string[], runFile: (testFile: string) => Promise<FileTally>, options?: {
|
|
30
26
|
label?: (testFile: string) => string;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@fairfox/polly",
|
|
3
|
-
"version": "0.75.
|
|
3
|
+
"version": "0.75.3",
|
|
4
4
|
"private": false,
|
|
5
5
|
"type": "module",
|
|
6
6
|
"description": "Multi-execution-context framework with reactive state and cross-context messaging for Chrome extensions, PWAs, and worker-based applications",
|