@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.
@@ -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=EEBB85AB8D494BC664756E2164756E21
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": "EEBB85AB8D494BC664756E2164756E21",
12
+ "mappings": ";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAQA;AAYO,IAAM,aAAa,OAAqB,CAAC,CAAC;AAEjD,IAAI,SAAS;AACb,SAAS,OAAO,GAAW;AAAA,EACzB,UAAU;AAAA,EACV,OAAO,aAAa;AAAA;AAGf,SAAS,QAAQ,CACtB,SACA,OAAsD,CAAC,GAC/C;AAAA,EACR,MAAM,QAAoB;AAAA,IACxB,IAAI,QAAQ;AAAA,IACZ;AAAA,IACA,UAAU,KAAK,YAAY;AAAA,IAC3B,QAAQ,KAAK;AAAA,IACb,WAAW,KAAK,IAAI;AAAA,EACtB;AAAA,EACA,WAAW,QAAQ,CAAC,GAAG,WAAW,OAAO,KAAK;AAAA,EAC9C,OAAO,MAAM;AAAA;AAGR,SAAS,UAAU,CAAC,IAAmB;AAAA,EAC5C,IAAI,OAAO,WAAW;AAAA,IACpB,WAAW,QAAQ,CAAC;AAAA,IACpB;AAAA,EACF;AAAA,EACA,WAAW,QAAQ,WAAW,MAAM,OAAO,CAAC,MAAM,EAAE,OAAO,EAAE;AAAA;AAOxD,SAAS,WAAW,CAAC,QAAgB,KAAsB;AAAA,EAChE,MAAM,UAAU,eAAe,QAAQ,IAAI,UAAU,OAAO,GAAG;AAAA,EAC/D,OAAO,SAAS,SAAS,EAAE,QAAQ,UAAU,QAAQ,CAAC;AAAA;;AChDxD,mBAAS;AAOT,IAAM,QAAQ,QAAuB,CAAC,CAAC;AAGhC,SAAS,YAAY,GAA4B;AAAA,EACtD,OAAO,MAAM;AAAA;AAIR,SAAS,cAAc,GAAY;AAAA,EACxC,OAAO,MAAM,MAAM,SAAS;AAAA;AAIvB,SAAS,UAAU,GAA6B;AAAA,EACrD,MAAM,IAAI,MAAM;AAAA,EAChB,OAAO,EAAE,EAAE,SAAS;AAAA;AAIf,SAAS,WAAW,CAAC,OAA2B;AAAA,EACrD,MAAM,QAAQ,CAAC,GAAG,MAAM,OAAO,KAAK;AAAA;AAI/B,SAAS,UAAU,CAAC,IAAuC;AAAA,EAChE,MAAM,IAAI,MAAM;AAAA,EAChB,IAAI,EAAE,WAAW;AAAA,IAAG;AAAA,EACpB,IAAI,OAAO,WAAW;AAAA,IACpB,MAAM,MAAM,EAAE,EAAE,SAAS;AAAA,IACzB,MAAM,QAAQ,EAAE,MAAM,GAAG,EAAE;AAAA,IAC3B,OAAO;AAAA,EACT;AAAA,EACA,MAAM,MAAM,EAAE,UAAU,CAAC,MAAM,EAAE,OAAO,EAAE;AAAA,EAC1C,IAAI,QAAQ;AAAA,IAAI;AAAA,EAChB,MAAM,QAAQ,EAAE;AAAA,EAChB,MAAM,QAAQ,CAAC,GAAG,EAAE,MAAM,GAAG,GAAG,GAAG,GAAG,EAAE,MAAM,MAAM,CAAC,CAAC;AAAA,EACtD,OAAO;AAAA;AAIF,SAAS,eAAe,GAA6B;AAAA,EAC1D,MAAM,MAAM,WAAW;AAAA,EACvB,IAAI,CAAC;AAAA,IAAK;AAAA,EACV,IAAI,UAAU;AAAA,EACd,OAAO,WAAW,IAAI,EAAE;AAAA;AAInB,SAAS,iBAAiB,GAAS;AAAA,EACxC,MAAM,QAAQ,CAAC;AAAA;;;AC/CV,IAAM,mBAAmB,IAAI,IAAI,CAAC,UAAU,KAAK,SAAS,UAAU,UAAU,CAAC;AAG/E,IAAM,qBAAqB,IAAI,IAAI,CAAC,SAAS,UAAU,UAAU,OAAO,CAAC;AAezE,SAAS,eAAe,CAAC,SAA8C;AAAA,EAC5E,MAAM,OAA+B,CAAC;AAAA,EACtC,WAAW,QAAQ,MAAM,KAAK,QAAQ,UAAU,GAAG;AAAA,IACjD,IAAI,KAAK,KAAK,WAAW,cAAc,GAAG;AAAA,MACxC,MAAM,MAAM,KAAK,KACd,QAAQ,gBAAgB,EAAE,EAC1B,QAAQ,aAAa,CAAC,IAAY,WAAmB,OAAO,YAAY,CAAC;AAAA,MAC5E,KAAK,OAAO,KAAK;AAAA,IACnB;AAAA,EACF;AAAA,EACA,OAAO;AAAA;AAOF,SAAS,gBAAe,GAAS;AAAA,EACtC,MAAM,WAAW,SAAS,iBACxB,+DACF;AAAA,EACA,IAAI,SAAS,WAAW;AAAA,IAAG;AAAA,EAC3B,MAAM,cAAa,SAAS,SAAS,SAAS;AAAA,EAC9C,IAAI,CAAC;AAAA,IAAY;AAAA,EACjB,YAAW,cACT,IAAI,YAAY,iBAAiB;AAAA,IAC/B,SAAS;AAAA,IACT,QAAQ,EAAE,IAAI,YAAW,aAAa,iBAAiB,EAAE;AAAA,EAC3D,CAAC,CACH;AAAA;AAMK,SAAS,aAAa,CAAC,OAAqC;AAAA,EACjE,MAAM,SAAS,MAAM;AAAA,EACrB,IAAI,EAAE,kBAAkB;AAAA,IAAU,OAAO;AAAA,EACzC,MAAM,gBAAgB,OAAO,QAAQ,eAAe;AAAA,EACpD,IAAI,EAAE,yBAAyB;AAAA,IAAc,OAAO;AAAA,EACpD,IAAI,MAAM,SAAS,WAAW,cAAc,YAAY;AAAA,IAAQ,OAAO;AAAA,EACvE,MAAM,SAAS,cAAc,aAAa,aAAa;AAAA,EACvD,IAAI,CAAC;AAAA,IAAQ,OAAO;AAAA,EACpB,OAAO;AAAA,IACL;AAAA,IACA,SAAS;AAAA,IACT;AAAA,IACA,MAAM,gBAAgB,aAAa;AAAA,EACrC;AAAA;AAOK,SAAS,sBAAsB,CACpC,YACA,UAAyE,CAAC,GAC9D;AAAA,EACZ,MAAM,oBAAoB,CAAC,UAAiB;AAAA,IAC1C,MAAM,WAAW,cAAc,KAAK;AAAA,IACpC,IAAI;AAAA,MAAU,WAAW,QAAQ;AAAA;AAAA,EAGnC,MAAM,gBAAgB,CAAC,UAAyB;AAAA,IAC9C,IAAI,MAAM,QAAQ,UAAU;AAAA,MAC1B,IAAI,QAAQ,UAAU;AAAA,QACpB,QAAQ,SAAS;AAAA,MACnB,EAAO;AAAA,QACL,gBAAwB;AAAA,QACxB,iBAAgB;AAAA;AAAA,MAElB;AAAA,IACF;AAAA,IACA,IAAI,MAAM,QAAQ,WAAW,MAAM,QAAQ,KAAK;AAAA,MAC9C,MAAM,SAAS,MAAM;AAAA,MACrB,IAAI,EAAE,kBAAkB;AAAA,QAAU;AAAA,MAClC,IAAI,iBAAiB,IAAI,OAAO,OAAO;AAAA,QAAG;AAAA,MAC1C,MAAM,WAAW,cAAc,KAAK;AAAA,MACpC,IAAI,UAAU;AAAA,QACZ,MAAM,eAAe;AAAA,QACrB,WAAW,QAAQ;AAAA,MACrB;AAAA,IACF;AAAA;AAAA,EAGF,MAAM,kBAAkB,CAAC,UAAsB;AAAA,IAC7C,IAAI,EAAE,MAAM,kBAAkB;AAAA,MAAU;AAAA,IACxC,MAAM,iBAAiB,MAAM,OAAO,QAAQ,mBAAmB;AAAA,IAC/D,IAAI;AAAA,MAAgB;AAAA,IACpB,IAAI,MAAM,OAAO,QAAQ,uBAAuB;AAAA,MAAG;AAAA,IACnD,IAAI,QAAQ,uBAAuB;AAAA,MACjC,QAAQ,sBAAsB;AAAA,IAChC,EAAO;AAAA,MACL,gBAAwB;AAAA,MACxB,iBAAgB;AAAA;AAAA;AAAA,EAIpB,WAAW,aAAa,oBAAoB;AAAA,IAC1C,SAAS,iBAAiB,WAAW,iBAAiB;AAAA,EACxD;AAAA,EACA,SAAS,iBAAiB,WAAW,aAAa;AAAA,EAClD,SAAS,iBAAiB,aAAa,eAAe;AAAA,EAEtD,OAAO,MAAM;AAAA,IACX,WAAW,aAAa,oBAAoB;AAAA,MAC1C,SAAS,oBAAoB,WAAW,iBAAiB;AAAA,IAC3D;AAAA,IACA,SAAS,oBAAoB,WAAW,aAAa;AAAA,IACrD,SAAS,oBAAoB,aAAa,eAAe;AAAA;AAAA;;AC9H7D,qCAA6D;AAI7D,SAAS,aAAa,CAAC,QAAuD;AAAA,EAC5E,IAAI,CAAC;AAAA,IAAQ,OAAO;AAAA,EACpB,IAAI,OAAO,oBAAoB,aAAa;AAAA,IAC1C,OAAO,kBAAkB;AAAA,EAC3B;AAAA,EACA,MAAM,IAAI;AAAA,EACV,OAAO,OAAO,EAAE,YAAY,YAAY,EAAE,YAAY;AAAA;AA2CjD,SAAS,UAA2D,CACzE,QAC6B;AAAA,EAC7B,MAAM,YAAY,OAAO,KAAK,OAAO,aAAa;AAAA,EAElD,MAAM,SAAS,CAAC;AAAA,EAChB,WAAW,OAAO,WAAW;AAAA,IAC3B,OAAO,OAAO,QAAO,OAAO,cAAc,IAAI;AAAA,EAChD;AAAA,EAEA,MAAM,SAAS,SAAkB,MAAM;AAAA,IACrC,MAAM,IAAI,CAAC;AAAA,IACX,WAAW,OAAO,WAAW;AAAA,MAC3B,EAAE,OAAO,OAAO,KAAK;AAAA,IACvB;AAAA,IACA,OAAO;AAAA,GACR;AAAA,EAED,MAAM,SAAS,QAAO,KAAK;AAAA,EAC3B,MAAM,eAAe,QAAO,KAAK;AAAA,EACjC,MAAM,SAAS,QAA+C,CAAC,CAAC;AAAA,EAChE,MAAM,aAAa,QAA+B,CAAC,CAAC;AAAA,EAEpD,IAAI,eAAuC;AAAA,EAC3C,IAAI,qBAA0C;AAAA,EAE9C,SAAS,gBAAgB,GAAY;AAAA,IACnC,IAAI,CAAC,cAAc;AAAA,MACjB,MAAM,IAAI,MACR,SAAS,OAAO,qFAClB;AAAA,IACF;AAAA,IACA,OAAO,aAAa;AAAA;AAAA,EAGtB,SAAS,cAAc,GAAS;AAAA,IAC9B,WAAW,OAAO,WAAW;AAAA,MAC3B,OAAO,KAAK,QAAQ,OAAO,cAAc;AAAA,IAC3C;AAAA,IACA,OAAO,QAAQ,CAAC;AAAA;AAAA,EAIlB,SAAS,IAAI,CAAC,UAA6B,SAAiC,CAAC,GAAS;AAAA,IACpF,eAAe;AAAA,IACf,WAAW,QAAQ;AAAA,IACnB,IAAI,OAAO,UAAU,cAAc;AAAA,MACjC,MAAM,iBAAiB,OAAO,OAAO,EAAE,MAAM,QAAQ,QAAQ,iBAAiB,EAAE,CAAC,KAAK,CAAC;AAAA,MACvF,YAAY,GAAG,MAAM,OAAO,QAAQ,cAAc,GAAG;AAAA,QACnD,IAAI,KAAK,QAAQ;AAAA,UACf,OAAO,GAAoB,QAAQ;AAAA,QACrC;AAAA,MACF;AAAA,IACF;AAAA,IACA,IAAI,UAAU;AAAA,MACZ,YAAY,GAAG,MAAM,OAAO,QAAQ,QAAQ,GAAG;AAAA,QAC7C,IAAI,KAAK,QAAQ;AAAA,UACf,OAAO,GAAoB,QAAQ;AAAA,QACrC;AAAA,MACF;AAAA,IACF;AAAA,IACA,OAAO,QAAQ;AAAA;AAAA,EAGjB,SAAS,KAAK,GAAS;AAAA,IACrB,OAAO,QAAQ;AAAA,IACf,eAAe;AAAA,IACf,WAAW,QAAQ,CAAC;AAAA;AAAA,EAItB,eAAe,MAAM,CAAC,OAA8B;AAAA,IAClD,IAAI;AAAA,MAAO,MAAM,eAAe;AAAA,IAChC,IAAI,SAAS,cAAc,MAAM,MAAM,GAAG;AAAA,MACxC,MAAM,KAAK,IAAI,SAAS,MAAM,MAAM;AAAA,MACpC,WAAW,OAAO,WAAW;AAAA,QAC3B,MAAM,MAAM,GAAG,IAAI,GAAwB;AAAA,QAC3C,IAAI,QAAQ,MAAM;AAAA,UAChB,OAAO,KAAK,QAAQ,OAAO,GAAG;AAAA,QAChC;AAAA,MACF;AAAA,IACF;AAAA,IACA,MAAM,UAAU,OAAO;AAAA,IACvB,MAAM,mBAAmB,OAAO,WAAW,OAAO;AAAA,IAClD,IAAI,oBAAoB,OAAO,KAAK,gBAAgB,EAAE,SAAS,GAAG;AAAA,MAChE,OAAO,QAAQ;AAAA,MACf;AAAA,IACF;AAAA,IACA,OAAO,QAAQ,CAAC;AAAA,IAChB,aAAa,QAAQ;AAAA,IACrB,IAAI;AAAA,MACF,MAAM,OAAO,SAAS;AAAA,QACpB,QAAQ;AAAA,QACR,QAAQ,iBAAiB;AAAA,MAC3B,CAAC;AAAA,MACD,MAAM;AAAA,MACN,OAAO,KAAK;AAAA,MACZ,YAAY,GAAG,OAAO,eAAe,GAAG;AAAA,cACxC;AAAA,MACA,aAAa,QAAQ;AAAA;AAAA;AAAA,EAIzB,SAAS,UAAU,CAAC,WAAgC;AAAA,IAClD,eAAe;AAAA,IACf,qBAAqB;AAAA,IACrB,MAAM,UAAU,OAAO;AAAA,IACvB,IAAI,SAAS;AAAA,MACX,qBAAqB,OAAO,MAAM;AAAA,QAChC,IAAI,CAAC,OAAO;AAAA,UAAO;AAAA,QACnB,IAAI,CAAC,QAAQ,EAAE,QAAQ,UAAU,EAAE,CAAC;AAAA,UAAG,MAAM;AAAA,OAC9C;AAAA,IACH;AAAA;AAAA,EAGF,MAAM,UAAmC;AAAA,KACtC,GAAG,OAAO,cAAc,GAAG,WAAW;AAAA,MACrC,KAAK,WAAW,IAAI;AAAA;AAAA,KAErB,GAAG,OAAO,eAAe,MAAM;AAAA,MAC9B,MAAM;AAAA;AAAA,KAEP,GAAG,OAAO,gBAAgB,SAAS,YAAY;AAAA,MAC9C,MAAM,OAAO,KAAK;AAAA;AAAA,EAEtB;AAAA,EAEA,OAAO;AAAA,IACL,MAAM,OAAO;AAAA,IACb;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,EACF;AAAA;AAaK,SAAS,aAAsB,CACpC,OACkB;AAAA,EAClB,MAAM,SAAkC,CAAC;AAAA,EACzC,WAAW,QAAQ,OAAO;AAAA,IACxB,OAAO,OAAO,QAAQ,KAAK,OAAO;AAAA,EACpC;AAAA,EAEA,MAAM,WAAW,SAAwB,MAAM;AAAA,IAC7C,WAAW,QAAQ,OAAO;AAAA,MACxB,IAAI,KAAK,OAAO;AAAA,QAAO,OAAO,KAAK;AAAA,IACrC;AAAA,IACA,OAAO;AAAA,GACR;AAAA,EAED,OAAO;AAAA,IACL,SAAS;AAAA,IACT;AAAA,IACA,QAAQ,GAAG;AAAA,MACT,WAAW,QAAQ;AAAA,QAAO,KAAK,MAAM;AAAA;AAAA,IAEvC,UAAU,CAAC,WAAW;AAAA,MACpB,WAAW,QAAQ;AAAA,QAAO,KAAK,WAAW,SAAS;AAAA;AAAA,EAEvD;AAAA;;AC5OF;AACA;AAAA;AAQO,SAAS,WAA6B,CAAC,MAAY;AAAA,EACxD,OAAO;AAAA;AAGT,IAAM,eAAe,cAAuB,IAAI;AAOzC,SAAS,aAAsB,GAAG,UAAU,UAAuC;AAAA,EACxF,uBAAO,OAAkD,aAAa,UAA/D;AAAA,IAAuB,OAAO;AAAA,IAA9B;AAAA,sCAAkD;AAAA;AAGpD,SAAS,SAAkB,GAAY;AAAA,EAC5C,MAAM,MAAM,WAAW,YAAY;AAAA,EACnC,IAAI,QAAQ,MAAM;AAAA,IAChB,MAAM,IAAI,MAAM,+CAA+C;AAAA,EACjE;AAAA,EACA,OAAO;AAAA;;AC7BF,SAAS,iBAAiB,CAC/B,QAAgC,CAAC,GACjC,UAAU,OACG;AAAA,EACb,MAAM,UAAU,IAAI,IAAI,OAAO,QAAQ,KAAK,CAAC;AAAA,EAC7C,MAAM,KAAK;AAAA,IACT;AAAA,IACA,UAAU;AAAA,IACV,cAAc,CAAC,SAAiB,QAAQ,IAAI,IAAI,KAAK;AAAA,IACrD,cAAc,CAAC,MAAc,UAAkB;AAAA,MAC7C,QAAQ,IAAI,MAAM,KAAK;AAAA;AAAA,IAEzB,cAAc,CAAC,SAAiB,QAAQ,IAAI,IAAI;AAAA,IAChD,iBAAiB,CAAC,SAAiB;AAAA,MACjC,QAAQ,OAAO,IAAI;AAAA;AAAA,IAErB,YAAY,MAAM,KAAK,QAAQ,QAAQ,CAAC,EAAE,IAAI,EAAE,MAAM,YAAY;AAAA,MAChE;AAAA,MACA;AAAA,IACF,EAAE;AAAA,EACJ;AAAA,EACA,OAAO;AAAA;AAIF,SAAS,qBAAqB,CAAC,MAAuD;AAAA,EAC3F,IAAI;AAAA,EACJ,IAAI,OAAO,oBAAoB,eAAe,gBAAgB,iBAAiB;AAAA,IAC7E,SAAS;AAAA,EACX,EAAO;AAAA,IACL,SAAS,sBAAsB,IAAyC;AAAA;AAAA,EAE1E,IAAI,mBAAmB;AAAA,EACvB,OAAO;AAAA,IACL,MAAM;AAAA,IACN;AAAA,IACA,eAAe;AAAA,IACf,cAAc,GAAG;AAAA,MACf,mBAAmB;AAAA;AAAA,IAErB,eAAe,GAAG;AAAA,QAGd,gBAAgB,GAAG;AAAA,MACrB,OAAO;AAAA;AAAA,EAEX;AAAA;AAGF,SAAS,qBAAqB,CAAC,QAAiD;AAAA,EAC9E,OAAO;AAAA,IACL,UAAU;AAAA,IACV,SAAS;AAAA,IACT,UAAU,OAAO,QAAQ,MAAM,EAAE,IAAI,EAAE,MAAM,YAAY;AAAA,MACvD;AAAA,MACA;AAAA,IACF,EAAE;AAAA,EACJ;AAAA;AAMK,SAAS,gBAAwC,CAAC,UAA4B,CAAC,GAAY;AAAA,EAChG,OAAO;AAAA;AAOT,eAAsB,SAAkB,CACtC,UACA,QACA,KACe;AAAA,EACf,MAAM,UAA8C,SAAS;AAAA,EAC7D,IAAI,CAAC,SAAS;AAAA,IACZ,MAAM,IAAI,MAAM,qCAAqC,SAAS;AAAA,EAChE;AAAA,EACA,MAAM,UAAkC;AAAA,IACtC,QAAQ,IAAI;AAAA,IACZ,OAAO,IAAI,SAAS,IAAI,MAAM,OAAO;AAAA,IACrC,SAAS,IAAI,WAAW,kBAAkB;AAAA,IAC1C,MAAM,IAAI,QAAQ,CAAC;AAAA,EACrB;AAAA,EACA,MAAM,QAAQ,OAAO;AAAA;",
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:after {
597
- content: var(--polly-caret-glyph);
598
- display: block;
599
- font-size: var(--polly-caret-size);
600
- color: var(--polly-caret-color);
601
- transition: transform var(--polly-motion-fast) ease;
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:after {
608
- transform: rotate(180deg);
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:after {
786
- content: var(--polly-caret-glyph);
787
- display: block;
788
- font-size: var(--polly-caret-size);
789
- color: var(--polly-caret-color);
790
- transition: transform var(--polly-motion-fast) ease;
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:after {
797
- transform: rotate(180deg);
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 one glyph, one size, one colour shared across
84
- * every disclosure-style primitive (Collapsible, Select / ActionSelect)
85
- * so the indicator reads the same wherever it appears. Rotation
86
- * magnitude stays per-use: a future primitive may want a quarter
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-md);
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 write results to window.__testResults.
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 recorded on window.__testResults
7
- * and polled by the companion runner (run.ts). Applications import this
8
- * module in their *.browser.ts test files and call done() at the end.
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["__testResults"] = results;
247
- window["__done"] = true;
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=3EDEF01EC53C182D64756E2164756E21
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 write results to window.__testResults.\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 (window as unknown as Record<string, unknown>)[\"__testResults\"] = results;\n (window as unknown as Record<string, unknown>)[\"__done\"] = true;\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"
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": "3EDEF01EC53C182D64756E2164756E21",
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
- if (isProtocolError(err)) {
169
- log(` ⚠️ protocol error (${errMessage(err)}) retrying once on a fresh page`);
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
- page = await browser.newPage();
260
- page.on("console", (msg) => {
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
- page.on("pageerror", (err) => {
267
- console.log(` ❌ page error: ${errMessage(err)}`);
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 page.goto(`http://127.0.0.1:${server.port}/`, { waitUntil: "domcontentloaded" });
270
- const timeout = 15000;
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=B8D937D1C5C3E44264756E2164756E21
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 caps how long any single CDP call (e.g. page.evaluate)\n// waits before throwing. Puppeteer's default is 180s, so a stalled\n// renderer hangs the runner for three minutes. 30s fails fast while\n// still tolerating a slow-but-healthy initial sync.\n\nconst browser = await puppeteer.launch({\n headless,\n args: [\"--no-sandbox\", \"--disable-setuid-sandbox\"],\n protocolTimeout: 30_000,\n});\n\n/**\n * Build, serve, and run one test file on a fresh page. Returns its\n * pass/fail tally. Build failures and test timeouts are reported here\n * (not thrown). A thrown error (e.g. a ProtocolError from a stalled\n * renderer) propagates to the caller so it can retry the file; the\n * page and server are always cleaned up first.\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 page = await browser.newPage();\n page.on(\"console\", (msg) => {\n const text = msg.text();\n if (text.includes(\"[test]\")) {\n console.log(` ${text}`);\n }\n });\n page.on(\"pageerror\", (err: unknown) => {\n console.log(` ❌ page error: ${errMessage(err)}`);\n });\n\n await page.goto(`http://127.0.0.1:${server.port}/`, { waitUntil: \"domcontentloaded\" });\n\n const timeout = 15_000;\n const deadline = Date.now() + timeout;\n let finished = false;\n while (Date.now() < deadline) {\n finished = await page.evaluate(\n () => (window as unknown as Record<string, unknown>)[\"__done\"] === true\n );\n if (finished) break;\n await new Promise((r) => setTimeout(r, 100));\n }\n\n if (!finished) {\n console.log(` ❌ timed out after ${timeout}ms`);\n return { passed: 0, failed: 1 };\n }\n\n const results = await page.evaluate(\n () =>\n (window as unknown as Record<string, unknown>)[\"__testResults\"] as unknown as Array<{\n name: string;\n passed: boolean;\n error?: string;\n }>\n );\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 // Always run, even on the error path. close() can itself throw a\n // ProtocolError under a stall — swallow it so cleanup completes.\n if (page) {\n await page.close().catch(() => {\n // ignore — page may already be gone after a stall\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",
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 transient CDP\n * timeout on one file never aborts the whole suite — is a pure function\n * that can be unit-tested without launching a browser. run.ts supplies\n * the real 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\n/** A transient CDP timeout (renderer stall) — retryable, unlike a red test. */\nexport function isProtocolError(err: unknown): boolean {\n return err instanceof Error && err.name === \"ProtocolError\";\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 * - A thrown `ProtocolError` (renderer stall) is retried once on a fresh\n * page; if the retry also throws, the file is recorded as failed.\n * - Any other thrown error fails only that file.\n * - A file that returns a tally with `failed > 0` (a genuine red test) is\n * reported as-is and never retried.\n *\n * The loop never rejects: a stall in one file can never abort the suite,\n * so 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 if (isProtocolError(err)) {\n log(` ⚠️ protocol error (${errMessage(err)}) — retrying once on a fresh page`);\n try {\n result = await runFile(testFile);\n } catch (retryErr) {\n log(` ❌ retry failed: ${errMessage(retryErr)}`);\n result = { passed: 0, failed: 1 };\n }\n } else {\n // A non-protocol error: record the file as failed and keep going,\n // never abort the whole suite.\n log(` ❌ ${errMessage(err)}`);\n result = { passed: 0, failed: 1 };\n }\n }\n\n totalPassed += result.passed;\n totalFailed += result.failed;\n }\n\n return { passed: totalPassed, failed: totalFailed };\n}\n"
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": "B8D937D1C5C3E44264756E2164756E21",
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 transient CDP
5
- * timeout on one file never aborts the whole suite — is a pure function
6
- * that can be unit-tested without launching a browser. run.ts supplies
7
- * the real per-file `runFile`; the test suite supplies a fake one.
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
- * - A thrown `ProtocolError` (renderer stall) is retried once on a fresh
21
- * page; if the retry also throws, the file is recorded as failed.
22
- * - Any other thrown error fails only that file.
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 stall in one file can never abort the suite,
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.1",
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",