@vielzeug/assay 26.10.0 → 26.10.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # @vielzeug/assay
2
2
 
3
- > Framework-agnostic DOM testing primitives — scoped queries, event dispatch, async waiting
3
+ > Framework-agnostic DOM testing primitives: scoped queries, event dispatch, async waiting
4
4
 
5
5
  ## Installation
6
6
 
@@ -35,4 +35,4 @@ await eventually(() => expect(view.get('.status').textContent).toBe('Saved'), {
35
35
 
36
36
  ## License
37
37
 
38
- MIT © [Helmuth Saatkamp](https://github.com/helmuthdu) — part of the [Vielzeug](https://github.com/helmuthdu/vielzeug) monorepo.
38
+ MIT © [Helmuth Saatkamp](https://github.com/helmuthdu): part of the [Vielzeug](https://github.com/helmuthdu/vielzeug) monorepo.
@@ -1 +1 @@
1
- {"version":3,"file":"assay.cjs","names":[],"sources":["../src/errors.ts","../src/events.ts","../src/query.ts","../src/wait.ts"],"sourcesContent":["/** Base class for all Assay errors. Use `instanceof AssayError` to catch any Assay-originated error. */\nexport class AssayError extends Error {\n constructor(message: string, opts?: ErrorOptions) {\n super(message, opts);\n this.name = new.target.name;\n Object.setPrototypeOf(this, new.target.prototype);\n }\n}\n\n/** Thrown when a required query has no match. */\nexport class AssayQueryError extends AssayError {}\n\n/** Thrown when an Assay wait reaches its timeout. */\nexport class AssayTimeoutError extends AssayError {}\n","/**\n * # Confidence boundary — read first\n *\n * `dispatch` is a **low-level synthetic-event dispatch API**. It fires a caller-\n * supplied DOM `Event` synchronously and returns the `dispatchEvent`\n * result. It does **not** model real user interaction: there is no focus/blur\n * sequencing, no input-event coordination, no keyboard layout, no pointer\n * hit-testing, and no trust flag (`event.isTrusted` is always `false`).\n *\n * Use it to drive event handlers in unit/integration tests where you control\n * the exact event shape. For **behavioral confidence** — that a real click\n * actually activates a control, that focus management works end-to-end, that\n * keyboard shortcuts fire under a real input device — write Playwright tests.\n * Assay deliberately stops at the dispatch boundary; anything that depends on\n * the browser's interaction semantics belongs in a browser-driven suite.\n */\n\n/** Dispatch a pre-built `Event` instance unchanged. Returns the `dispatchEvent` result. */\nexport const dispatch = (target: EventTarget, event: Event): boolean => target.dispatchEvent(event);\n\n/** Synthetic convenience dispatchers. They share the same confidence boundary as `dispatch()`. */\nexport const fireBlur = (target: EventTarget, init: FocusEventInit = {}): boolean =>\n dispatch(target, new FocusEvent('blur', init));\n\nexport const fireChange = (target: EventTarget, init: EventInit = {}): boolean =>\n dispatch(target, new Event('change', { bubbles: true, ...init }));\n\nexport const fireClick = (target: EventTarget, init: MouseEventInit = {}): boolean =>\n dispatch(target, new MouseEvent('click', { bubbles: true, cancelable: true, ...init }));\n\nexport const fireCustom = <T>(target: EventTarget, type: string, init: CustomEventInit<T> = {}): boolean =>\n dispatch(target, new CustomEvent(type, { bubbles: true, cancelable: true, composed: false, ...init }));\n\nexport const fireFocus = (target: EventTarget, init: FocusEventInit = {}): boolean =>\n dispatch(target, new FocusEvent('focus', init));\n\nexport const fireInput = (target: EventTarget, init: InputEventInit = {}): boolean =>\n dispatch(target, new InputEvent('input', { bubbles: true, ...init }));\n\nexport const fireKeyDown = (target: EventTarget, init: KeyboardEventInit = {}): boolean =>\n dispatch(target, new KeyboardEvent('keydown', { bubbles: true, cancelable: true, ...init }));\n\nexport const fireKeyUp = (target: EventTarget, init: KeyboardEventInit = {}): boolean =>\n dispatch(target, new KeyboardEvent('keyup', { bubbles: true, cancelable: true, ...init }));\n\nexport const fireSubmit = (target: EventTarget, init: SubmitEventInit = {}): boolean =>\n dispatch(target, new SubmitEvent('submit', { bubbles: true, cancelable: true, ...init }));\n","import { AssayQueryError } from './errors';\n\n/** DOM query helpers for test environments. */\n\n// ─── Types ───────────────────────────────────────────────────────────────────\n\n/** Scoped query helpers for any queryable DOM root — see {@link within}. */\nexport interface QueryScope {\n get<E extends Element = Element>(selector: string): E;\n getByTestId<E extends Element = Element>(testId: string): E;\n getByText<E extends Element = Element>(text: string, selector?: string): E;\n query<E extends Element = Element>(selector: string): E | null;\n queryAll<E extends Element = Element>(selector: string): E[];\n queryAllByTestId<E extends Element = Element>(testId: string): E[];\n queryAllByText<E extends Element = Element>(text: string, selector?: string): E[];\n queryByTestId<E extends Element = Element>(testId: string): E | null;\n queryByText<E extends Element = Element>(text: string, selector?: string): E | null;\n}\n\n// ─── Helpers ─────────────────────────────────────────────────────────────────\n\ntype QueryRoot = ParentNode;\n\nconst describeRoot = (root: QueryRoot): string => {\n if (root instanceof Element) return root.outerHTML.slice(0, 500);\n\n if (root instanceof ShadowRoot) return `<shadow-root>${root.innerHTML.slice(0, 500)}</shadow-root>`;\n\n return root.nodeName;\n};\n\nconst requireMatch = <E extends Element>(element: E | null, description: string, root: QueryRoot): E => {\n if (element) return element;\n\n throw new AssayQueryError(`Unable to find ${description} within ${describeRoot(root)}.`);\n};\n\nconst query = <E extends Element = Element>(root: QueryRoot, selector: string): E | null =>\n root.querySelector<E>(selector);\n\nconst queryAll = <E extends Element = Element>(root: QueryRoot, selector: string): E[] => {\n return Array.from(root.querySelectorAll<E>(selector));\n};\n\nconst queryByTestId = <E extends Element = Element>(root: QueryRoot, testId: string): E | null =>\n queryAll<E>(root, '[data-testid]').find((element) => element.getAttribute('data-testid') === testId) ?? null;\n\nconst queryAllByTestId = <E extends Element = Element>(root: QueryRoot, testId: string): E[] =>\n queryAll<E>(root, '[data-testid]').filter((element) => element.getAttribute('data-testid') === testId);\n\nconst queryByText = <E extends Element = Element>(root: QueryRoot, text: string, selector = '*'): E | null => {\n for (const element of root.querySelectorAll<E>(selector)) {\n if (element.textContent?.trim() === text) return element;\n }\n\n return null;\n};\n\nconst queryAllByText = <E extends Element = Element>(root: QueryRoot, text: string, selector = '*'): E[] =>\n queryAll<E>(root, selector).filter((element) => element.textContent?.trim() === text);\n\n/**\n * Queries the shadow root of a custom element for a matching CSS selector.\n * Returns `null` if the element has no shadow root or no match is found — safe to call on\n * both open-shadow custom elements and plain elements without checking `shadowRoot` first.\n */\nexport function queryInShadow<E extends Element = Element>(host: Element, selector: string): E | null {\n return host.shadowRoot ? query<E>(host.shadowRoot, selector) : null;\n}\n\n/** Queries all matching elements inside the shadow root of a custom element. */\nexport function queryAllInShadow<E extends Element = Element>(host: Element, selector: string): E[] {\n return host.shadowRoot ? queryAll<E>(host.shadowRoot, selector) : [];\n}\n\n/**\n * Queries a shadow DOM element by its CSS `part` attribute — shorthand for\n * `queryInShadow(host, '[part=\"name\"]')`.\n *\n * @example\n * const btn = queryPart(carousel, 'prev-btn');\n * expect(btn).not.toBeNull();\n */\nexport function queryPart<E extends Element = Element>(host: Element, part: string): E | null {\n return (\n queryAllInShadow<E>(host, '[part]').find((element) => element.getAttribute('part')?.split(/\\s+/).includes(part)) ??\n null\n );\n}\n\n/**\n * Returns the light-DOM children assigned to a named slot, or every child assigned to the\n * default slot when no name is given.\n *\n * @example\n * const slides = getSlotted(carousel);\n * expect(slides).toHaveLength(3);\n */\nexport function getSlotted<E extends Element = Element>(host: Element, slotName?: string): E[] {\n return Array.from(host.children).filter((element) =>\n slotName === undefined ? !element.getAttribute('slot') : element.getAttribute('slot') === slotName,\n ) as E[];\n}\n\n// ─── API ─────────────────────────────────────────────────────────────────────\n\n/**\n * Create query helpers scoped to a DOM root — useful for light DOM and shadow roots.\n *\n * @example\n * const view = within(fixture.shadow!);\n * expect(view.get('.title').textContent).toBe('Hello');\n */\nexport function within(root: QueryRoot): QueryScope {\n return {\n get: <E extends Element = Element>(selector: string) =>\n requireMatch(query<E>(root, selector), `\"${selector}\"`, root),\n getByTestId: <E extends Element = Element>(testId: string) =>\n requireMatch(queryByTestId<E>(root, testId), `data-testid \"${testId}\"`, root),\n getByText: <E extends Element = Element>(text: string, selector = '*') =>\n requireMatch(queryByText<E>(root, text, selector), `text \"${text}\" matching \"${selector}\"`, root),\n query: <E extends Element = Element>(selector: string) => query<E>(root, selector),\n queryAll: <E extends Element = Element>(selector: string) => queryAll<E>(root, selector),\n queryAllByTestId: <E extends Element = Element>(testId: string) => queryAllByTestId<E>(root, testId),\n queryAllByText: <E extends Element = Element>(text: string, selector = '*') =>\n queryAllByText<E>(root, text, selector),\n queryByTestId: <E extends Element = Element>(testId: string) => queryByTestId<E>(root, testId),\n queryByText: <E extends Element = Element>(text: string, selector = '*') => queryByText<E>(root, text, selector),\n };\n}\n","import { abortError, sleep } from '@vielzeug/arsenal';\n\nimport { AssayTimeoutError } from './errors';\n\nexport interface DelayOptions {\n signal?: AbortSignal;\n}\n\nexport interface WaitOptions {\n /** Polling interval in ms (default: 50). */\n interval?: number;\n /** Cancel the pending wait. */\n signal?: AbortSignal;\n /** Maximum wait time in ms (default: 1000). */\n timeout?: number;\n}\n\nexport interface EventuallyOptions extends WaitOptions {\n /** Context included in the timeout error. */\n message?: string;\n}\n\nconst DEADLINE = Symbol('assay.deadline');\nconst MAX_TIMER_MS = 2_147_483_647;\nconst now = (): number => performance.now();\n\nconst validateDuration = (name: string, value: number, allowZero: boolean): void => {\n if (!Number.isFinite(value) || value > MAX_TIMER_MS || (allowZero ? value < 0 : value <= 0)) {\n throw new RangeError(\n `${name} must be a ${allowZero ? 'non-negative' : 'positive'} finite number no greater than ${MAX_TIMER_MS}`,\n );\n }\n};\n\nconst validateWait = (interval: number, timeout: number): void => {\n validateDuration('interval', interval, false);\n validateDuration('timeout', timeout, true);\n};\n\nconst runBounded = <T>(run: () => T | Promise<T>, deadline: number, signal?: AbortSignal): Promise<T> =>\n new Promise((resolve, reject) => {\n if (signal?.aborted) {\n reject(abortError(signal, 'The operation was aborted.'));\n return;\n }\n\n let settled = false;\n const cleanup = (): boolean => {\n if (settled) return false;\n settled = true;\n clearTimeout(timer);\n signal?.removeEventListener('abort', abort);\n return true;\n };\n const resolveOnce = (value: T): void => {\n if (cleanup()) resolve(value);\n };\n const rejectOnce = (error: unknown): void => {\n if (cleanup()) reject(error);\n };\n const timer = setTimeout(() => rejectOnce(DEADLINE), Math.max(0, deadline - now()));\n const abort = () => rejectOnce(abortError(signal, 'The operation was aborted.'));\n\n signal?.addEventListener('abort', abort, { once: true });\n\n try {\n Promise.resolve(run()).then(resolveOnce, rejectOnce);\n } catch (error) {\n rejectOnce(error);\n }\n });\n\nexport const delay = (ms = 0, options: DelayOptions = {}): Promise<void> => {\n validateDuration('ms', ms, true);\n return sleep(ms, options.signal);\n};\n\nexport async function waitUntil(\n predicate: () => boolean | Promise<boolean>,\n { interval = 50, signal, timeout = 1000 }: WaitOptions = {},\n): Promise<void> {\n validateWait(interval, timeout);\n const deadline = now() + timeout;\n let first = true;\n\n for (;;) {\n if (signal?.aborted) throw abortError(signal, 'The operation was aborted.');\n if (!first && now() >= deadline) throw new AssayTimeoutError(`waitUntil timed out after ${timeout}ms`);\n first = false;\n\n let matched: boolean;\n try {\n matched = await runBounded(predicate, deadline, signal);\n } catch (error) {\n if (error === DEADLINE) throw new AssayTimeoutError(`waitUntil timed out after ${timeout}ms`);\n throw error;\n }\n if (matched) return;\n\n const remaining = deadline - now();\n if (remaining <= 0) throw new AssayTimeoutError(`waitUntil timed out after ${timeout}ms`);\n await sleep(Math.min(interval, remaining), signal);\n }\n}\n\n/**\n * Retry an assertion until it stops throwing or the timeout elapses.\n *\n * Polls the assertion on a fixed interval and preserves the last failure as the\n * timeout cause. Pending asynchronous assertions remain bounded by the same\n * deadline. Use `waitUntil()` for boolean conditions, `delay()` for explicit\n * timer dependencies, and `nextTick()` for one queued microtask boundary.\n *\n * @example\n * await eventually(() => expect(region.textContent).toBe('Saved'));\n */\nexport async function eventually(\n assertion: () => void | Promise<void>,\n { interval = 50, message, signal, timeout = 1000 }: EventuallyOptions = {},\n): Promise<void> {\n validateWait(interval, timeout);\n const deadline = now() + timeout;\n let first = true;\n let lastError: unknown;\n\n for (;;) {\n if (signal?.aborted) throw abortError(signal, 'The operation was aborted.');\n if (!first && now() >= deadline) break;\n first = false;\n\n try {\n await runBounded(assertion, deadline, signal);\n return;\n } catch (error) {\n if (signal?.aborted) throw abortError(signal, 'The operation was aborted.');\n if (error === DEADLINE) break;\n lastError = error;\n }\n\n const remaining = deadline - now();\n if (remaining <= 0) break;\n await sleep(Math.min(interval, remaining), signal);\n }\n\n const timing = `eventually timed out after ${timeout}ms`;\n const context = message ? `${message} (${timing})` : timing;\n const detail = lastError instanceof Error ? lastError.message : lastError == null ? '' : String(lastError);\n\n throw new AssayTimeoutError(detail ? `${context}\\n${detail}` : context, { cause: lastError });\n}\n\n/** Resolve with the next event of `type`, or reject when it times out or is aborted. */\nexport function waitForEvent<T extends Event = Event>(\n target: EventTarget,\n type: string,\n { signal, timeout = 1000 }: Omit<WaitOptions, 'interval'> = {},\n): Promise<T> {\n validateDuration('timeout', timeout, true);\n\n return new Promise((resolve, reject) => {\n if (signal?.aborted) {\n reject(abortError(signal, 'The operation was aborted.'));\n return;\n }\n\n const dispose = () => {\n clearTimeout(timer);\n signal?.removeEventListener('abort', onAbort);\n target.removeEventListener(type, onEvent);\n };\n const onEvent = (event: Event): void => {\n dispose();\n resolve(event as T);\n };\n const timer = setTimeout(() => {\n dispose();\n reject(new AssayTimeoutError(`waitForEvent: \"${type}\" timed out after ${timeout}ms`));\n }, timeout);\n const onAbort = () => {\n dispose();\n reject(abortError(signal, 'The operation was aborted.'));\n };\n\n target.addEventListener(type, onEvent, { once: true });\n signal?.addEventListener('abort', onAbort, { once: true });\n });\n}\n"],"mappings":"sGACA,IAAa,WAAb,cAAgC,KAAM,CACpC,YAAY,EAAiB,EAAqB,CAChD,MAAM,EAAS,CAAI,EACnB,KAAK,KAAO,WAAW,KACvB,OAAO,eAAe,KAAM,WAAW,SAAS,CAClD,CACF,EAGa,gBAAb,cAAqC,UAAW,CAAC,EAGpC,kBAAb,cAAuC,UAAW,CAAC,ECKtC,UAAY,EAAqB,IAA0B,EAAO,cAAc,CAAK,EAGrF,UAAY,EAAqB,EAAuB,CAAC,IACpE,SAAS,EAAQ,IAAI,WAAW,OAAQ,CAAI,CAAC,EAElC,YAAc,EAAqB,EAAkB,CAAC,IACjE,SAAS,EAAQ,IAAI,MAAM,SAAU,CAAE,QAAS,GAAM,GAAG,CAAK,CAAC,CAAC,EAErD,WAAa,EAAqB,EAAuB,CAAC,IACrE,SAAS,EAAQ,IAAI,WAAW,QAAS,CAAE,QAAS,GAAM,WAAY,GAAM,GAAG,CAAK,CAAC,CAAC,EAE3E,YAAiB,EAAqB,EAAc,EAA2B,CAAC,IAC3F,SAAS,EAAQ,IAAI,YAAY,EAAM,CAAE,QAAS,GAAM,WAAY,GAAM,SAAU,GAAO,GAAG,CAAK,CAAC,CAAC,EAE1F,WAAa,EAAqB,EAAuB,CAAC,IACrE,SAAS,EAAQ,IAAI,WAAW,QAAS,CAAI,CAAC,EAEnC,WAAa,EAAqB,EAAuB,CAAC,IACrE,SAAS,EAAQ,IAAI,WAAW,QAAS,CAAE,QAAS,GAAM,GAAG,CAAK,CAAC,CAAC,EAEzD,aAAe,EAAqB,EAA0B,CAAC,IAC1E,SAAS,EAAQ,IAAI,cAAc,UAAW,CAAE,QAAS,GAAM,WAAY,GAAM,GAAG,CAAK,CAAC,CAAC,EAEhF,WAAa,EAAqB,EAA0B,CAAC,IACxE,SAAS,EAAQ,IAAI,cAAc,QAAS,CAAE,QAAS,GAAM,WAAY,GAAM,GAAG,CAAK,CAAC,CAAC,EAE9E,YAAc,EAAqB,EAAwB,CAAC,IACvE,SAAS,EAAQ,IAAI,YAAY,SAAU,CAAE,QAAS,GAAM,WAAY,GAAM,GAAG,CAAK,CAAC,CAAC,ECvBpF,aAAgB,GAChB,aAAgB,QAAgB,EAAK,UAAU,MAAM,EAAG,GAAG,EAE3D,aAAgB,WAAmB,gBAAgB,EAAK,UAAU,MAAM,EAAG,GAAG,EAAE,gBAE7E,EAAK,SAGR,cAAmC,EAAmB,EAAqB,IAAuB,CACtG,GAAI,EAAS,OAAO,EAEpB,MAAM,IAAI,gBAAgB,kBAAkB,EAAY,UAAU,aAAa,CAAI,EAAE,EAAE,CACzF,EAEM,OAAsC,EAAiB,IAC3D,EAAK,cAAiB,CAAQ,EAE1B,UAAyC,EAAiB,IACvD,MAAM,KAAK,EAAK,iBAAoB,CAAQ,CAAC,EAGhD,eAA8C,EAAiB,IACnE,SAAY,EAAM,eAAe,CAAC,CAAC,KAAM,GAAY,EAAQ,aAAa,aAAa,IAAM,CAAM,GAAK,KAEpG,kBAAiD,EAAiB,IACtE,SAAY,EAAM,eAAe,CAAC,CAAC,OAAQ,GAAY,EAAQ,aAAa,aAAa,IAAM,CAAM,EAEjG,aAA4C,EAAiB,EAAc,EAAW,MAAkB,CAC5G,IAAK,IAAM,KAAW,EAAK,iBAAoB,CAAQ,EACrD,GAAI,EAAQ,aAAa,KAAK,IAAM,EAAM,OAAO,EAGnD,OAAO,IACT,EAEM,gBAA+C,EAAiB,EAAc,EAAW,MAC7F,SAAY,EAAM,CAAQ,CAAC,CAAC,OAAQ,GAAY,EAAQ,aAAa,KAAK,IAAM,CAAI,EAOtF,SAAgB,cAA2C,EAAe,EAA4B,CACpG,OAAO,EAAK,WAAa,MAAS,EAAK,WAAY,CAAQ,EAAI,IACjE,CAGA,SAAgB,iBAA8C,EAAe,EAAuB,CAClG,OAAO,EAAK,WAAa,SAAY,EAAK,WAAY,CAAQ,EAAI,CAAC,CACrE,CAUA,SAAgB,UAAuC,EAAe,EAAwB,CAC5F,OACE,iBAAoB,EAAM,QAAQ,CAAC,CAAC,KAAM,GAAY,EAAQ,aAAa,MAAM,CAAC,EAAE,MAAM,KAAK,CAAC,CAAC,SAAS,CAAI,CAAC,GAC/G,IAEJ,CAUA,SAAgB,WAAwC,EAAe,EAAwB,CAC7F,OAAO,MAAM,KAAK,EAAK,QAAQ,CAAC,CAAC,OAAQ,GACvC,IAAa,IAAA,GAAY,CAAC,EAAQ,aAAa,MAAM,EAAI,EAAQ,aAAa,MAAM,IAAM,CAC5F,CACF,CAWA,SAAgB,OAAO,EAA6B,CAClD,MAAO,CACL,IAAmC,GACjC,aAAa,MAAS,EAAM,CAAQ,EAAG,IAAI,EAAS,GAAI,CAAI,EAC9D,YAA2C,GACzC,aAAa,cAAiB,EAAM,CAAM,EAAG,gBAAgB,EAAO,GAAI,CAAI,EAC9E,WAAyC,EAAc,EAAW,MAChE,aAAa,YAAe,EAAM,EAAM,CAAQ,EAAG,SAAS,EAAK,cAAc,EAAS,GAAI,CAAI,EAClG,MAAqC,GAAqB,MAAS,EAAM,CAAQ,EACjF,SAAwC,GAAqB,SAAY,EAAM,CAAQ,EACvF,iBAAgD,GAAmB,iBAAoB,EAAM,CAAM,EACnG,gBAA8C,EAAc,EAAW,MACrE,eAAkB,EAAM,EAAM,CAAQ,EACxC,cAA6C,GAAmB,cAAiB,EAAM,CAAM,EAC7F,aAA2C,EAAc,EAAW,MAAQ,YAAe,EAAM,EAAM,CAAQ,CACjH,CACF,CC3GA,IAAM,EAAW,OAAO,gBAAgB,EAClC,EAAe,WACf,QAAoB,YAAY,IAAI,EAEpC,kBAAoB,EAAc,EAAe,IAA6B,CAClF,GAAI,CAAC,OAAO,SAAS,CAAK,GAAK,EAAQ,IAAiB,EAAY,EAAQ,EAAI,GAAS,GACvF,MAAU,WACR,GAAG,EAAK,aAAa,EAAY,eAAiB,WAAW,iCAAiC,GAChG,CAEJ,EAEM,cAAgB,EAAkB,IAA0B,CAChE,iBAAiB,WAAY,EAAU,EAAK,EAC5C,iBAAiB,UAAW,EAAS,EAAI,CAC3C,EAEM,YAAiB,EAA2B,EAAkB,IAClE,IAAI,SAAS,EAAS,IAAW,CAC/B,GAAI,GAAQ,QAAS,CACnB,GAAA,EAAO,EAAA,WAAA,CAAW,EAAQ,4BAA4B,CAAC,EACvD,MACF,CAEA,IAAI,EAAU,GACR,YACJ,CAAI,IACJ,EAAU,GACV,aAAa,CAAK,EAClB,GAAQ,oBAAoB,QAAS,KAAK,EACnC,IAEH,YAAe,GAAmB,CAClC,QAAQ,GAAG,EAAQ,CAAK,CAC9B,EACM,WAAc,GAAyB,CACvC,QAAQ,GAAG,EAAO,CAAK,CAC7B,EACM,EAAQ,eAAiB,WAAW,CAAQ,EAAG,KAAK,IAAI,EAAG,EAAW,IAAI,CAAC,CAAC,EAC5E,UAAc,YAAA,EAAW,EAAA,WAAA,CAAW,EAAQ,4BAA4B,CAAC,EAE/E,GAAQ,iBAAiB,QAAS,MAAO,CAAE,KAAM,EAAK,CAAC,EAEvD,GAAI,CACF,QAAQ,QAAQ,EAAI,CAAC,CAAC,CAAC,KAAK,YAAa,UAAU,CACrD,OAAS,EAAO,CACd,WAAW,CAAK,CAClB,CACF,CAAC,EAEU,OAAS,EAAK,EAAG,EAAwB,CAAC,KACrD,iBAAiB,KAAM,EAAI,EAAI,GAC/B,EAAO,EAAA,MAAA,CAAM,EAAI,EAAQ,MAAM,GAGjC,eAAsB,UACpB,EACA,CAAE,WAAW,GAAI,SAAQ,UAAU,KAAsB,CAAC,EAC3C,CACf,aAAa,EAAU,CAAO,EAC9B,IAAM,EAAW,IAAI,EAAI,EACrB,EAAQ,GAEZ,OAAS,CACP,GAAI,GAAQ,QAAS,MAAA,EAAM,EAAA,WAAA,CAAW,EAAQ,4BAA4B,EAC1E,GAAI,CAAC,GAAS,IAAI,GAAK,EAAU,MAAM,IAAI,kBAAkB,6BAA6B,EAAQ,GAAG,EACrG,EAAQ,GAER,IAAI,EACJ,GAAI,CACF,EAAU,MAAM,WAAW,EAAW,EAAU,CAAM,CACxD,OAAS,EAAO,CAEd,MADI,IAAU,EAAgB,IAAI,kBAAkB,6BAA6B,EAAQ,GAAG,EACtF,CACR,CACA,GAAI,EAAS,OAEb,IAAM,EAAY,EAAW,IAAI,EACjC,GAAI,GAAa,EAAG,MAAM,IAAI,kBAAkB,6BAA6B,EAAQ,GAAG,EACxF,MAAA,EAAM,EAAA,MAAA,CAAM,KAAK,IAAI,EAAU,CAAS,EAAG,CAAM,CACnD,CACF,CAaA,eAAsB,WACpB,EACA,CAAE,WAAW,GAAI,UAAS,SAAQ,UAAU,KAA4B,CAAC,EAC1D,CACf,aAAa,EAAU,CAAO,EAC9B,IAAM,EAAW,IAAI,EAAI,EACrB,EAAQ,GACR,EAEJ,OAAS,CACP,GAAI,GAAQ,QAAS,MAAA,EAAM,EAAA,WAAA,CAAW,EAAQ,4BAA4B,EAC1E,GAAI,CAAC,GAAS,IAAI,GAAK,EAAU,MACjC,EAAQ,GAER,GAAI,CACF,MAAM,WAAW,EAAW,EAAU,CAAM,EAC5C,MACF,OAAS,EAAO,CACd,GAAI,GAAQ,QAAS,MAAA,EAAM,EAAA,WAAA,CAAW,EAAQ,4BAA4B,EAC1E,GAAI,IAAU,EAAU,MACxB,EAAY,CACd,CAEA,IAAM,EAAY,EAAW,IAAI,EACjC,GAAI,GAAa,EAAG,MACpB,MAAA,EAAM,EAAA,MAAA,CAAM,KAAK,IAAI,EAAU,CAAS,EAAG,CAAM,CACnD,CAEA,IAAM,EAAS,8BAA8B,EAAQ,IAC/C,EAAU,EAAU,GAAG,EAAQ,IAAI,EAAO,GAAK,EAC/C,EAAS,aAAqB,MAAQ,EAAU,QAAU,GAAa,KAAO,GAAK,OAAO,CAAS,EAEzG,MAAM,IAAI,kBAAkB,EAAS,GAAG,EAAQ,IAAI,IAAW,EAAS,CAAE,MAAO,CAAU,CAAC,CAC9F,CAGA,SAAgB,aACd,EACA,EACA,CAAE,SAAQ,UAAU,KAAwC,CAAC,EACjD,CAGZ,OAFA,iBAAiB,UAAW,EAAS,EAAI,EAElC,IAAI,SAAS,EAAS,IAAW,CACtC,GAAI,GAAQ,QAAS,CACnB,GAAA,EAAO,EAAA,WAAA,CAAW,EAAQ,4BAA4B,CAAC,EACvD,MACF,CAEA,IAAM,YAAgB,CACpB,aAAa,CAAK,EAClB,GAAQ,oBAAoB,QAAS,OAAO,EAC5C,EAAO,oBAAoB,EAAM,OAAO,CAC1C,EACM,QAAW,GAAuB,CACtC,QAAQ,EACR,EAAQ,CAAU,CACpB,EACM,EAAQ,eAAiB,CAC7B,QAAQ,EACR,EAAO,IAAI,kBAAkB,kBAAkB,EAAK,oBAAoB,EAAQ,GAAG,CAAC,CACtF,EAAG,CAAO,EACJ,YAAgB,CACpB,QAAQ,EACR,GAAA,EAAO,EAAA,WAAA,CAAW,EAAQ,4BAA4B,CAAC,CACzD,EAEA,EAAO,iBAAiB,EAAM,QAAS,CAAE,KAAM,EAAK,CAAC,EACrD,GAAQ,iBAAiB,QAAS,QAAS,CAAE,KAAM,EAAK,CAAC,CAC3D,CAAC,CACH"}
1
+ {"version":3,"file":"assay.cjs","names":[],"sources":["../src/errors.ts","../src/events.ts","../src/query.ts","../src/wait.ts"],"sourcesContent":["/** Base class for all Assay errors. Use `instanceof AssayError` to catch any Assay-originated error. */\nexport class AssayError extends Error {\n constructor(message: string, opts?: ErrorOptions) {\n super(message, opts);\n this.name = new.target.name;\n Object.setPrototypeOf(this, new.target.prototype);\n }\n}\n\n/** Thrown when a required query has no match. */\nexport class AssayQueryError extends AssayError {}\n\n/** Thrown when an Assay wait reaches its timeout. */\nexport class AssayTimeoutError extends AssayError {}\n","/**\n * # Confidence boundary: read first\n *\n * `dispatch` is a **low-level synthetic-event dispatch API**. It fires a caller-\n * supplied DOM `Event` synchronously and returns the `dispatchEvent`\n * result. It does **not** model real user interaction: there is no focus/blur\n * sequencing, no input-event coordination, no keyboard layout, no pointer\n * hit-testing, and no trust flag (`event.isTrusted` is always `false`).\n *\n * Use it to drive event handlers in unit/integration tests where you control\n * the exact event shape. For **behavioral confidence**, that a real click\n * actually activates a control, that focus management works end-to-end, that\n * keyboard shortcuts fire under a real input device: write Playwright tests.\n * Assay deliberately stops at the dispatch boundary; anything that depends on\n * the browser's interaction semantics belongs in a browser-driven suite.\n */\n\n/** Dispatch a pre-built `Event` instance unchanged. Returns the `dispatchEvent` result. */\nexport const dispatch = (target: EventTarget, event: Event): boolean => target.dispatchEvent(event);\n\n/** Synthetic convenience dispatchers. They share the same confidence boundary as `dispatch()`. */\nexport const fireBlur = (target: EventTarget, init: FocusEventInit = {}): boolean =>\n dispatch(target, new FocusEvent('blur', init));\n\nexport const fireChange = (target: EventTarget, init: EventInit = {}): boolean =>\n dispatch(target, new Event('change', { bubbles: true, ...init }));\n\nexport const fireClick = (target: EventTarget, init: MouseEventInit = {}): boolean =>\n dispatch(target, new MouseEvent('click', { bubbles: true, cancelable: true, ...init }));\n\nexport const fireCustom = <T>(target: EventTarget, type: string, init: CustomEventInit<T> = {}): boolean =>\n dispatch(target, new CustomEvent(type, { bubbles: true, cancelable: true, composed: false, ...init }));\n\nexport const fireFocus = (target: EventTarget, init: FocusEventInit = {}): boolean =>\n dispatch(target, new FocusEvent('focus', init));\n\nexport const fireInput = (target: EventTarget, init: InputEventInit = {}): boolean =>\n dispatch(target, new InputEvent('input', { bubbles: true, ...init }));\n\nexport const fireKeyDown = (target: EventTarget, init: KeyboardEventInit = {}): boolean =>\n dispatch(target, new KeyboardEvent('keydown', { bubbles: true, cancelable: true, ...init }));\n\nexport const fireKeyUp = (target: EventTarget, init: KeyboardEventInit = {}): boolean =>\n dispatch(target, new KeyboardEvent('keyup', { bubbles: true, cancelable: true, ...init }));\n\nexport const fireSubmit = (target: EventTarget, init: SubmitEventInit = {}): boolean =>\n dispatch(target, new SubmitEvent('submit', { bubbles: true, cancelable: true, ...init }));\n","import { AssayQueryError } from './errors';\n\n/** DOM query helpers for test environments. */\n\n// ─── Types ───────────────────────────────────────────────────────────────────\n\n/** Scoped query helpers for any queryable DOM root: see {@link within}. */\nexport interface QueryScope {\n get<E extends Element = Element>(selector: string): E;\n getByTestId<E extends Element = Element>(testId: string): E;\n getByText<E extends Element = Element>(text: string, selector?: string): E;\n query<E extends Element = Element>(selector: string): E | null;\n queryAll<E extends Element = Element>(selector: string): E[];\n queryAllByTestId<E extends Element = Element>(testId: string): E[];\n queryAllByText<E extends Element = Element>(text: string, selector?: string): E[];\n queryByTestId<E extends Element = Element>(testId: string): E | null;\n queryByText<E extends Element = Element>(text: string, selector?: string): E | null;\n}\n\n// ─── Helpers ─────────────────────────────────────────────────────────────────\n\ntype QueryRoot = ParentNode;\n\nconst describeRoot = (root: QueryRoot): string => {\n if (root instanceof Element) return root.outerHTML.slice(0, 500);\n\n if (root instanceof ShadowRoot) return `<shadow-root>${root.innerHTML.slice(0, 500)}</shadow-root>`;\n\n return root.nodeName;\n};\n\nconst requireMatch = <E extends Element>(element: E | null, description: string, root: QueryRoot): E => {\n if (element) return element;\n\n throw new AssayQueryError(`Unable to find ${description} within ${describeRoot(root)}.`);\n};\n\nconst query = <E extends Element = Element>(root: QueryRoot, selector: string): E | null =>\n root.querySelector<E>(selector);\n\nconst queryAll = <E extends Element = Element>(root: QueryRoot, selector: string): E[] => {\n return Array.from(root.querySelectorAll<E>(selector));\n};\n\nconst queryByTestId = <E extends Element = Element>(root: QueryRoot, testId: string): E | null =>\n queryAll<E>(root, '[data-testid]').find((element) => element.getAttribute('data-testid') === testId) ?? null;\n\nconst queryAllByTestId = <E extends Element = Element>(root: QueryRoot, testId: string): E[] =>\n queryAll<E>(root, '[data-testid]').filter((element) => element.getAttribute('data-testid') === testId);\n\nconst queryByText = <E extends Element = Element>(root: QueryRoot, text: string, selector = '*'): E | null => {\n for (const element of root.querySelectorAll<E>(selector)) {\n if (element.textContent?.trim() === text) return element;\n }\n\n return null;\n};\n\nconst queryAllByText = <E extends Element = Element>(root: QueryRoot, text: string, selector = '*'): E[] =>\n queryAll<E>(root, selector).filter((element) => element.textContent?.trim() === text);\n\n/**\n * Queries the shadow root of a custom element for a matching CSS selector.\n * Returns `null` if the element has no shadow root or no match is found: safe to call on\n * both open-shadow custom elements and plain elements without checking `shadowRoot` first.\n */\nexport function queryInShadow<E extends Element = Element>(host: Element, selector: string): E | null {\n return host.shadowRoot ? query<E>(host.shadowRoot, selector) : null;\n}\n\n/** Queries all matching elements inside the shadow root of a custom element. */\nexport function queryAllInShadow<E extends Element = Element>(host: Element, selector: string): E[] {\n return host.shadowRoot ? queryAll<E>(host.shadowRoot, selector) : [];\n}\n\n/**\n * Queries a shadow DOM element by its CSS `part` attribute: shorthand for\n * `queryInShadow(host, '[part=\"name\"]')`.\n *\n * @example\n * const btn = queryPart(carousel, 'prev-btn');\n * expect(btn).not.toBeNull();\n */\nexport function queryPart<E extends Element = Element>(host: Element, part: string): E | null {\n return (\n queryAllInShadow<E>(host, '[part]').find((element) => element.getAttribute('part')?.split(/\\s+/).includes(part)) ??\n null\n );\n}\n\n/**\n * Returns the light-DOM children assigned to a named slot, or every child assigned to the\n * default slot when no name is given.\n *\n * @example\n * const slides = getSlotted(carousel);\n * expect(slides).toHaveLength(3);\n */\nexport function getSlotted<E extends Element = Element>(host: Element, slotName?: string): E[] {\n return Array.from(host.children).filter((element) =>\n slotName === undefined ? !element.getAttribute('slot') : element.getAttribute('slot') === slotName,\n ) as E[];\n}\n\n// ─── API ─────────────────────────────────────────────────────────────────────\n\n/**\n * Create query helpers scoped to a DOM root: useful for light DOM and shadow roots.\n *\n * @example\n * const view = within(fixture.shadow!);\n * expect(view.get('.title').textContent).toBe('Hello');\n */\nexport function within(root: QueryRoot): QueryScope {\n return {\n get: <E extends Element = Element>(selector: string) =>\n requireMatch(query<E>(root, selector), `\"${selector}\"`, root),\n getByTestId: <E extends Element = Element>(testId: string) =>\n requireMatch(queryByTestId<E>(root, testId), `data-testid \"${testId}\"`, root),\n getByText: <E extends Element = Element>(text: string, selector = '*') =>\n requireMatch(queryByText<E>(root, text, selector), `text \"${text}\" matching \"${selector}\"`, root),\n query: <E extends Element = Element>(selector: string) => query<E>(root, selector),\n queryAll: <E extends Element = Element>(selector: string) => queryAll<E>(root, selector),\n queryAllByTestId: <E extends Element = Element>(testId: string) => queryAllByTestId<E>(root, testId),\n queryAllByText: <E extends Element = Element>(text: string, selector = '*') =>\n queryAllByText<E>(root, text, selector),\n queryByTestId: <E extends Element = Element>(testId: string) => queryByTestId<E>(root, testId),\n queryByText: <E extends Element = Element>(text: string, selector = '*') => queryByText<E>(root, text, selector),\n };\n}\n","import { abortError, sleep } from '@vielzeug/arsenal';\n\nimport { AssayTimeoutError } from './errors';\n\nexport interface DelayOptions {\n signal?: AbortSignal;\n}\n\nexport interface WaitOptions {\n /** Polling interval in ms (default: 50). */\n interval?: number;\n /** Cancel the pending wait. */\n signal?: AbortSignal;\n /** Maximum wait time in ms (default: 1000). */\n timeout?: number;\n}\n\nexport interface EventuallyOptions extends WaitOptions {\n /** Context included in the timeout error. */\n message?: string;\n}\n\nconst DEADLINE = Symbol('assay.deadline');\nconst MAX_TIMER_MS = 2_147_483_647;\nconst now = (): number => performance.now();\n\nconst validateDuration = (name: string, value: number, allowZero: boolean): void => {\n if (!Number.isFinite(value) || value > MAX_TIMER_MS || (allowZero ? value < 0 : value <= 0)) {\n throw new RangeError(\n `${name} must be a ${allowZero ? 'non-negative' : 'positive'} finite number no greater than ${MAX_TIMER_MS}`,\n );\n }\n};\n\nconst validateWait = (interval: number, timeout: number): void => {\n validateDuration('interval', interval, false);\n validateDuration('timeout', timeout, true);\n};\n\nconst runBounded = <T>(run: () => T | Promise<T>, deadline: number, signal?: AbortSignal): Promise<T> =>\n new Promise((resolve, reject) => {\n if (signal?.aborted) {\n reject(abortError(signal, 'The operation was aborted.'));\n return;\n }\n\n let settled = false;\n const cleanup = (): boolean => {\n if (settled) return false;\n settled = true;\n clearTimeout(timer);\n signal?.removeEventListener('abort', abort);\n return true;\n };\n const resolveOnce = (value: T): void => {\n if (cleanup()) resolve(value);\n };\n const rejectOnce = (error: unknown): void => {\n if (cleanup()) reject(error);\n };\n const timer = setTimeout(() => rejectOnce(DEADLINE), Math.max(0, deadline - now()));\n const abort = () => rejectOnce(abortError(signal, 'The operation was aborted.'));\n\n signal?.addEventListener('abort', abort, { once: true });\n\n try {\n Promise.resolve(run()).then(resolveOnce, rejectOnce);\n } catch (error) {\n rejectOnce(error);\n }\n });\n\nexport const delay = (ms = 0, options: DelayOptions = {}): Promise<void> => {\n validateDuration('ms', ms, true);\n return sleep(ms, options.signal);\n};\n\nexport async function waitUntil(\n predicate: () => boolean | Promise<boolean>,\n { interval = 50, signal, timeout = 1000 }: WaitOptions = {},\n): Promise<void> {\n validateWait(interval, timeout);\n const deadline = now() + timeout;\n let first = true;\n\n for (;;) {\n if (signal?.aborted) throw abortError(signal, 'The operation was aborted.');\n if (!first && now() >= deadline) throw new AssayTimeoutError(`waitUntil timed out after ${timeout}ms`);\n first = false;\n\n let matched: boolean;\n try {\n matched = await runBounded(predicate, deadline, signal);\n } catch (error) {\n if (error === DEADLINE) throw new AssayTimeoutError(`waitUntil timed out after ${timeout}ms`);\n throw error;\n }\n if (matched) return;\n\n const remaining = deadline - now();\n if (remaining <= 0) throw new AssayTimeoutError(`waitUntil timed out after ${timeout}ms`);\n await sleep(Math.min(interval, remaining), signal);\n }\n}\n\n/**\n * Retry an assertion until it stops throwing or the timeout elapses.\n *\n * Polls the assertion on a fixed interval and preserves the last failure as the\n * timeout cause. Pending asynchronous assertions remain bounded by the same\n * deadline. Use `waitUntil()` for boolean conditions, `delay()` for explicit\n * timer dependencies, and `nextTick()` for one queued microtask boundary.\n *\n * @example\n * await eventually(() => expect(region.textContent).toBe('Saved'));\n */\nexport async function eventually(\n assertion: () => void | Promise<void>,\n { interval = 50, message, signal, timeout = 1000 }: EventuallyOptions = {},\n): Promise<void> {\n validateWait(interval, timeout);\n const deadline = now() + timeout;\n let first = true;\n let lastError: unknown;\n\n for (;;) {\n if (signal?.aborted) throw abortError(signal, 'The operation was aborted.');\n if (!first && now() >= deadline) break;\n first = false;\n\n try {\n await runBounded(assertion, deadline, signal);\n return;\n } catch (error) {\n if (signal?.aborted) throw abortError(signal, 'The operation was aborted.');\n if (error === DEADLINE) break;\n lastError = error;\n }\n\n const remaining = deadline - now();\n if (remaining <= 0) break;\n await sleep(Math.min(interval, remaining), signal);\n }\n\n const timing = `eventually timed out after ${timeout}ms`;\n const context = message ? `${message} (${timing})` : timing;\n const detail = lastError instanceof Error ? lastError.message : lastError == null ? '' : String(lastError);\n\n throw new AssayTimeoutError(detail ? `${context}\\n${detail}` : context, { cause: lastError });\n}\n\n/** Resolve with the next event of `type`, or reject when it times out or is aborted. */\nexport function waitForEvent<T extends Event = Event>(\n target: EventTarget,\n type: string,\n { signal, timeout = 1000 }: Omit<WaitOptions, 'interval'> = {},\n): Promise<T> {\n validateDuration('timeout', timeout, true);\n\n return new Promise((resolve, reject) => {\n if (signal?.aborted) {\n reject(abortError(signal, 'The operation was aborted.'));\n return;\n }\n\n const dispose = () => {\n clearTimeout(timer);\n signal?.removeEventListener('abort', onAbort);\n target.removeEventListener(type, onEvent);\n };\n const onEvent = (event: Event): void => {\n dispose();\n resolve(event as T);\n };\n const timer = setTimeout(() => {\n dispose();\n reject(new AssayTimeoutError(`waitForEvent: \"${type}\" timed out after ${timeout}ms`));\n }, timeout);\n const onAbort = () => {\n dispose();\n reject(abortError(signal, 'The operation was aborted.'));\n };\n\n target.addEventListener(type, onEvent, { once: true });\n signal?.addEventListener('abort', onAbort, { once: true });\n });\n}\n"],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"assay.iife.js","names":[],"sources":["../src/errors.ts","../src/events.ts","../src/query.ts","../src/wait.ts"],"sourcesContent":["/** Base class for all Assay errors. Use `instanceof AssayError` to catch any Assay-originated error. */\nexport class AssayError extends Error {\n constructor(message: string, opts?: ErrorOptions) {\n super(message, opts);\n this.name = new.target.name;\n Object.setPrototypeOf(this, new.target.prototype);\n }\n}\n\n/** Thrown when a required query has no match. */\nexport class AssayQueryError extends AssayError {}\n\n/** Thrown when an Assay wait reaches its timeout. */\nexport class AssayTimeoutError extends AssayError {}\n","/**\n * # Confidence boundary — read first\n *\n * `dispatch` is a **low-level synthetic-event dispatch API**. It fires a caller-\n * supplied DOM `Event` synchronously and returns the `dispatchEvent`\n * result. It does **not** model real user interaction: there is no focus/blur\n * sequencing, no input-event coordination, no keyboard layout, no pointer\n * hit-testing, and no trust flag (`event.isTrusted` is always `false`).\n *\n * Use it to drive event handlers in unit/integration tests where you control\n * the exact event shape. For **behavioral confidence** — that a real click\n * actually activates a control, that focus management works end-to-end, that\n * keyboard shortcuts fire under a real input device — write Playwright tests.\n * Assay deliberately stops at the dispatch boundary; anything that depends on\n * the browser's interaction semantics belongs in a browser-driven suite.\n */\n\n/** Dispatch a pre-built `Event` instance unchanged. Returns the `dispatchEvent` result. */\nexport const dispatch = (target: EventTarget, event: Event): boolean => target.dispatchEvent(event);\n\n/** Synthetic convenience dispatchers. They share the same confidence boundary as `dispatch()`. */\nexport const fireBlur = (target: EventTarget, init: FocusEventInit = {}): boolean =>\n dispatch(target, new FocusEvent('blur', init));\n\nexport const fireChange = (target: EventTarget, init: EventInit = {}): boolean =>\n dispatch(target, new Event('change', { bubbles: true, ...init }));\n\nexport const fireClick = (target: EventTarget, init: MouseEventInit = {}): boolean =>\n dispatch(target, new MouseEvent('click', { bubbles: true, cancelable: true, ...init }));\n\nexport const fireCustom = <T>(target: EventTarget, type: string, init: CustomEventInit<T> = {}): boolean =>\n dispatch(target, new CustomEvent(type, { bubbles: true, cancelable: true, composed: false, ...init }));\n\nexport const fireFocus = (target: EventTarget, init: FocusEventInit = {}): boolean =>\n dispatch(target, new FocusEvent('focus', init));\n\nexport const fireInput = (target: EventTarget, init: InputEventInit = {}): boolean =>\n dispatch(target, new InputEvent('input', { bubbles: true, ...init }));\n\nexport const fireKeyDown = (target: EventTarget, init: KeyboardEventInit = {}): boolean =>\n dispatch(target, new KeyboardEvent('keydown', { bubbles: true, cancelable: true, ...init }));\n\nexport const fireKeyUp = (target: EventTarget, init: KeyboardEventInit = {}): boolean =>\n dispatch(target, new KeyboardEvent('keyup', { bubbles: true, cancelable: true, ...init }));\n\nexport const fireSubmit = (target: EventTarget, init: SubmitEventInit = {}): boolean =>\n dispatch(target, new SubmitEvent('submit', { bubbles: true, cancelable: true, ...init }));\n","import { AssayQueryError } from './errors';\n\n/** DOM query helpers for test environments. */\n\n// ─── Types ───────────────────────────────────────────────────────────────────\n\n/** Scoped query helpers for any queryable DOM root — see {@link within}. */\nexport interface QueryScope {\n get<E extends Element = Element>(selector: string): E;\n getByTestId<E extends Element = Element>(testId: string): E;\n getByText<E extends Element = Element>(text: string, selector?: string): E;\n query<E extends Element = Element>(selector: string): E | null;\n queryAll<E extends Element = Element>(selector: string): E[];\n queryAllByTestId<E extends Element = Element>(testId: string): E[];\n queryAllByText<E extends Element = Element>(text: string, selector?: string): E[];\n queryByTestId<E extends Element = Element>(testId: string): E | null;\n queryByText<E extends Element = Element>(text: string, selector?: string): E | null;\n}\n\n// ─── Helpers ─────────────────────────────────────────────────────────────────\n\ntype QueryRoot = ParentNode;\n\nconst describeRoot = (root: QueryRoot): string => {\n if (root instanceof Element) return root.outerHTML.slice(0, 500);\n\n if (root instanceof ShadowRoot) return `<shadow-root>${root.innerHTML.slice(0, 500)}</shadow-root>`;\n\n return root.nodeName;\n};\n\nconst requireMatch = <E extends Element>(element: E | null, description: string, root: QueryRoot): E => {\n if (element) return element;\n\n throw new AssayQueryError(`Unable to find ${description} within ${describeRoot(root)}.`);\n};\n\nconst query = <E extends Element = Element>(root: QueryRoot, selector: string): E | null =>\n root.querySelector<E>(selector);\n\nconst queryAll = <E extends Element = Element>(root: QueryRoot, selector: string): E[] => {\n return Array.from(root.querySelectorAll<E>(selector));\n};\n\nconst queryByTestId = <E extends Element = Element>(root: QueryRoot, testId: string): E | null =>\n queryAll<E>(root, '[data-testid]').find((element) => element.getAttribute('data-testid') === testId) ?? null;\n\nconst queryAllByTestId = <E extends Element = Element>(root: QueryRoot, testId: string): E[] =>\n queryAll<E>(root, '[data-testid]').filter((element) => element.getAttribute('data-testid') === testId);\n\nconst queryByText = <E extends Element = Element>(root: QueryRoot, text: string, selector = '*'): E | null => {\n for (const element of root.querySelectorAll<E>(selector)) {\n if (element.textContent?.trim() === text) return element;\n }\n\n return null;\n};\n\nconst queryAllByText = <E extends Element = Element>(root: QueryRoot, text: string, selector = '*'): E[] =>\n queryAll<E>(root, selector).filter((element) => element.textContent?.trim() === text);\n\n/**\n * Queries the shadow root of a custom element for a matching CSS selector.\n * Returns `null` if the element has no shadow root or no match is found — safe to call on\n * both open-shadow custom elements and plain elements without checking `shadowRoot` first.\n */\nexport function queryInShadow<E extends Element = Element>(host: Element, selector: string): E | null {\n return host.shadowRoot ? query<E>(host.shadowRoot, selector) : null;\n}\n\n/** Queries all matching elements inside the shadow root of a custom element. */\nexport function queryAllInShadow<E extends Element = Element>(host: Element, selector: string): E[] {\n return host.shadowRoot ? queryAll<E>(host.shadowRoot, selector) : [];\n}\n\n/**\n * Queries a shadow DOM element by its CSS `part` attribute — shorthand for\n * `queryInShadow(host, '[part=\"name\"]')`.\n *\n * @example\n * const btn = queryPart(carousel, 'prev-btn');\n * expect(btn).not.toBeNull();\n */\nexport function queryPart<E extends Element = Element>(host: Element, part: string): E | null {\n return (\n queryAllInShadow<E>(host, '[part]').find((element) => element.getAttribute('part')?.split(/\\s+/).includes(part)) ??\n null\n );\n}\n\n/**\n * Returns the light-DOM children assigned to a named slot, or every child assigned to the\n * default slot when no name is given.\n *\n * @example\n * const slides = getSlotted(carousel);\n * expect(slides).toHaveLength(3);\n */\nexport function getSlotted<E extends Element = Element>(host: Element, slotName?: string): E[] {\n return Array.from(host.children).filter((element) =>\n slotName === undefined ? !element.getAttribute('slot') : element.getAttribute('slot') === slotName,\n ) as E[];\n}\n\n// ─── API ─────────────────────────────────────────────────────────────────────\n\n/**\n * Create query helpers scoped to a DOM root — useful for light DOM and shadow roots.\n *\n * @example\n * const view = within(fixture.shadow!);\n * expect(view.get('.title').textContent).toBe('Hello');\n */\nexport function within(root: QueryRoot): QueryScope {\n return {\n get: <E extends Element = Element>(selector: string) =>\n requireMatch(query<E>(root, selector), `\"${selector}\"`, root),\n getByTestId: <E extends Element = Element>(testId: string) =>\n requireMatch(queryByTestId<E>(root, testId), `data-testid \"${testId}\"`, root),\n getByText: <E extends Element = Element>(text: string, selector = '*') =>\n requireMatch(queryByText<E>(root, text, selector), `text \"${text}\" matching \"${selector}\"`, root),\n query: <E extends Element = Element>(selector: string) => query<E>(root, selector),\n queryAll: <E extends Element = Element>(selector: string) => queryAll<E>(root, selector),\n queryAllByTestId: <E extends Element = Element>(testId: string) => queryAllByTestId<E>(root, testId),\n queryAllByText: <E extends Element = Element>(text: string, selector = '*') =>\n queryAllByText<E>(root, text, selector),\n queryByTestId: <E extends Element = Element>(testId: string) => queryByTestId<E>(root, testId),\n queryByText: <E extends Element = Element>(text: string, selector = '*') => queryByText<E>(root, text, selector),\n };\n}\n","import { abortError, sleep } from '@vielzeug/arsenal';\n\nimport { AssayTimeoutError } from './errors';\n\nexport interface DelayOptions {\n signal?: AbortSignal;\n}\n\nexport interface WaitOptions {\n /** Polling interval in ms (default: 50). */\n interval?: number;\n /** Cancel the pending wait. */\n signal?: AbortSignal;\n /** Maximum wait time in ms (default: 1000). */\n timeout?: number;\n}\n\nexport interface EventuallyOptions extends WaitOptions {\n /** Context included in the timeout error. */\n message?: string;\n}\n\nconst DEADLINE = Symbol('assay.deadline');\nconst MAX_TIMER_MS = 2_147_483_647;\nconst now = (): number => performance.now();\n\nconst validateDuration = (name: string, value: number, allowZero: boolean): void => {\n if (!Number.isFinite(value) || value > MAX_TIMER_MS || (allowZero ? value < 0 : value <= 0)) {\n throw new RangeError(\n `${name} must be a ${allowZero ? 'non-negative' : 'positive'} finite number no greater than ${MAX_TIMER_MS}`,\n );\n }\n};\n\nconst validateWait = (interval: number, timeout: number): void => {\n validateDuration('interval', interval, false);\n validateDuration('timeout', timeout, true);\n};\n\nconst runBounded = <T>(run: () => T | Promise<T>, deadline: number, signal?: AbortSignal): Promise<T> =>\n new Promise((resolve, reject) => {\n if (signal?.aborted) {\n reject(abortError(signal, 'The operation was aborted.'));\n return;\n }\n\n let settled = false;\n const cleanup = (): boolean => {\n if (settled) return false;\n settled = true;\n clearTimeout(timer);\n signal?.removeEventListener('abort', abort);\n return true;\n };\n const resolveOnce = (value: T): void => {\n if (cleanup()) resolve(value);\n };\n const rejectOnce = (error: unknown): void => {\n if (cleanup()) reject(error);\n };\n const timer = setTimeout(() => rejectOnce(DEADLINE), Math.max(0, deadline - now()));\n const abort = () => rejectOnce(abortError(signal, 'The operation was aborted.'));\n\n signal?.addEventListener('abort', abort, { once: true });\n\n try {\n Promise.resolve(run()).then(resolveOnce, rejectOnce);\n } catch (error) {\n rejectOnce(error);\n }\n });\n\nexport const delay = (ms = 0, options: DelayOptions = {}): Promise<void> => {\n validateDuration('ms', ms, true);\n return sleep(ms, options.signal);\n};\n\nexport async function waitUntil(\n predicate: () => boolean | Promise<boolean>,\n { interval = 50, signal, timeout = 1000 }: WaitOptions = {},\n): Promise<void> {\n validateWait(interval, timeout);\n const deadline = now() + timeout;\n let first = true;\n\n for (;;) {\n if (signal?.aborted) throw abortError(signal, 'The operation was aborted.');\n if (!first && now() >= deadline) throw new AssayTimeoutError(`waitUntil timed out after ${timeout}ms`);\n first = false;\n\n let matched: boolean;\n try {\n matched = await runBounded(predicate, deadline, signal);\n } catch (error) {\n if (error === DEADLINE) throw new AssayTimeoutError(`waitUntil timed out after ${timeout}ms`);\n throw error;\n }\n if (matched) return;\n\n const remaining = deadline - now();\n if (remaining <= 0) throw new AssayTimeoutError(`waitUntil timed out after ${timeout}ms`);\n await sleep(Math.min(interval, remaining), signal);\n }\n}\n\n/**\n * Retry an assertion until it stops throwing or the timeout elapses.\n *\n * Polls the assertion on a fixed interval and preserves the last failure as the\n * timeout cause. Pending asynchronous assertions remain bounded by the same\n * deadline. Use `waitUntil()` for boolean conditions, `delay()` for explicit\n * timer dependencies, and `nextTick()` for one queued microtask boundary.\n *\n * @example\n * await eventually(() => expect(region.textContent).toBe('Saved'));\n */\nexport async function eventually(\n assertion: () => void | Promise<void>,\n { interval = 50, message, signal, timeout = 1000 }: EventuallyOptions = {},\n): Promise<void> {\n validateWait(interval, timeout);\n const deadline = now() + timeout;\n let first = true;\n let lastError: unknown;\n\n for (;;) {\n if (signal?.aborted) throw abortError(signal, 'The operation was aborted.');\n if (!first && now() >= deadline) break;\n first = false;\n\n try {\n await runBounded(assertion, deadline, signal);\n return;\n } catch (error) {\n if (signal?.aborted) throw abortError(signal, 'The operation was aborted.');\n if (error === DEADLINE) break;\n lastError = error;\n }\n\n const remaining = deadline - now();\n if (remaining <= 0) break;\n await sleep(Math.min(interval, remaining), signal);\n }\n\n const timing = `eventually timed out after ${timeout}ms`;\n const context = message ? `${message} (${timing})` : timing;\n const detail = lastError instanceof Error ? lastError.message : lastError == null ? '' : String(lastError);\n\n throw new AssayTimeoutError(detail ? `${context}\\n${detail}` : context, { cause: lastError });\n}\n\n/** Resolve with the next event of `type`, or reject when it times out or is aborted. */\nexport function waitForEvent<T extends Event = Event>(\n target: EventTarget,\n type: string,\n { signal, timeout = 1000 }: Omit<WaitOptions, 'interval'> = {},\n): Promise<T> {\n validateDuration('timeout', timeout, true);\n\n return new Promise((resolve, reject) => {\n if (signal?.aborted) {\n reject(abortError(signal, 'The operation was aborted.'));\n return;\n }\n\n const dispose = () => {\n clearTimeout(timer);\n signal?.removeEventListener('abort', onAbort);\n target.removeEventListener(type, onEvent);\n };\n const onEvent = (event: Event): void => {\n dispose();\n resolve(event as T);\n };\n const timer = setTimeout(() => {\n dispose();\n reject(new AssayTimeoutError(`waitForEvent: \"${type}\" timed out after ${timeout}ms`));\n }, timeout);\n const onAbort = () => {\n dispose();\n reject(abortError(signal, 'The operation was aborted.'));\n };\n\n target.addEventListener(type, onEvent, { once: true });\n signal?.addEventListener('abort', onAbort, { once: true });\n });\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"assay.iife.js","names":[],"sources":["../src/errors.ts","../src/events.ts","../src/query.ts","../src/wait.ts"],"sourcesContent":["/** Base class for all Assay errors. Use `instanceof AssayError` to catch any Assay-originated error. */\nexport class AssayError extends Error {\n constructor(message: string, opts?: ErrorOptions) {\n super(message, opts);\n this.name = new.target.name;\n Object.setPrototypeOf(this, new.target.prototype);\n }\n}\n\n/** Thrown when a required query has no match. */\nexport class AssayQueryError extends AssayError {}\n\n/** Thrown when an Assay wait reaches its timeout. */\nexport class AssayTimeoutError extends AssayError {}\n","/**\n * # Confidence boundary: read first\n *\n * `dispatch` is a **low-level synthetic-event dispatch API**. It fires a caller-\n * supplied DOM `Event` synchronously and returns the `dispatchEvent`\n * result. It does **not** model real user interaction: there is no focus/blur\n * sequencing, no input-event coordination, no keyboard layout, no pointer\n * hit-testing, and no trust flag (`event.isTrusted` is always `false`).\n *\n * Use it to drive event handlers in unit/integration tests where you control\n * the exact event shape. For **behavioral confidence**, that a real click\n * actually activates a control, that focus management works end-to-end, that\n * keyboard shortcuts fire under a real input device: write Playwright tests.\n * Assay deliberately stops at the dispatch boundary; anything that depends on\n * the browser's interaction semantics belongs in a browser-driven suite.\n */\n\n/** Dispatch a pre-built `Event` instance unchanged. Returns the `dispatchEvent` result. */\nexport const dispatch = (target: EventTarget, event: Event): boolean => target.dispatchEvent(event);\n\n/** Synthetic convenience dispatchers. They share the same confidence boundary as `dispatch()`. */\nexport const fireBlur = (target: EventTarget, init: FocusEventInit = {}): boolean =>\n dispatch(target, new FocusEvent('blur', init));\n\nexport const fireChange = (target: EventTarget, init: EventInit = {}): boolean =>\n dispatch(target, new Event('change', { bubbles: true, ...init }));\n\nexport const fireClick = (target: EventTarget, init: MouseEventInit = {}): boolean =>\n dispatch(target, new MouseEvent('click', { bubbles: true, cancelable: true, ...init }));\n\nexport const fireCustom = <T>(target: EventTarget, type: string, init: CustomEventInit<T> = {}): boolean =>\n dispatch(target, new CustomEvent(type, { bubbles: true, cancelable: true, composed: false, ...init }));\n\nexport const fireFocus = (target: EventTarget, init: FocusEventInit = {}): boolean =>\n dispatch(target, new FocusEvent('focus', init));\n\nexport const fireInput = (target: EventTarget, init: InputEventInit = {}): boolean =>\n dispatch(target, new InputEvent('input', { bubbles: true, ...init }));\n\nexport const fireKeyDown = (target: EventTarget, init: KeyboardEventInit = {}): boolean =>\n dispatch(target, new KeyboardEvent('keydown', { bubbles: true, cancelable: true, ...init }));\n\nexport const fireKeyUp = (target: EventTarget, init: KeyboardEventInit = {}): boolean =>\n dispatch(target, new KeyboardEvent('keyup', { bubbles: true, cancelable: true, ...init }));\n\nexport const fireSubmit = (target: EventTarget, init: SubmitEventInit = {}): boolean =>\n dispatch(target, new SubmitEvent('submit', { bubbles: true, cancelable: true, ...init }));\n","import { AssayQueryError } from './errors';\n\n/** DOM query helpers for test environments. */\n\n// ─── Types ───────────────────────────────────────────────────────────────────\n\n/** Scoped query helpers for any queryable DOM root: see {@link within}. */\nexport interface QueryScope {\n get<E extends Element = Element>(selector: string): E;\n getByTestId<E extends Element = Element>(testId: string): E;\n getByText<E extends Element = Element>(text: string, selector?: string): E;\n query<E extends Element = Element>(selector: string): E | null;\n queryAll<E extends Element = Element>(selector: string): E[];\n queryAllByTestId<E extends Element = Element>(testId: string): E[];\n queryAllByText<E extends Element = Element>(text: string, selector?: string): E[];\n queryByTestId<E extends Element = Element>(testId: string): E | null;\n queryByText<E extends Element = Element>(text: string, selector?: string): E | null;\n}\n\n// ─── Helpers ─────────────────────────────────────────────────────────────────\n\ntype QueryRoot = ParentNode;\n\nconst describeRoot = (root: QueryRoot): string => {\n if (root instanceof Element) return root.outerHTML.slice(0, 500);\n\n if (root instanceof ShadowRoot) return `<shadow-root>${root.innerHTML.slice(0, 500)}</shadow-root>`;\n\n return root.nodeName;\n};\n\nconst requireMatch = <E extends Element>(element: E | null, description: string, root: QueryRoot): E => {\n if (element) return element;\n\n throw new AssayQueryError(`Unable to find ${description} within ${describeRoot(root)}.`);\n};\n\nconst query = <E extends Element = Element>(root: QueryRoot, selector: string): E | null =>\n root.querySelector<E>(selector);\n\nconst queryAll = <E extends Element = Element>(root: QueryRoot, selector: string): E[] => {\n return Array.from(root.querySelectorAll<E>(selector));\n};\n\nconst queryByTestId = <E extends Element = Element>(root: QueryRoot, testId: string): E | null =>\n queryAll<E>(root, '[data-testid]').find((element) => element.getAttribute('data-testid') === testId) ?? null;\n\nconst queryAllByTestId = <E extends Element = Element>(root: QueryRoot, testId: string): E[] =>\n queryAll<E>(root, '[data-testid]').filter((element) => element.getAttribute('data-testid') === testId);\n\nconst queryByText = <E extends Element = Element>(root: QueryRoot, text: string, selector = '*'): E | null => {\n for (const element of root.querySelectorAll<E>(selector)) {\n if (element.textContent?.trim() === text) return element;\n }\n\n return null;\n};\n\nconst queryAllByText = <E extends Element = Element>(root: QueryRoot, text: string, selector = '*'): E[] =>\n queryAll<E>(root, selector).filter((element) => element.textContent?.trim() === text);\n\n/**\n * Queries the shadow root of a custom element for a matching CSS selector.\n * Returns `null` if the element has no shadow root or no match is found: safe to call on\n * both open-shadow custom elements and plain elements without checking `shadowRoot` first.\n */\nexport function queryInShadow<E extends Element = Element>(host: Element, selector: string): E | null {\n return host.shadowRoot ? query<E>(host.shadowRoot, selector) : null;\n}\n\n/** Queries all matching elements inside the shadow root of a custom element. */\nexport function queryAllInShadow<E extends Element = Element>(host: Element, selector: string): E[] {\n return host.shadowRoot ? queryAll<E>(host.shadowRoot, selector) : [];\n}\n\n/**\n * Queries a shadow DOM element by its CSS `part` attribute: shorthand for\n * `queryInShadow(host, '[part=\"name\"]')`.\n *\n * @example\n * const btn = queryPart(carousel, 'prev-btn');\n * expect(btn).not.toBeNull();\n */\nexport function queryPart<E extends Element = Element>(host: Element, part: string): E | null {\n return (\n queryAllInShadow<E>(host, '[part]').find((element) => element.getAttribute('part')?.split(/\\s+/).includes(part)) ??\n null\n );\n}\n\n/**\n * Returns the light-DOM children assigned to a named slot, or every child assigned to the\n * default slot when no name is given.\n *\n * @example\n * const slides = getSlotted(carousel);\n * expect(slides).toHaveLength(3);\n */\nexport function getSlotted<E extends Element = Element>(host: Element, slotName?: string): E[] {\n return Array.from(host.children).filter((element) =>\n slotName === undefined ? !element.getAttribute('slot') : element.getAttribute('slot') === slotName,\n ) as E[];\n}\n\n// ─── API ─────────────────────────────────────────────────────────────────────\n\n/**\n * Create query helpers scoped to a DOM root: useful for light DOM and shadow roots.\n *\n * @example\n * const view = within(fixture.shadow!);\n * expect(view.get('.title').textContent).toBe('Hello');\n */\nexport function within(root: QueryRoot): QueryScope {\n return {\n get: <E extends Element = Element>(selector: string) =>\n requireMatch(query<E>(root, selector), `\"${selector}\"`, root),\n getByTestId: <E extends Element = Element>(testId: string) =>\n requireMatch(queryByTestId<E>(root, testId), `data-testid \"${testId}\"`, root),\n getByText: <E extends Element = Element>(text: string, selector = '*') =>\n requireMatch(queryByText<E>(root, text, selector), `text \"${text}\" matching \"${selector}\"`, root),\n query: <E extends Element = Element>(selector: string) => query<E>(root, selector),\n queryAll: <E extends Element = Element>(selector: string) => queryAll<E>(root, selector),\n queryAllByTestId: <E extends Element = Element>(testId: string) => queryAllByTestId<E>(root, testId),\n queryAllByText: <E extends Element = Element>(text: string, selector = '*') =>\n queryAllByText<E>(root, text, selector),\n queryByTestId: <E extends Element = Element>(testId: string) => queryByTestId<E>(root, testId),\n queryByText: <E extends Element = Element>(text: string, selector = '*') => queryByText<E>(root, text, selector),\n };\n}\n","import { abortError, sleep } from '@vielzeug/arsenal';\n\nimport { AssayTimeoutError } from './errors';\n\nexport interface DelayOptions {\n signal?: AbortSignal;\n}\n\nexport interface WaitOptions {\n /** Polling interval in ms (default: 50). */\n interval?: number;\n /** Cancel the pending wait. */\n signal?: AbortSignal;\n /** Maximum wait time in ms (default: 1000). */\n timeout?: number;\n}\n\nexport interface EventuallyOptions extends WaitOptions {\n /** Context included in the timeout error. */\n message?: string;\n}\n\nconst DEADLINE = Symbol('assay.deadline');\nconst MAX_TIMER_MS = 2_147_483_647;\nconst now = (): number => performance.now();\n\nconst validateDuration = (name: string, value: number, allowZero: boolean): void => {\n if (!Number.isFinite(value) || value > MAX_TIMER_MS || (allowZero ? value < 0 : value <= 0)) {\n throw new RangeError(\n `${name} must be a ${allowZero ? 'non-negative' : 'positive'} finite number no greater than ${MAX_TIMER_MS}`,\n );\n }\n};\n\nconst validateWait = (interval: number, timeout: number): void => {\n validateDuration('interval', interval, false);\n validateDuration('timeout', timeout, true);\n};\n\nconst runBounded = <T>(run: () => T | Promise<T>, deadline: number, signal?: AbortSignal): Promise<T> =>\n new Promise((resolve, reject) => {\n if (signal?.aborted) {\n reject(abortError(signal, 'The operation was aborted.'));\n return;\n }\n\n let settled = false;\n const cleanup = (): boolean => {\n if (settled) return false;\n settled = true;\n clearTimeout(timer);\n signal?.removeEventListener('abort', abort);\n return true;\n };\n const resolveOnce = (value: T): void => {\n if (cleanup()) resolve(value);\n };\n const rejectOnce = (error: unknown): void => {\n if (cleanup()) reject(error);\n };\n const timer = setTimeout(() => rejectOnce(DEADLINE), Math.max(0, deadline - now()));\n const abort = () => rejectOnce(abortError(signal, 'The operation was aborted.'));\n\n signal?.addEventListener('abort', abort, { once: true });\n\n try {\n Promise.resolve(run()).then(resolveOnce, rejectOnce);\n } catch (error) {\n rejectOnce(error);\n }\n });\n\nexport const delay = (ms = 0, options: DelayOptions = {}): Promise<void> => {\n validateDuration('ms', ms, true);\n return sleep(ms, options.signal);\n};\n\nexport async function waitUntil(\n predicate: () => boolean | Promise<boolean>,\n { interval = 50, signal, timeout = 1000 }: WaitOptions = {},\n): Promise<void> {\n validateWait(interval, timeout);\n const deadline = now() + timeout;\n let first = true;\n\n for (;;) {\n if (signal?.aborted) throw abortError(signal, 'The operation was aborted.');\n if (!first && now() >= deadline) throw new AssayTimeoutError(`waitUntil timed out after ${timeout}ms`);\n first = false;\n\n let matched: boolean;\n try {\n matched = await runBounded(predicate, deadline, signal);\n } catch (error) {\n if (error === DEADLINE) throw new AssayTimeoutError(`waitUntil timed out after ${timeout}ms`);\n throw error;\n }\n if (matched) return;\n\n const remaining = deadline - now();\n if (remaining <= 0) throw new AssayTimeoutError(`waitUntil timed out after ${timeout}ms`);\n await sleep(Math.min(interval, remaining), signal);\n }\n}\n\n/**\n * Retry an assertion until it stops throwing or the timeout elapses.\n *\n * Polls the assertion on a fixed interval and preserves the last failure as the\n * timeout cause. Pending asynchronous assertions remain bounded by the same\n * deadline. Use `waitUntil()` for boolean conditions, `delay()` for explicit\n * timer dependencies, and `nextTick()` for one queued microtask boundary.\n *\n * @example\n * await eventually(() => expect(region.textContent).toBe('Saved'));\n */\nexport async function eventually(\n assertion: () => void | Promise<void>,\n { interval = 50, message, signal, timeout = 1000 }: EventuallyOptions = {},\n): Promise<void> {\n validateWait(interval, timeout);\n const deadline = now() + timeout;\n let first = true;\n let lastError: unknown;\n\n for (;;) {\n if (signal?.aborted) throw abortError(signal, 'The operation was aborted.');\n if (!first && now() >= deadline) break;\n first = false;\n\n try {\n await runBounded(assertion, deadline, signal);\n return;\n } catch (error) {\n if (signal?.aborted) throw abortError(signal, 'The operation was aborted.');\n if (error === DEADLINE) break;\n lastError = error;\n }\n\n const remaining = deadline - now();\n if (remaining <= 0) break;\n await sleep(Math.min(interval, remaining), signal);\n }\n\n const timing = `eventually timed out after ${timeout}ms`;\n const context = message ? `${message} (${timing})` : timing;\n const detail = lastError instanceof Error ? lastError.message : lastError == null ? '' : String(lastError);\n\n throw new AssayTimeoutError(detail ? `${context}\\n${detail}` : context, { cause: lastError });\n}\n\n/** Resolve with the next event of `type`, or reject when it times out or is aborted. */\nexport function waitForEvent<T extends Event = Event>(\n target: EventTarget,\n type: string,\n { signal, timeout = 1000 }: Omit<WaitOptions, 'interval'> = {},\n): Promise<T> {\n validateDuration('timeout', timeout, true);\n\n return new Promise((resolve, reject) => {\n if (signal?.aborted) {\n reject(abortError(signal, 'The operation was aborted.'));\n return;\n }\n\n const dispose = () => {\n clearTimeout(timer);\n signal?.removeEventListener('abort', onAbort);\n target.removeEventListener(type, onEvent);\n };\n const onEvent = (event: Event): void => {\n dispose();\n resolve(event as T);\n };\n const timer = setTimeout(() => {\n dispose();\n reject(new AssayTimeoutError(`waitForEvent: \"${type}\" timed out after ${timeout}ms`));\n }, timeout);\n const onAbort = () => {\n dispose();\n reject(abortError(signal, 'The operation was aborted.'));\n };\n\n target.addEventListener(type, onEvent, { once: true });\n signal?.addEventListener('abort', onAbort, { once: true });\n });\n}\n"],"mappings":"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"}
package/dist/assay.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"assay.js","names":[],"sources":["../src/errors.ts","../src/events.ts","../src/query.ts","../src/wait.ts"],"sourcesContent":["/** Base class for all Assay errors. Use `instanceof AssayError` to catch any Assay-originated error. */\nexport class AssayError extends Error {\n constructor(message: string, opts?: ErrorOptions) {\n super(message, opts);\n this.name = new.target.name;\n Object.setPrototypeOf(this, new.target.prototype);\n }\n}\n\n/** Thrown when a required query has no match. */\nexport class AssayQueryError extends AssayError {}\n\n/** Thrown when an Assay wait reaches its timeout. */\nexport class AssayTimeoutError extends AssayError {}\n","/**\n * # Confidence boundary — read first\n *\n * `dispatch` is a **low-level synthetic-event dispatch API**. It fires a caller-\n * supplied DOM `Event` synchronously and returns the `dispatchEvent`\n * result. It does **not** model real user interaction: there is no focus/blur\n * sequencing, no input-event coordination, no keyboard layout, no pointer\n * hit-testing, and no trust flag (`event.isTrusted` is always `false`).\n *\n * Use it to drive event handlers in unit/integration tests where you control\n * the exact event shape. For **behavioral confidence** — that a real click\n * actually activates a control, that focus management works end-to-end, that\n * keyboard shortcuts fire under a real input device — write Playwright tests.\n * Assay deliberately stops at the dispatch boundary; anything that depends on\n * the browser's interaction semantics belongs in a browser-driven suite.\n */\n\n/** Dispatch a pre-built `Event` instance unchanged. Returns the `dispatchEvent` result. */\nexport const dispatch = (target: EventTarget, event: Event): boolean => target.dispatchEvent(event);\n\n/** Synthetic convenience dispatchers. They share the same confidence boundary as `dispatch()`. */\nexport const fireBlur = (target: EventTarget, init: FocusEventInit = {}): boolean =>\n dispatch(target, new FocusEvent('blur', init));\n\nexport const fireChange = (target: EventTarget, init: EventInit = {}): boolean =>\n dispatch(target, new Event('change', { bubbles: true, ...init }));\n\nexport const fireClick = (target: EventTarget, init: MouseEventInit = {}): boolean =>\n dispatch(target, new MouseEvent('click', { bubbles: true, cancelable: true, ...init }));\n\nexport const fireCustom = <T>(target: EventTarget, type: string, init: CustomEventInit<T> = {}): boolean =>\n dispatch(target, new CustomEvent(type, { bubbles: true, cancelable: true, composed: false, ...init }));\n\nexport const fireFocus = (target: EventTarget, init: FocusEventInit = {}): boolean =>\n dispatch(target, new FocusEvent('focus', init));\n\nexport const fireInput = (target: EventTarget, init: InputEventInit = {}): boolean =>\n dispatch(target, new InputEvent('input', { bubbles: true, ...init }));\n\nexport const fireKeyDown = (target: EventTarget, init: KeyboardEventInit = {}): boolean =>\n dispatch(target, new KeyboardEvent('keydown', { bubbles: true, cancelable: true, ...init }));\n\nexport const fireKeyUp = (target: EventTarget, init: KeyboardEventInit = {}): boolean =>\n dispatch(target, new KeyboardEvent('keyup', { bubbles: true, cancelable: true, ...init }));\n\nexport const fireSubmit = (target: EventTarget, init: SubmitEventInit = {}): boolean =>\n dispatch(target, new SubmitEvent('submit', { bubbles: true, cancelable: true, ...init }));\n","import { AssayQueryError } from './errors';\n\n/** DOM query helpers for test environments. */\n\n// ─── Types ───────────────────────────────────────────────────────────────────\n\n/** Scoped query helpers for any queryable DOM root — see {@link within}. */\nexport interface QueryScope {\n get<E extends Element = Element>(selector: string): E;\n getByTestId<E extends Element = Element>(testId: string): E;\n getByText<E extends Element = Element>(text: string, selector?: string): E;\n query<E extends Element = Element>(selector: string): E | null;\n queryAll<E extends Element = Element>(selector: string): E[];\n queryAllByTestId<E extends Element = Element>(testId: string): E[];\n queryAllByText<E extends Element = Element>(text: string, selector?: string): E[];\n queryByTestId<E extends Element = Element>(testId: string): E | null;\n queryByText<E extends Element = Element>(text: string, selector?: string): E | null;\n}\n\n// ─── Helpers ─────────────────────────────────────────────────────────────────\n\ntype QueryRoot = ParentNode;\n\nconst describeRoot = (root: QueryRoot): string => {\n if (root instanceof Element) return root.outerHTML.slice(0, 500);\n\n if (root instanceof ShadowRoot) return `<shadow-root>${root.innerHTML.slice(0, 500)}</shadow-root>`;\n\n return root.nodeName;\n};\n\nconst requireMatch = <E extends Element>(element: E | null, description: string, root: QueryRoot): E => {\n if (element) return element;\n\n throw new AssayQueryError(`Unable to find ${description} within ${describeRoot(root)}.`);\n};\n\nconst query = <E extends Element = Element>(root: QueryRoot, selector: string): E | null =>\n root.querySelector<E>(selector);\n\nconst queryAll = <E extends Element = Element>(root: QueryRoot, selector: string): E[] => {\n return Array.from(root.querySelectorAll<E>(selector));\n};\n\nconst queryByTestId = <E extends Element = Element>(root: QueryRoot, testId: string): E | null =>\n queryAll<E>(root, '[data-testid]').find((element) => element.getAttribute('data-testid') === testId) ?? null;\n\nconst queryAllByTestId = <E extends Element = Element>(root: QueryRoot, testId: string): E[] =>\n queryAll<E>(root, '[data-testid]').filter((element) => element.getAttribute('data-testid') === testId);\n\nconst queryByText = <E extends Element = Element>(root: QueryRoot, text: string, selector = '*'): E | null => {\n for (const element of root.querySelectorAll<E>(selector)) {\n if (element.textContent?.trim() === text) return element;\n }\n\n return null;\n};\n\nconst queryAllByText = <E extends Element = Element>(root: QueryRoot, text: string, selector = '*'): E[] =>\n queryAll<E>(root, selector).filter((element) => element.textContent?.trim() === text);\n\n/**\n * Queries the shadow root of a custom element for a matching CSS selector.\n * Returns `null` if the element has no shadow root or no match is found — safe to call on\n * both open-shadow custom elements and plain elements without checking `shadowRoot` first.\n */\nexport function queryInShadow<E extends Element = Element>(host: Element, selector: string): E | null {\n return host.shadowRoot ? query<E>(host.shadowRoot, selector) : null;\n}\n\n/** Queries all matching elements inside the shadow root of a custom element. */\nexport function queryAllInShadow<E extends Element = Element>(host: Element, selector: string): E[] {\n return host.shadowRoot ? queryAll<E>(host.shadowRoot, selector) : [];\n}\n\n/**\n * Queries a shadow DOM element by its CSS `part` attribute — shorthand for\n * `queryInShadow(host, '[part=\"name\"]')`.\n *\n * @example\n * const btn = queryPart(carousel, 'prev-btn');\n * expect(btn).not.toBeNull();\n */\nexport function queryPart<E extends Element = Element>(host: Element, part: string): E | null {\n return (\n queryAllInShadow<E>(host, '[part]').find((element) => element.getAttribute('part')?.split(/\\s+/).includes(part)) ??\n null\n );\n}\n\n/**\n * Returns the light-DOM children assigned to a named slot, or every child assigned to the\n * default slot when no name is given.\n *\n * @example\n * const slides = getSlotted(carousel);\n * expect(slides).toHaveLength(3);\n */\nexport function getSlotted<E extends Element = Element>(host: Element, slotName?: string): E[] {\n return Array.from(host.children).filter((element) =>\n slotName === undefined ? !element.getAttribute('slot') : element.getAttribute('slot') === slotName,\n ) as E[];\n}\n\n// ─── API ─────────────────────────────────────────────────────────────────────\n\n/**\n * Create query helpers scoped to a DOM root — useful for light DOM and shadow roots.\n *\n * @example\n * const view = within(fixture.shadow!);\n * expect(view.get('.title').textContent).toBe('Hello');\n */\nexport function within(root: QueryRoot): QueryScope {\n return {\n get: <E extends Element = Element>(selector: string) =>\n requireMatch(query<E>(root, selector), `\"${selector}\"`, root),\n getByTestId: <E extends Element = Element>(testId: string) =>\n requireMatch(queryByTestId<E>(root, testId), `data-testid \"${testId}\"`, root),\n getByText: <E extends Element = Element>(text: string, selector = '*') =>\n requireMatch(queryByText<E>(root, text, selector), `text \"${text}\" matching \"${selector}\"`, root),\n query: <E extends Element = Element>(selector: string) => query<E>(root, selector),\n queryAll: <E extends Element = Element>(selector: string) => queryAll<E>(root, selector),\n queryAllByTestId: <E extends Element = Element>(testId: string) => queryAllByTestId<E>(root, testId),\n queryAllByText: <E extends Element = Element>(text: string, selector = '*') =>\n queryAllByText<E>(root, text, selector),\n queryByTestId: <E extends Element = Element>(testId: string) => queryByTestId<E>(root, testId),\n queryByText: <E extends Element = Element>(text: string, selector = '*') => queryByText<E>(root, text, selector),\n };\n}\n","import { abortError, sleep } from '@vielzeug/arsenal';\n\nimport { AssayTimeoutError } from './errors';\n\nexport interface DelayOptions {\n signal?: AbortSignal;\n}\n\nexport interface WaitOptions {\n /** Polling interval in ms (default: 50). */\n interval?: number;\n /** Cancel the pending wait. */\n signal?: AbortSignal;\n /** Maximum wait time in ms (default: 1000). */\n timeout?: number;\n}\n\nexport interface EventuallyOptions extends WaitOptions {\n /** Context included in the timeout error. */\n message?: string;\n}\n\nconst DEADLINE = Symbol('assay.deadline');\nconst MAX_TIMER_MS = 2_147_483_647;\nconst now = (): number => performance.now();\n\nconst validateDuration = (name: string, value: number, allowZero: boolean): void => {\n if (!Number.isFinite(value) || value > MAX_TIMER_MS || (allowZero ? value < 0 : value <= 0)) {\n throw new RangeError(\n `${name} must be a ${allowZero ? 'non-negative' : 'positive'} finite number no greater than ${MAX_TIMER_MS}`,\n );\n }\n};\n\nconst validateWait = (interval: number, timeout: number): void => {\n validateDuration('interval', interval, false);\n validateDuration('timeout', timeout, true);\n};\n\nconst runBounded = <T>(run: () => T | Promise<T>, deadline: number, signal?: AbortSignal): Promise<T> =>\n new Promise((resolve, reject) => {\n if (signal?.aborted) {\n reject(abortError(signal, 'The operation was aborted.'));\n return;\n }\n\n let settled = false;\n const cleanup = (): boolean => {\n if (settled) return false;\n settled = true;\n clearTimeout(timer);\n signal?.removeEventListener('abort', abort);\n return true;\n };\n const resolveOnce = (value: T): void => {\n if (cleanup()) resolve(value);\n };\n const rejectOnce = (error: unknown): void => {\n if (cleanup()) reject(error);\n };\n const timer = setTimeout(() => rejectOnce(DEADLINE), Math.max(0, deadline - now()));\n const abort = () => rejectOnce(abortError(signal, 'The operation was aborted.'));\n\n signal?.addEventListener('abort', abort, { once: true });\n\n try {\n Promise.resolve(run()).then(resolveOnce, rejectOnce);\n } catch (error) {\n rejectOnce(error);\n }\n });\n\nexport const delay = (ms = 0, options: DelayOptions = {}): Promise<void> => {\n validateDuration('ms', ms, true);\n return sleep(ms, options.signal);\n};\n\nexport async function waitUntil(\n predicate: () => boolean | Promise<boolean>,\n { interval = 50, signal, timeout = 1000 }: WaitOptions = {},\n): Promise<void> {\n validateWait(interval, timeout);\n const deadline = now() + timeout;\n let first = true;\n\n for (;;) {\n if (signal?.aborted) throw abortError(signal, 'The operation was aborted.');\n if (!first && now() >= deadline) throw new AssayTimeoutError(`waitUntil timed out after ${timeout}ms`);\n first = false;\n\n let matched: boolean;\n try {\n matched = await runBounded(predicate, deadline, signal);\n } catch (error) {\n if (error === DEADLINE) throw new AssayTimeoutError(`waitUntil timed out after ${timeout}ms`);\n throw error;\n }\n if (matched) return;\n\n const remaining = deadline - now();\n if (remaining <= 0) throw new AssayTimeoutError(`waitUntil timed out after ${timeout}ms`);\n await sleep(Math.min(interval, remaining), signal);\n }\n}\n\n/**\n * Retry an assertion until it stops throwing or the timeout elapses.\n *\n * Polls the assertion on a fixed interval and preserves the last failure as the\n * timeout cause. Pending asynchronous assertions remain bounded by the same\n * deadline. Use `waitUntil()` for boolean conditions, `delay()` for explicit\n * timer dependencies, and `nextTick()` for one queued microtask boundary.\n *\n * @example\n * await eventually(() => expect(region.textContent).toBe('Saved'));\n */\nexport async function eventually(\n assertion: () => void | Promise<void>,\n { interval = 50, message, signal, timeout = 1000 }: EventuallyOptions = {},\n): Promise<void> {\n validateWait(interval, timeout);\n const deadline = now() + timeout;\n let first = true;\n let lastError: unknown;\n\n for (;;) {\n if (signal?.aborted) throw abortError(signal, 'The operation was aborted.');\n if (!first && now() >= deadline) break;\n first = false;\n\n try {\n await runBounded(assertion, deadline, signal);\n return;\n } catch (error) {\n if (signal?.aborted) throw abortError(signal, 'The operation was aborted.');\n if (error === DEADLINE) break;\n lastError = error;\n }\n\n const remaining = deadline - now();\n if (remaining <= 0) break;\n await sleep(Math.min(interval, remaining), signal);\n }\n\n const timing = `eventually timed out after ${timeout}ms`;\n const context = message ? `${message} (${timing})` : timing;\n const detail = lastError instanceof Error ? lastError.message : lastError == null ? '' : String(lastError);\n\n throw new AssayTimeoutError(detail ? `${context}\\n${detail}` : context, { cause: lastError });\n}\n\n/** Resolve with the next event of `type`, or reject when it times out or is aborted. */\nexport function waitForEvent<T extends Event = Event>(\n target: EventTarget,\n type: string,\n { signal, timeout = 1000 }: Omit<WaitOptions, 'interval'> = {},\n): Promise<T> {\n validateDuration('timeout', timeout, true);\n\n return new Promise((resolve, reject) => {\n if (signal?.aborted) {\n reject(abortError(signal, 'The operation was aborted.'));\n return;\n }\n\n const dispose = () => {\n clearTimeout(timer);\n signal?.removeEventListener('abort', onAbort);\n target.removeEventListener(type, onEvent);\n };\n const onEvent = (event: Event): void => {\n dispose();\n resolve(event as T);\n };\n const timer = setTimeout(() => {\n dispose();\n reject(new AssayTimeoutError(`waitForEvent: \"${type}\" timed out after ${timeout}ms`));\n }, timeout);\n const onAbort = () => {\n dispose();\n reject(abortError(signal, 'The operation was aborted.'));\n };\n\n target.addEventListener(type, onEvent, { once: true });\n signal?.addEventListener('abort', onAbort, { once: true });\n });\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"assay.js","names":[],"sources":["../src/errors.ts","../src/events.ts","../src/query.ts","../src/wait.ts"],"sourcesContent":["/** Base class for all Assay errors. Use `instanceof AssayError` to catch any Assay-originated error. */\nexport class AssayError extends Error {\n constructor(message: string, opts?: ErrorOptions) {\n super(message, opts);\n this.name = new.target.name;\n Object.setPrototypeOf(this, new.target.prototype);\n }\n}\n\n/** Thrown when a required query has no match. */\nexport class AssayQueryError extends AssayError {}\n\n/** Thrown when an Assay wait reaches its timeout. */\nexport class AssayTimeoutError extends AssayError {}\n","/**\n * # Confidence boundary: read first\n *\n * `dispatch` is a **low-level synthetic-event dispatch API**. It fires a caller-\n * supplied DOM `Event` synchronously and returns the `dispatchEvent`\n * result. It does **not** model real user interaction: there is no focus/blur\n * sequencing, no input-event coordination, no keyboard layout, no pointer\n * hit-testing, and no trust flag (`event.isTrusted` is always `false`).\n *\n * Use it to drive event handlers in unit/integration tests where you control\n * the exact event shape. For **behavioral confidence**, that a real click\n * actually activates a control, that focus management works end-to-end, that\n * keyboard shortcuts fire under a real input device: write Playwright tests.\n * Assay deliberately stops at the dispatch boundary; anything that depends on\n * the browser's interaction semantics belongs in a browser-driven suite.\n */\n\n/** Dispatch a pre-built `Event` instance unchanged. Returns the `dispatchEvent` result. */\nexport const dispatch = (target: EventTarget, event: Event): boolean => target.dispatchEvent(event);\n\n/** Synthetic convenience dispatchers. They share the same confidence boundary as `dispatch()`. */\nexport const fireBlur = (target: EventTarget, init: FocusEventInit = {}): boolean =>\n dispatch(target, new FocusEvent('blur', init));\n\nexport const fireChange = (target: EventTarget, init: EventInit = {}): boolean =>\n dispatch(target, new Event('change', { bubbles: true, ...init }));\n\nexport const fireClick = (target: EventTarget, init: MouseEventInit = {}): boolean =>\n dispatch(target, new MouseEvent('click', { bubbles: true, cancelable: true, ...init }));\n\nexport const fireCustom = <T>(target: EventTarget, type: string, init: CustomEventInit<T> = {}): boolean =>\n dispatch(target, new CustomEvent(type, { bubbles: true, cancelable: true, composed: false, ...init }));\n\nexport const fireFocus = (target: EventTarget, init: FocusEventInit = {}): boolean =>\n dispatch(target, new FocusEvent('focus', init));\n\nexport const fireInput = (target: EventTarget, init: InputEventInit = {}): boolean =>\n dispatch(target, new InputEvent('input', { bubbles: true, ...init }));\n\nexport const fireKeyDown = (target: EventTarget, init: KeyboardEventInit = {}): boolean =>\n dispatch(target, new KeyboardEvent('keydown', { bubbles: true, cancelable: true, ...init }));\n\nexport const fireKeyUp = (target: EventTarget, init: KeyboardEventInit = {}): boolean =>\n dispatch(target, new KeyboardEvent('keyup', { bubbles: true, cancelable: true, ...init }));\n\nexport const fireSubmit = (target: EventTarget, init: SubmitEventInit = {}): boolean =>\n dispatch(target, new SubmitEvent('submit', { bubbles: true, cancelable: true, ...init }));\n","import { AssayQueryError } from './errors';\n\n/** DOM query helpers for test environments. */\n\n// ─── Types ───────────────────────────────────────────────────────────────────\n\n/** Scoped query helpers for any queryable DOM root: see {@link within}. */\nexport interface QueryScope {\n get<E extends Element = Element>(selector: string): E;\n getByTestId<E extends Element = Element>(testId: string): E;\n getByText<E extends Element = Element>(text: string, selector?: string): E;\n query<E extends Element = Element>(selector: string): E | null;\n queryAll<E extends Element = Element>(selector: string): E[];\n queryAllByTestId<E extends Element = Element>(testId: string): E[];\n queryAllByText<E extends Element = Element>(text: string, selector?: string): E[];\n queryByTestId<E extends Element = Element>(testId: string): E | null;\n queryByText<E extends Element = Element>(text: string, selector?: string): E | null;\n}\n\n// ─── Helpers ─────────────────────────────────────────────────────────────────\n\ntype QueryRoot = ParentNode;\n\nconst describeRoot = (root: QueryRoot): string => {\n if (root instanceof Element) return root.outerHTML.slice(0, 500);\n\n if (root instanceof ShadowRoot) return `<shadow-root>${root.innerHTML.slice(0, 500)}</shadow-root>`;\n\n return root.nodeName;\n};\n\nconst requireMatch = <E extends Element>(element: E | null, description: string, root: QueryRoot): E => {\n if (element) return element;\n\n throw new AssayQueryError(`Unable to find ${description} within ${describeRoot(root)}.`);\n};\n\nconst query = <E extends Element = Element>(root: QueryRoot, selector: string): E | null =>\n root.querySelector<E>(selector);\n\nconst queryAll = <E extends Element = Element>(root: QueryRoot, selector: string): E[] => {\n return Array.from(root.querySelectorAll<E>(selector));\n};\n\nconst queryByTestId = <E extends Element = Element>(root: QueryRoot, testId: string): E | null =>\n queryAll<E>(root, '[data-testid]').find((element) => element.getAttribute('data-testid') === testId) ?? null;\n\nconst queryAllByTestId = <E extends Element = Element>(root: QueryRoot, testId: string): E[] =>\n queryAll<E>(root, '[data-testid]').filter((element) => element.getAttribute('data-testid') === testId);\n\nconst queryByText = <E extends Element = Element>(root: QueryRoot, text: string, selector = '*'): E | null => {\n for (const element of root.querySelectorAll<E>(selector)) {\n if (element.textContent?.trim() === text) return element;\n }\n\n return null;\n};\n\nconst queryAllByText = <E extends Element = Element>(root: QueryRoot, text: string, selector = '*'): E[] =>\n queryAll<E>(root, selector).filter((element) => element.textContent?.trim() === text);\n\n/**\n * Queries the shadow root of a custom element for a matching CSS selector.\n * Returns `null` if the element has no shadow root or no match is found: safe to call on\n * both open-shadow custom elements and plain elements without checking `shadowRoot` first.\n */\nexport function queryInShadow<E extends Element = Element>(host: Element, selector: string): E | null {\n return host.shadowRoot ? query<E>(host.shadowRoot, selector) : null;\n}\n\n/** Queries all matching elements inside the shadow root of a custom element. */\nexport function queryAllInShadow<E extends Element = Element>(host: Element, selector: string): E[] {\n return host.shadowRoot ? queryAll<E>(host.shadowRoot, selector) : [];\n}\n\n/**\n * Queries a shadow DOM element by its CSS `part` attribute: shorthand for\n * `queryInShadow(host, '[part=\"name\"]')`.\n *\n * @example\n * const btn = queryPart(carousel, 'prev-btn');\n * expect(btn).not.toBeNull();\n */\nexport function queryPart<E extends Element = Element>(host: Element, part: string): E | null {\n return (\n queryAllInShadow<E>(host, '[part]').find((element) => element.getAttribute('part')?.split(/\\s+/).includes(part)) ??\n null\n );\n}\n\n/**\n * Returns the light-DOM children assigned to a named slot, or every child assigned to the\n * default slot when no name is given.\n *\n * @example\n * const slides = getSlotted(carousel);\n * expect(slides).toHaveLength(3);\n */\nexport function getSlotted<E extends Element = Element>(host: Element, slotName?: string): E[] {\n return Array.from(host.children).filter((element) =>\n slotName === undefined ? !element.getAttribute('slot') : element.getAttribute('slot') === slotName,\n ) as E[];\n}\n\n// ─── API ─────────────────────────────────────────────────────────────────────\n\n/**\n * Create query helpers scoped to a DOM root: useful for light DOM and shadow roots.\n *\n * @example\n * const view = within(fixture.shadow!);\n * expect(view.get('.title').textContent).toBe('Hello');\n */\nexport function within(root: QueryRoot): QueryScope {\n return {\n get: <E extends Element = Element>(selector: string) =>\n requireMatch(query<E>(root, selector), `\"${selector}\"`, root),\n getByTestId: <E extends Element = Element>(testId: string) =>\n requireMatch(queryByTestId<E>(root, testId), `data-testid \"${testId}\"`, root),\n getByText: <E extends Element = Element>(text: string, selector = '*') =>\n requireMatch(queryByText<E>(root, text, selector), `text \"${text}\" matching \"${selector}\"`, root),\n query: <E extends Element = Element>(selector: string) => query<E>(root, selector),\n queryAll: <E extends Element = Element>(selector: string) => queryAll<E>(root, selector),\n queryAllByTestId: <E extends Element = Element>(testId: string) => queryAllByTestId<E>(root, testId),\n queryAllByText: <E extends Element = Element>(text: string, selector = '*') =>\n queryAllByText<E>(root, text, selector),\n queryByTestId: <E extends Element = Element>(testId: string) => queryByTestId<E>(root, testId),\n queryByText: <E extends Element = Element>(text: string, selector = '*') => queryByText<E>(root, text, selector),\n };\n}\n","import { abortError, sleep } from '@vielzeug/arsenal';\n\nimport { AssayTimeoutError } from './errors';\n\nexport interface DelayOptions {\n signal?: AbortSignal;\n}\n\nexport interface WaitOptions {\n /** Polling interval in ms (default: 50). */\n interval?: number;\n /** Cancel the pending wait. */\n signal?: AbortSignal;\n /** Maximum wait time in ms (default: 1000). */\n timeout?: number;\n}\n\nexport interface EventuallyOptions extends WaitOptions {\n /** Context included in the timeout error. */\n message?: string;\n}\n\nconst DEADLINE = Symbol('assay.deadline');\nconst MAX_TIMER_MS = 2_147_483_647;\nconst now = (): number => performance.now();\n\nconst validateDuration = (name: string, value: number, allowZero: boolean): void => {\n if (!Number.isFinite(value) || value > MAX_TIMER_MS || (allowZero ? value < 0 : value <= 0)) {\n throw new RangeError(\n `${name} must be a ${allowZero ? 'non-negative' : 'positive'} finite number no greater than ${MAX_TIMER_MS}`,\n );\n }\n};\n\nconst validateWait = (interval: number, timeout: number): void => {\n validateDuration('interval', interval, false);\n validateDuration('timeout', timeout, true);\n};\n\nconst runBounded = <T>(run: () => T | Promise<T>, deadline: number, signal?: AbortSignal): Promise<T> =>\n new Promise((resolve, reject) => {\n if (signal?.aborted) {\n reject(abortError(signal, 'The operation was aborted.'));\n return;\n }\n\n let settled = false;\n const cleanup = (): boolean => {\n if (settled) return false;\n settled = true;\n clearTimeout(timer);\n signal?.removeEventListener('abort', abort);\n return true;\n };\n const resolveOnce = (value: T): void => {\n if (cleanup()) resolve(value);\n };\n const rejectOnce = (error: unknown): void => {\n if (cleanup()) reject(error);\n };\n const timer = setTimeout(() => rejectOnce(DEADLINE), Math.max(0, deadline - now()));\n const abort = () => rejectOnce(abortError(signal, 'The operation was aborted.'));\n\n signal?.addEventListener('abort', abort, { once: true });\n\n try {\n Promise.resolve(run()).then(resolveOnce, rejectOnce);\n } catch (error) {\n rejectOnce(error);\n }\n });\n\nexport const delay = (ms = 0, options: DelayOptions = {}): Promise<void> => {\n validateDuration('ms', ms, true);\n return sleep(ms, options.signal);\n};\n\nexport async function waitUntil(\n predicate: () => boolean | Promise<boolean>,\n { interval = 50, signal, timeout = 1000 }: WaitOptions = {},\n): Promise<void> {\n validateWait(interval, timeout);\n const deadline = now() + timeout;\n let first = true;\n\n for (;;) {\n if (signal?.aborted) throw abortError(signal, 'The operation was aborted.');\n if (!first && now() >= deadline) throw new AssayTimeoutError(`waitUntil timed out after ${timeout}ms`);\n first = false;\n\n let matched: boolean;\n try {\n matched = await runBounded(predicate, deadline, signal);\n } catch (error) {\n if (error === DEADLINE) throw new AssayTimeoutError(`waitUntil timed out after ${timeout}ms`);\n throw error;\n }\n if (matched) return;\n\n const remaining = deadline - now();\n if (remaining <= 0) throw new AssayTimeoutError(`waitUntil timed out after ${timeout}ms`);\n await sleep(Math.min(interval, remaining), signal);\n }\n}\n\n/**\n * Retry an assertion until it stops throwing or the timeout elapses.\n *\n * Polls the assertion on a fixed interval and preserves the last failure as the\n * timeout cause. Pending asynchronous assertions remain bounded by the same\n * deadline. Use `waitUntil()` for boolean conditions, `delay()` for explicit\n * timer dependencies, and `nextTick()` for one queued microtask boundary.\n *\n * @example\n * await eventually(() => expect(region.textContent).toBe('Saved'));\n */\nexport async function eventually(\n assertion: () => void | Promise<void>,\n { interval = 50, message, signal, timeout = 1000 }: EventuallyOptions = {},\n): Promise<void> {\n validateWait(interval, timeout);\n const deadline = now() + timeout;\n let first = true;\n let lastError: unknown;\n\n for (;;) {\n if (signal?.aborted) throw abortError(signal, 'The operation was aborted.');\n if (!first && now() >= deadline) break;\n first = false;\n\n try {\n await runBounded(assertion, deadline, signal);\n return;\n } catch (error) {\n if (signal?.aborted) throw abortError(signal, 'The operation was aborted.');\n if (error === DEADLINE) break;\n lastError = error;\n }\n\n const remaining = deadline - now();\n if (remaining <= 0) break;\n await sleep(Math.min(interval, remaining), signal);\n }\n\n const timing = `eventually timed out after ${timeout}ms`;\n const context = message ? `${message} (${timing})` : timing;\n const detail = lastError instanceof Error ? lastError.message : lastError == null ? '' : String(lastError);\n\n throw new AssayTimeoutError(detail ? `${context}\\n${detail}` : context, { cause: lastError });\n}\n\n/** Resolve with the next event of `type`, or reject when it times out or is aborted. */\nexport function waitForEvent<T extends Event = Event>(\n target: EventTarget,\n type: string,\n { signal, timeout = 1000 }: Omit<WaitOptions, 'interval'> = {},\n): Promise<T> {\n validateDuration('timeout', timeout, true);\n\n return new Promise((resolve, reject) => {\n if (signal?.aborted) {\n reject(abortError(signal, 'The operation was aborted.'));\n return;\n }\n\n const dispose = () => {\n clearTimeout(timer);\n signal?.removeEventListener('abort', onAbort);\n target.removeEventListener(type, onEvent);\n };\n const onEvent = (event: Event): void => {\n dispose();\n resolve(event as T);\n };\n const timer = setTimeout(() => {\n dispose();\n reject(new AssayTimeoutError(`waitForEvent: \"${type}\" timed out after ${timeout}ms`));\n }, timeout);\n const onAbort = () => {\n dispose();\n reject(abortError(signal, 'The operation was aborted.'));\n };\n\n target.addEventListener(type, onEvent, { once: true });\n signal?.addEventListener('abort', onAbort, { once: true });\n });\n}\n"],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"events.cjs","names":[],"sources":["../src/events.ts"],"sourcesContent":["/**\n * # Confidence boundary — read first\n *\n * `dispatch` is a **low-level synthetic-event dispatch API**. It fires a caller-\n * supplied DOM `Event` synchronously and returns the `dispatchEvent`\n * result. It does **not** model real user interaction: there is no focus/blur\n * sequencing, no input-event coordination, no keyboard layout, no pointer\n * hit-testing, and no trust flag (`event.isTrusted` is always `false`).\n *\n * Use it to drive event handlers in unit/integration tests where you control\n * the exact event shape. For **behavioral confidence** — that a real click\n * actually activates a control, that focus management works end-to-end, that\n * keyboard shortcuts fire under a real input device — write Playwright tests.\n * Assay deliberately stops at the dispatch boundary; anything that depends on\n * the browser's interaction semantics belongs in a browser-driven suite.\n */\n\n/** Dispatch a pre-built `Event` instance unchanged. Returns the `dispatchEvent` result. */\nexport const dispatch = (target: EventTarget, event: Event): boolean => target.dispatchEvent(event);\n\n/** Synthetic convenience dispatchers. They share the same confidence boundary as `dispatch()`. */\nexport const fireBlur = (target: EventTarget, init: FocusEventInit = {}): boolean =>\n dispatch(target, new FocusEvent('blur', init));\n\nexport const fireChange = (target: EventTarget, init: EventInit = {}): boolean =>\n dispatch(target, new Event('change', { bubbles: true, ...init }));\n\nexport const fireClick = (target: EventTarget, init: MouseEventInit = {}): boolean =>\n dispatch(target, new MouseEvent('click', { bubbles: true, cancelable: true, ...init }));\n\nexport const fireCustom = <T>(target: EventTarget, type: string, init: CustomEventInit<T> = {}): boolean =>\n dispatch(target, new CustomEvent(type, { bubbles: true, cancelable: true, composed: false, ...init }));\n\nexport const fireFocus = (target: EventTarget, init: FocusEventInit = {}): boolean =>\n dispatch(target, new FocusEvent('focus', init));\n\nexport const fireInput = (target: EventTarget, init: InputEventInit = {}): boolean =>\n dispatch(target, new InputEvent('input', { bubbles: true, ...init }));\n\nexport const fireKeyDown = (target: EventTarget, init: KeyboardEventInit = {}): boolean =>\n dispatch(target, new KeyboardEvent('keydown', { bubbles: true, cancelable: true, ...init }));\n\nexport const fireKeyUp = (target: EventTarget, init: KeyboardEventInit = {}): boolean =>\n dispatch(target, new KeyboardEvent('keyup', { bubbles: true, cancelable: true, ...init }));\n\nexport const fireSubmit = (target: EventTarget, init: SubmitEventInit = {}): boolean =>\n dispatch(target, new SubmitEvent('submit', { bubbles: true, cancelable: true, ...init }));\n"],"mappings":"AAkBA,IAAa,UAAY,EAAqB,IAA0B,EAAO,cAAc,CAAK,EAGrF,UAAY,EAAqB,EAAuB,CAAC,IACpE,SAAS,EAAQ,IAAI,WAAW,OAAQ,CAAI,CAAC,EAElC,YAAc,EAAqB,EAAkB,CAAC,IACjE,SAAS,EAAQ,IAAI,MAAM,SAAU,CAAE,QAAS,GAAM,GAAG,CAAK,CAAC,CAAC,EAErD,WAAa,EAAqB,EAAuB,CAAC,IACrE,SAAS,EAAQ,IAAI,WAAW,QAAS,CAAE,QAAS,GAAM,WAAY,GAAM,GAAG,CAAK,CAAC,CAAC,EAE3E,YAAiB,EAAqB,EAAc,EAA2B,CAAC,IAC3F,SAAS,EAAQ,IAAI,YAAY,EAAM,CAAE,QAAS,GAAM,WAAY,GAAM,SAAU,GAAO,GAAG,CAAK,CAAC,CAAC,EAE1F,WAAa,EAAqB,EAAuB,CAAC,IACrE,SAAS,EAAQ,IAAI,WAAW,QAAS,CAAI,CAAC,EAEnC,WAAa,EAAqB,EAAuB,CAAC,IACrE,SAAS,EAAQ,IAAI,WAAW,QAAS,CAAE,QAAS,GAAM,GAAG,CAAK,CAAC,CAAC,EAEzD,aAAe,EAAqB,EAA0B,CAAC,IAC1E,SAAS,EAAQ,IAAI,cAAc,UAAW,CAAE,QAAS,GAAM,WAAY,GAAM,GAAG,CAAK,CAAC,CAAC,EAEhF,WAAa,EAAqB,EAA0B,CAAC,IACxE,SAAS,EAAQ,IAAI,cAAc,QAAS,CAAE,QAAS,GAAM,WAAY,GAAM,GAAG,CAAK,CAAC,CAAC,EAE9E,YAAc,EAAqB,EAAwB,CAAC,IACvE,SAAS,EAAQ,IAAI,YAAY,SAAU,CAAE,QAAS,GAAM,WAAY,GAAM,GAAG,CAAK,CAAC,CAAC"}
1
+ {"version":3,"file":"events.cjs","names":[],"sources":["../src/events.ts"],"sourcesContent":["/**\n * # Confidence boundary: read first\n *\n * `dispatch` is a **low-level synthetic-event dispatch API**. It fires a caller-\n * supplied DOM `Event` synchronously and returns the `dispatchEvent`\n * result. It does **not** model real user interaction: there is no focus/blur\n * sequencing, no input-event coordination, no keyboard layout, no pointer\n * hit-testing, and no trust flag (`event.isTrusted` is always `false`).\n *\n * Use it to drive event handlers in unit/integration tests where you control\n * the exact event shape. For **behavioral confidence**, that a real click\n * actually activates a control, that focus management works end-to-end, that\n * keyboard shortcuts fire under a real input device: write Playwright tests.\n * Assay deliberately stops at the dispatch boundary; anything that depends on\n * the browser's interaction semantics belongs in a browser-driven suite.\n */\n\n/** Dispatch a pre-built `Event` instance unchanged. Returns the `dispatchEvent` result. */\nexport const dispatch = (target: EventTarget, event: Event): boolean => target.dispatchEvent(event);\n\n/** Synthetic convenience dispatchers. They share the same confidence boundary as `dispatch()`. */\nexport const fireBlur = (target: EventTarget, init: FocusEventInit = {}): boolean =>\n dispatch(target, new FocusEvent('blur', init));\n\nexport const fireChange = (target: EventTarget, init: EventInit = {}): boolean =>\n dispatch(target, new Event('change', { bubbles: true, ...init }));\n\nexport const fireClick = (target: EventTarget, init: MouseEventInit = {}): boolean =>\n dispatch(target, new MouseEvent('click', { bubbles: true, cancelable: true, ...init }));\n\nexport const fireCustom = <T>(target: EventTarget, type: string, init: CustomEventInit<T> = {}): boolean =>\n dispatch(target, new CustomEvent(type, { bubbles: true, cancelable: true, composed: false, ...init }));\n\nexport const fireFocus = (target: EventTarget, init: FocusEventInit = {}): boolean =>\n dispatch(target, new FocusEvent('focus', init));\n\nexport const fireInput = (target: EventTarget, init: InputEventInit = {}): boolean =>\n dispatch(target, new InputEvent('input', { bubbles: true, ...init }));\n\nexport const fireKeyDown = (target: EventTarget, init: KeyboardEventInit = {}): boolean =>\n dispatch(target, new KeyboardEvent('keydown', { bubbles: true, cancelable: true, ...init }));\n\nexport const fireKeyUp = (target: EventTarget, init: KeyboardEventInit = {}): boolean =>\n dispatch(target, new KeyboardEvent('keyup', { bubbles: true, cancelable: true, ...init }));\n\nexport const fireSubmit = (target: EventTarget, init: SubmitEventInit = {}): boolean =>\n dispatch(target, new SubmitEvent('submit', { bubbles: true, cancelable: true, ...init }));\n"],"mappings":"AAkBA,IAAa,UAAY,EAAqB,IAA0B,EAAO,cAAc,CAAK,EAGrF,UAAY,EAAqB,EAAuB,CAAC,IACpE,SAAS,EAAQ,IAAI,WAAW,OAAQ,CAAI,CAAC,EAElC,YAAc,EAAqB,EAAkB,CAAC,IACjE,SAAS,EAAQ,IAAI,MAAM,SAAU,CAAE,QAAS,GAAM,GAAG,CAAK,CAAC,CAAC,EAErD,WAAa,EAAqB,EAAuB,CAAC,IACrE,SAAS,EAAQ,IAAI,WAAW,QAAS,CAAE,QAAS,GAAM,WAAY,GAAM,GAAG,CAAK,CAAC,CAAC,EAE3E,YAAiB,EAAqB,EAAc,EAA2B,CAAC,IAC3F,SAAS,EAAQ,IAAI,YAAY,EAAM,CAAE,QAAS,GAAM,WAAY,GAAM,SAAU,GAAO,GAAG,CAAK,CAAC,CAAC,EAE1F,WAAa,EAAqB,EAAuB,CAAC,IACrE,SAAS,EAAQ,IAAI,WAAW,QAAS,CAAI,CAAC,EAEnC,WAAa,EAAqB,EAAuB,CAAC,IACrE,SAAS,EAAQ,IAAI,WAAW,QAAS,CAAE,QAAS,GAAM,GAAG,CAAK,CAAC,CAAC,EAEzD,aAAe,EAAqB,EAA0B,CAAC,IAC1E,SAAS,EAAQ,IAAI,cAAc,UAAW,CAAE,QAAS,GAAM,WAAY,GAAM,GAAG,CAAK,CAAC,CAAC,EAEhF,WAAa,EAAqB,EAA0B,CAAC,IACxE,SAAS,EAAQ,IAAI,cAAc,QAAS,CAAE,QAAS,GAAM,WAAY,GAAM,GAAG,CAAK,CAAC,CAAC,EAE9E,YAAc,EAAqB,EAAwB,CAAC,IACvE,SAAS,EAAQ,IAAI,YAAY,SAAU,CAAE,QAAS,GAAM,WAAY,GAAM,GAAG,CAAK,CAAC,CAAC"}
package/dist/events.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  /**
2
- * # Confidence boundary — read first
2
+ * # Confidence boundary: read first
3
3
  *
4
4
  * `dispatch` is a **low-level synthetic-event dispatch API**. It fires a caller-
5
5
  * supplied DOM `Event` synchronously and returns the `dispatchEvent`
@@ -8,9 +8,9 @@
8
8
  * hit-testing, and no trust flag (`event.isTrusted` is always `false`).
9
9
  *
10
10
  * Use it to drive event handlers in unit/integration tests where you control
11
- * the exact event shape. For **behavioral confidence** — that a real click
11
+ * the exact event shape. For **behavioral confidence**, that a real click
12
12
  * actually activates a control, that focus management works end-to-end, that
13
- * keyboard shortcuts fire under a real input device — write Playwright tests.
13
+ * keyboard shortcuts fire under a real input device: write Playwright tests.
14
14
  * Assay deliberately stops at the dispatch boundary; anything that depends on
15
15
  * the browser's interaction semantics belongs in a browser-driven suite.
16
16
  */
@@ -1 +1 @@
1
- {"version":3,"file":"events.js","names":[],"sources":["../src/events.ts"],"sourcesContent":["/**\n * # Confidence boundary — read first\n *\n * `dispatch` is a **low-level synthetic-event dispatch API**. It fires a caller-\n * supplied DOM `Event` synchronously and returns the `dispatchEvent`\n * result. It does **not** model real user interaction: there is no focus/blur\n * sequencing, no input-event coordination, no keyboard layout, no pointer\n * hit-testing, and no trust flag (`event.isTrusted` is always `false`).\n *\n * Use it to drive event handlers in unit/integration tests where you control\n * the exact event shape. For **behavioral confidence** — that a real click\n * actually activates a control, that focus management works end-to-end, that\n * keyboard shortcuts fire under a real input device — write Playwright tests.\n * Assay deliberately stops at the dispatch boundary; anything that depends on\n * the browser's interaction semantics belongs in a browser-driven suite.\n */\n\n/** Dispatch a pre-built `Event` instance unchanged. Returns the `dispatchEvent` result. */\nexport const dispatch = (target: EventTarget, event: Event): boolean => target.dispatchEvent(event);\n\n/** Synthetic convenience dispatchers. They share the same confidence boundary as `dispatch()`. */\nexport const fireBlur = (target: EventTarget, init: FocusEventInit = {}): boolean =>\n dispatch(target, new FocusEvent('blur', init));\n\nexport const fireChange = (target: EventTarget, init: EventInit = {}): boolean =>\n dispatch(target, new Event('change', { bubbles: true, ...init }));\n\nexport const fireClick = (target: EventTarget, init: MouseEventInit = {}): boolean =>\n dispatch(target, new MouseEvent('click', { bubbles: true, cancelable: true, ...init }));\n\nexport const fireCustom = <T>(target: EventTarget, type: string, init: CustomEventInit<T> = {}): boolean =>\n dispatch(target, new CustomEvent(type, { bubbles: true, cancelable: true, composed: false, ...init }));\n\nexport const fireFocus = (target: EventTarget, init: FocusEventInit = {}): boolean =>\n dispatch(target, new FocusEvent('focus', init));\n\nexport const fireInput = (target: EventTarget, init: InputEventInit = {}): boolean =>\n dispatch(target, new InputEvent('input', { bubbles: true, ...init }));\n\nexport const fireKeyDown = (target: EventTarget, init: KeyboardEventInit = {}): boolean =>\n dispatch(target, new KeyboardEvent('keydown', { bubbles: true, cancelable: true, ...init }));\n\nexport const fireKeyUp = (target: EventTarget, init: KeyboardEventInit = {}): boolean =>\n dispatch(target, new KeyboardEvent('keyup', { bubbles: true, cancelable: true, ...init }));\n\nexport const fireSubmit = (target: EventTarget, init: SubmitEventInit = {}): boolean =>\n dispatch(target, new SubmitEvent('submit', { bubbles: true, cancelable: true, ...init }));\n"],"mappings":"AAkBA,IAAa,UAAY,EAAqB,IAA0B,EAAO,cAAc,CAAK,EAGrF,UAAY,EAAqB,EAAuB,CAAC,IACpE,SAAS,EAAQ,IAAI,WAAW,OAAQ,CAAI,CAAC,EAElC,YAAc,EAAqB,EAAkB,CAAC,IACjE,SAAS,EAAQ,IAAI,MAAM,SAAU,CAAE,QAAS,GAAM,GAAG,CAAK,CAAC,CAAC,EAErD,WAAa,EAAqB,EAAuB,CAAC,IACrE,SAAS,EAAQ,IAAI,WAAW,QAAS,CAAE,QAAS,GAAM,WAAY,GAAM,GAAG,CAAK,CAAC,CAAC,EAE3E,YAAiB,EAAqB,EAAc,EAA2B,CAAC,IAC3F,SAAS,EAAQ,IAAI,YAAY,EAAM,CAAE,QAAS,GAAM,WAAY,GAAM,SAAU,GAAO,GAAG,CAAK,CAAC,CAAC,EAE1F,WAAa,EAAqB,EAAuB,CAAC,IACrE,SAAS,EAAQ,IAAI,WAAW,QAAS,CAAI,CAAC,EAEnC,WAAa,EAAqB,EAAuB,CAAC,IACrE,SAAS,EAAQ,IAAI,WAAW,QAAS,CAAE,QAAS,GAAM,GAAG,CAAK,CAAC,CAAC,EAEzD,aAAe,EAAqB,EAA0B,CAAC,IAC1E,SAAS,EAAQ,IAAI,cAAc,UAAW,CAAE,QAAS,GAAM,WAAY,GAAM,GAAG,CAAK,CAAC,CAAC,EAEhF,WAAa,EAAqB,EAA0B,CAAC,IACxE,SAAS,EAAQ,IAAI,cAAc,QAAS,CAAE,QAAS,GAAM,WAAY,GAAM,GAAG,CAAK,CAAC,CAAC,EAE9E,YAAc,EAAqB,EAAwB,CAAC,IACvE,SAAS,EAAQ,IAAI,YAAY,SAAU,CAAE,QAAS,GAAM,WAAY,GAAM,GAAG,CAAK,CAAC,CAAC"}
1
+ {"version":3,"file":"events.js","names":[],"sources":["../src/events.ts"],"sourcesContent":["/**\n * # Confidence boundary: read first\n *\n * `dispatch` is a **low-level synthetic-event dispatch API**. It fires a caller-\n * supplied DOM `Event` synchronously and returns the `dispatchEvent`\n * result. It does **not** model real user interaction: there is no focus/blur\n * sequencing, no input-event coordination, no keyboard layout, no pointer\n * hit-testing, and no trust flag (`event.isTrusted` is always `false`).\n *\n * Use it to drive event handlers in unit/integration tests where you control\n * the exact event shape. For **behavioral confidence**, that a real click\n * actually activates a control, that focus management works end-to-end, that\n * keyboard shortcuts fire under a real input device: write Playwright tests.\n * Assay deliberately stops at the dispatch boundary; anything that depends on\n * the browser's interaction semantics belongs in a browser-driven suite.\n */\n\n/** Dispatch a pre-built `Event` instance unchanged. Returns the `dispatchEvent` result. */\nexport const dispatch = (target: EventTarget, event: Event): boolean => target.dispatchEvent(event);\n\n/** Synthetic convenience dispatchers. They share the same confidence boundary as `dispatch()`. */\nexport const fireBlur = (target: EventTarget, init: FocusEventInit = {}): boolean =>\n dispatch(target, new FocusEvent('blur', init));\n\nexport const fireChange = (target: EventTarget, init: EventInit = {}): boolean =>\n dispatch(target, new Event('change', { bubbles: true, ...init }));\n\nexport const fireClick = (target: EventTarget, init: MouseEventInit = {}): boolean =>\n dispatch(target, new MouseEvent('click', { bubbles: true, cancelable: true, ...init }));\n\nexport const fireCustom = <T>(target: EventTarget, type: string, init: CustomEventInit<T> = {}): boolean =>\n dispatch(target, new CustomEvent(type, { bubbles: true, cancelable: true, composed: false, ...init }));\n\nexport const fireFocus = (target: EventTarget, init: FocusEventInit = {}): boolean =>\n dispatch(target, new FocusEvent('focus', init));\n\nexport const fireInput = (target: EventTarget, init: InputEventInit = {}): boolean =>\n dispatch(target, new InputEvent('input', { bubbles: true, ...init }));\n\nexport const fireKeyDown = (target: EventTarget, init: KeyboardEventInit = {}): boolean =>\n dispatch(target, new KeyboardEvent('keydown', { bubbles: true, cancelable: true, ...init }));\n\nexport const fireKeyUp = (target: EventTarget, init: KeyboardEventInit = {}): boolean =>\n dispatch(target, new KeyboardEvent('keyup', { bubbles: true, cancelable: true, ...init }));\n\nexport const fireSubmit = (target: EventTarget, init: SubmitEventInit = {}): boolean =>\n dispatch(target, new SubmitEvent('submit', { bubbles: true, cancelable: true, ...init }));\n"],"mappings":"AAkBA,IAAa,UAAY,EAAqB,IAA0B,EAAO,cAAc,CAAK,EAGrF,UAAY,EAAqB,EAAuB,CAAC,IACpE,SAAS,EAAQ,IAAI,WAAW,OAAQ,CAAI,CAAC,EAElC,YAAc,EAAqB,EAAkB,CAAC,IACjE,SAAS,EAAQ,IAAI,MAAM,SAAU,CAAE,QAAS,GAAM,GAAG,CAAK,CAAC,CAAC,EAErD,WAAa,EAAqB,EAAuB,CAAC,IACrE,SAAS,EAAQ,IAAI,WAAW,QAAS,CAAE,QAAS,GAAM,WAAY,GAAM,GAAG,CAAK,CAAC,CAAC,EAE3E,YAAiB,EAAqB,EAAc,EAA2B,CAAC,IAC3F,SAAS,EAAQ,IAAI,YAAY,EAAM,CAAE,QAAS,GAAM,WAAY,GAAM,SAAU,GAAO,GAAG,CAAK,CAAC,CAAC,EAE1F,WAAa,EAAqB,EAAuB,CAAC,IACrE,SAAS,EAAQ,IAAI,WAAW,QAAS,CAAI,CAAC,EAEnC,WAAa,EAAqB,EAAuB,CAAC,IACrE,SAAS,EAAQ,IAAI,WAAW,QAAS,CAAE,QAAS,GAAM,GAAG,CAAK,CAAC,CAAC,EAEzD,aAAe,EAAqB,EAA0B,CAAC,IAC1E,SAAS,EAAQ,IAAI,cAAc,UAAW,CAAE,QAAS,GAAM,WAAY,GAAM,GAAG,CAAK,CAAC,CAAC,EAEhF,WAAa,EAAqB,EAA0B,CAAC,IACxE,SAAS,EAAQ,IAAI,cAAc,QAAS,CAAE,QAAS,GAAM,WAAY,GAAM,GAAG,CAAK,CAAC,CAAC,EAE9E,YAAc,EAAqB,EAAwB,CAAC,IACvE,SAAS,EAAQ,IAAI,YAAY,SAAU,CAAE,QAAS,GAAM,WAAY,GAAM,GAAG,CAAK,CAAC,CAAC"}
@@ -1 +1 @@
1
- {"version":3,"file":"query.cjs","names":[],"sources":["../src/query.ts"],"sourcesContent":["import { AssayQueryError } from './errors';\n\n/** DOM query helpers for test environments. */\n\n// ─── Types ───────────────────────────────────────────────────────────────────\n\n/** Scoped query helpers for any queryable DOM root — see {@link within}. */\nexport interface QueryScope {\n get<E extends Element = Element>(selector: string): E;\n getByTestId<E extends Element = Element>(testId: string): E;\n getByText<E extends Element = Element>(text: string, selector?: string): E;\n query<E extends Element = Element>(selector: string): E | null;\n queryAll<E extends Element = Element>(selector: string): E[];\n queryAllByTestId<E extends Element = Element>(testId: string): E[];\n queryAllByText<E extends Element = Element>(text: string, selector?: string): E[];\n queryByTestId<E extends Element = Element>(testId: string): E | null;\n queryByText<E extends Element = Element>(text: string, selector?: string): E | null;\n}\n\n// ─── Helpers ─────────────────────────────────────────────────────────────────\n\ntype QueryRoot = ParentNode;\n\nconst describeRoot = (root: QueryRoot): string => {\n if (root instanceof Element) return root.outerHTML.slice(0, 500);\n\n if (root instanceof ShadowRoot) return `<shadow-root>${root.innerHTML.slice(0, 500)}</shadow-root>`;\n\n return root.nodeName;\n};\n\nconst requireMatch = <E extends Element>(element: E | null, description: string, root: QueryRoot): E => {\n if (element) return element;\n\n throw new AssayQueryError(`Unable to find ${description} within ${describeRoot(root)}.`);\n};\n\nconst query = <E extends Element = Element>(root: QueryRoot, selector: string): E | null =>\n root.querySelector<E>(selector);\n\nconst queryAll = <E extends Element = Element>(root: QueryRoot, selector: string): E[] => {\n return Array.from(root.querySelectorAll<E>(selector));\n};\n\nconst queryByTestId = <E extends Element = Element>(root: QueryRoot, testId: string): E | null =>\n queryAll<E>(root, '[data-testid]').find((element) => element.getAttribute('data-testid') === testId) ?? null;\n\nconst queryAllByTestId = <E extends Element = Element>(root: QueryRoot, testId: string): E[] =>\n queryAll<E>(root, '[data-testid]').filter((element) => element.getAttribute('data-testid') === testId);\n\nconst queryByText = <E extends Element = Element>(root: QueryRoot, text: string, selector = '*'): E | null => {\n for (const element of root.querySelectorAll<E>(selector)) {\n if (element.textContent?.trim() === text) return element;\n }\n\n return null;\n};\n\nconst queryAllByText = <E extends Element = Element>(root: QueryRoot, text: string, selector = '*'): E[] =>\n queryAll<E>(root, selector).filter((element) => element.textContent?.trim() === text);\n\n/**\n * Queries the shadow root of a custom element for a matching CSS selector.\n * Returns `null` if the element has no shadow root or no match is found — safe to call on\n * both open-shadow custom elements and plain elements without checking `shadowRoot` first.\n */\nexport function queryInShadow<E extends Element = Element>(host: Element, selector: string): E | null {\n return host.shadowRoot ? query<E>(host.shadowRoot, selector) : null;\n}\n\n/** Queries all matching elements inside the shadow root of a custom element. */\nexport function queryAllInShadow<E extends Element = Element>(host: Element, selector: string): E[] {\n return host.shadowRoot ? queryAll<E>(host.shadowRoot, selector) : [];\n}\n\n/**\n * Queries a shadow DOM element by its CSS `part` attribute — shorthand for\n * `queryInShadow(host, '[part=\"name\"]')`.\n *\n * @example\n * const btn = queryPart(carousel, 'prev-btn');\n * expect(btn).not.toBeNull();\n */\nexport function queryPart<E extends Element = Element>(host: Element, part: string): E | null {\n return (\n queryAllInShadow<E>(host, '[part]').find((element) => element.getAttribute('part')?.split(/\\s+/).includes(part)) ??\n null\n );\n}\n\n/**\n * Returns the light-DOM children assigned to a named slot, or every child assigned to the\n * default slot when no name is given.\n *\n * @example\n * const slides = getSlotted(carousel);\n * expect(slides).toHaveLength(3);\n */\nexport function getSlotted<E extends Element = Element>(host: Element, slotName?: string): E[] {\n return Array.from(host.children).filter((element) =>\n slotName === undefined ? !element.getAttribute('slot') : element.getAttribute('slot') === slotName,\n ) as E[];\n}\n\n// ─── API ─────────────────────────────────────────────────────────────────────\n\n/**\n * Create query helpers scoped to a DOM root — useful for light DOM and shadow roots.\n *\n * @example\n * const view = within(fixture.shadow!);\n * expect(view.get('.title').textContent).toBe('Hello');\n */\nexport function within(root: QueryRoot): QueryScope {\n return {\n get: <E extends Element = Element>(selector: string) =>\n requireMatch(query<E>(root, selector), `\"${selector}\"`, root),\n getByTestId: <E extends Element = Element>(testId: string) =>\n requireMatch(queryByTestId<E>(root, testId), `data-testid \"${testId}\"`, root),\n getByText: <E extends Element = Element>(text: string, selector = '*') =>\n requireMatch(queryByText<E>(root, text, selector), `text \"${text}\" matching \"${selector}\"`, root),\n query: <E extends Element = Element>(selector: string) => query<E>(root, selector),\n queryAll: <E extends Element = Element>(selector: string) => queryAll<E>(root, selector),\n queryAllByTestId: <E extends Element = Element>(testId: string) => queryAllByTestId<E>(root, testId),\n queryAllByText: <E extends Element = Element>(text: string, selector = '*') =>\n queryAllByText<E>(root, text, selector),\n queryByTestId: <E extends Element = Element>(testId: string) => queryByTestId<E>(root, testId),\n queryByText: <E extends Element = Element>(text: string, selector = '*') => queryByText<E>(root, text, selector),\n };\n}\n"],"mappings":"gCAuBA,IAAM,aAAgB,GAChB,aAAgB,QAAgB,EAAK,UAAU,MAAM,EAAG,GAAG,EAE3D,aAAgB,WAAmB,gBAAgB,EAAK,UAAU,MAAM,EAAG,GAAG,EAAE,gBAE7E,EAAK,SAGR,cAAmC,EAAmB,EAAqB,IAAuB,CACtG,GAAI,EAAS,OAAO,EAEpB,MAAM,IAAI,EAAA,gBAAgB,kBAAkB,EAAY,UAAU,aAAa,CAAI,EAAE,EAAE,CACzF,EAEM,OAAsC,EAAiB,IAC3D,EAAK,cAAiB,CAAQ,EAE1B,UAAyC,EAAiB,IACvD,MAAM,KAAK,EAAK,iBAAoB,CAAQ,CAAC,EAGhD,eAA8C,EAAiB,IACnE,SAAY,EAAM,eAAe,CAAC,CAAC,KAAM,GAAY,EAAQ,aAAa,aAAa,IAAM,CAAM,GAAK,KAEpG,kBAAiD,EAAiB,IACtE,SAAY,EAAM,eAAe,CAAC,CAAC,OAAQ,GAAY,EAAQ,aAAa,aAAa,IAAM,CAAM,EAEjG,aAA4C,EAAiB,EAAc,EAAW,MAAkB,CAC5G,IAAK,IAAM,KAAW,EAAK,iBAAoB,CAAQ,EACrD,GAAI,EAAQ,aAAa,KAAK,IAAM,EAAM,OAAO,EAGnD,OAAO,IACT,EAEM,gBAA+C,EAAiB,EAAc,EAAW,MAC7F,SAAY,EAAM,CAAQ,CAAC,CAAC,OAAQ,GAAY,EAAQ,aAAa,KAAK,IAAM,CAAI,EAOtF,SAAgB,cAA2C,EAAe,EAA4B,CACpG,OAAO,EAAK,WAAa,MAAS,EAAK,WAAY,CAAQ,EAAI,IACjE,CAGA,SAAgB,iBAA8C,EAAe,EAAuB,CAClG,OAAO,EAAK,WAAa,SAAY,EAAK,WAAY,CAAQ,EAAI,CAAC,CACrE,CAUA,SAAgB,UAAuC,EAAe,EAAwB,CAC5F,OACE,iBAAoB,EAAM,QAAQ,CAAC,CAAC,KAAM,GAAY,EAAQ,aAAa,MAAM,CAAC,EAAE,MAAM,KAAK,CAAC,CAAC,SAAS,CAAI,CAAC,GAC/G,IAEJ,CAUA,SAAgB,WAAwC,EAAe,EAAwB,CAC7F,OAAO,MAAM,KAAK,EAAK,QAAQ,CAAC,CAAC,OAAQ,GACvC,IAAa,IAAA,GAAY,CAAC,EAAQ,aAAa,MAAM,EAAI,EAAQ,aAAa,MAAM,IAAM,CAC5F,CACF,CAWA,SAAgB,OAAO,EAA6B,CAClD,MAAO,CACL,IAAmC,GACjC,aAAa,MAAS,EAAM,CAAQ,EAAG,IAAI,EAAS,GAAI,CAAI,EAC9D,YAA2C,GACzC,aAAa,cAAiB,EAAM,CAAM,EAAG,gBAAgB,EAAO,GAAI,CAAI,EAC9E,WAAyC,EAAc,EAAW,MAChE,aAAa,YAAe,EAAM,EAAM,CAAQ,EAAG,SAAS,EAAK,cAAc,EAAS,GAAI,CAAI,EAClG,MAAqC,GAAqB,MAAS,EAAM,CAAQ,EACjF,SAAwC,GAAqB,SAAY,EAAM,CAAQ,EACvF,iBAAgD,GAAmB,iBAAoB,EAAM,CAAM,EACnG,gBAA8C,EAAc,EAAW,MACrE,eAAkB,EAAM,EAAM,CAAQ,EACxC,cAA6C,GAAmB,cAAiB,EAAM,CAAM,EAC7F,aAA2C,EAAc,EAAW,MAAQ,YAAe,EAAM,EAAM,CAAQ,CACjH,CACF"}
1
+ {"version":3,"file":"query.cjs","names":[],"sources":["../src/query.ts"],"sourcesContent":["import { AssayQueryError } from './errors';\n\n/** DOM query helpers for test environments. */\n\n// ─── Types ───────────────────────────────────────────────────────────────────\n\n/** Scoped query helpers for any queryable DOM root: see {@link within}. */\nexport interface QueryScope {\n get<E extends Element = Element>(selector: string): E;\n getByTestId<E extends Element = Element>(testId: string): E;\n getByText<E extends Element = Element>(text: string, selector?: string): E;\n query<E extends Element = Element>(selector: string): E | null;\n queryAll<E extends Element = Element>(selector: string): E[];\n queryAllByTestId<E extends Element = Element>(testId: string): E[];\n queryAllByText<E extends Element = Element>(text: string, selector?: string): E[];\n queryByTestId<E extends Element = Element>(testId: string): E | null;\n queryByText<E extends Element = Element>(text: string, selector?: string): E | null;\n}\n\n// ─── Helpers ─────────────────────────────────────────────────────────────────\n\ntype QueryRoot = ParentNode;\n\nconst describeRoot = (root: QueryRoot): string => {\n if (root instanceof Element) return root.outerHTML.slice(0, 500);\n\n if (root instanceof ShadowRoot) return `<shadow-root>${root.innerHTML.slice(0, 500)}</shadow-root>`;\n\n return root.nodeName;\n};\n\nconst requireMatch = <E extends Element>(element: E | null, description: string, root: QueryRoot): E => {\n if (element) return element;\n\n throw new AssayQueryError(`Unable to find ${description} within ${describeRoot(root)}.`);\n};\n\nconst query = <E extends Element = Element>(root: QueryRoot, selector: string): E | null =>\n root.querySelector<E>(selector);\n\nconst queryAll = <E extends Element = Element>(root: QueryRoot, selector: string): E[] => {\n return Array.from(root.querySelectorAll<E>(selector));\n};\n\nconst queryByTestId = <E extends Element = Element>(root: QueryRoot, testId: string): E | null =>\n queryAll<E>(root, '[data-testid]').find((element) => element.getAttribute('data-testid') === testId) ?? null;\n\nconst queryAllByTestId = <E extends Element = Element>(root: QueryRoot, testId: string): E[] =>\n queryAll<E>(root, '[data-testid]').filter((element) => element.getAttribute('data-testid') === testId);\n\nconst queryByText = <E extends Element = Element>(root: QueryRoot, text: string, selector = '*'): E | null => {\n for (const element of root.querySelectorAll<E>(selector)) {\n if (element.textContent?.trim() === text) return element;\n }\n\n return null;\n};\n\nconst queryAllByText = <E extends Element = Element>(root: QueryRoot, text: string, selector = '*'): E[] =>\n queryAll<E>(root, selector).filter((element) => element.textContent?.trim() === text);\n\n/**\n * Queries the shadow root of a custom element for a matching CSS selector.\n * Returns `null` if the element has no shadow root or no match is found: safe to call on\n * both open-shadow custom elements and plain elements without checking `shadowRoot` first.\n */\nexport function queryInShadow<E extends Element = Element>(host: Element, selector: string): E | null {\n return host.shadowRoot ? query<E>(host.shadowRoot, selector) : null;\n}\n\n/** Queries all matching elements inside the shadow root of a custom element. */\nexport function queryAllInShadow<E extends Element = Element>(host: Element, selector: string): E[] {\n return host.shadowRoot ? queryAll<E>(host.shadowRoot, selector) : [];\n}\n\n/**\n * Queries a shadow DOM element by its CSS `part` attribute: shorthand for\n * `queryInShadow(host, '[part=\"name\"]')`.\n *\n * @example\n * const btn = queryPart(carousel, 'prev-btn');\n * expect(btn).not.toBeNull();\n */\nexport function queryPart<E extends Element = Element>(host: Element, part: string): E | null {\n return (\n queryAllInShadow<E>(host, '[part]').find((element) => element.getAttribute('part')?.split(/\\s+/).includes(part)) ??\n null\n );\n}\n\n/**\n * Returns the light-DOM children assigned to a named slot, or every child assigned to the\n * default slot when no name is given.\n *\n * @example\n * const slides = getSlotted(carousel);\n * expect(slides).toHaveLength(3);\n */\nexport function getSlotted<E extends Element = Element>(host: Element, slotName?: string): E[] {\n return Array.from(host.children).filter((element) =>\n slotName === undefined ? !element.getAttribute('slot') : element.getAttribute('slot') === slotName,\n ) as E[];\n}\n\n// ─── API ─────────────────────────────────────────────────────────────────────\n\n/**\n * Create query helpers scoped to a DOM root: useful for light DOM and shadow roots.\n *\n * @example\n * const view = within(fixture.shadow!);\n * expect(view.get('.title').textContent).toBe('Hello');\n */\nexport function within(root: QueryRoot): QueryScope {\n return {\n get: <E extends Element = Element>(selector: string) =>\n requireMatch(query<E>(root, selector), `\"${selector}\"`, root),\n getByTestId: <E extends Element = Element>(testId: string) =>\n requireMatch(queryByTestId<E>(root, testId), `data-testid \"${testId}\"`, root),\n getByText: <E extends Element = Element>(text: string, selector = '*') =>\n requireMatch(queryByText<E>(root, text, selector), `text \"${text}\" matching \"${selector}\"`, root),\n query: <E extends Element = Element>(selector: string) => query<E>(root, selector),\n queryAll: <E extends Element = Element>(selector: string) => queryAll<E>(root, selector),\n queryAllByTestId: <E extends Element = Element>(testId: string) => queryAllByTestId<E>(root, testId),\n queryAllByText: <E extends Element = Element>(text: string, selector = '*') =>\n queryAllByText<E>(root, text, selector),\n queryByTestId: <E extends Element = Element>(testId: string) => queryByTestId<E>(root, testId),\n queryByText: <E extends Element = Element>(text: string, selector = '*') => queryByText<E>(root, text, selector),\n };\n}\n"],"mappings":"gCAuBA,IAAM,aAAgB,GAChB,aAAgB,QAAgB,EAAK,UAAU,MAAM,EAAG,GAAG,EAE3D,aAAgB,WAAmB,gBAAgB,EAAK,UAAU,MAAM,EAAG,GAAG,EAAE,gBAE7E,EAAK,SAGR,cAAmC,EAAmB,EAAqB,IAAuB,CACtG,GAAI,EAAS,OAAO,EAEpB,MAAM,IAAI,EAAA,gBAAgB,kBAAkB,EAAY,UAAU,aAAa,CAAI,EAAE,EAAE,CACzF,EAEM,OAAsC,EAAiB,IAC3D,EAAK,cAAiB,CAAQ,EAE1B,UAAyC,EAAiB,IACvD,MAAM,KAAK,EAAK,iBAAoB,CAAQ,CAAC,EAGhD,eAA8C,EAAiB,IACnE,SAAY,EAAM,eAAe,CAAC,CAAC,KAAM,GAAY,EAAQ,aAAa,aAAa,IAAM,CAAM,GAAK,KAEpG,kBAAiD,EAAiB,IACtE,SAAY,EAAM,eAAe,CAAC,CAAC,OAAQ,GAAY,EAAQ,aAAa,aAAa,IAAM,CAAM,EAEjG,aAA4C,EAAiB,EAAc,EAAW,MAAkB,CAC5G,IAAK,IAAM,KAAW,EAAK,iBAAoB,CAAQ,EACrD,GAAI,EAAQ,aAAa,KAAK,IAAM,EAAM,OAAO,EAGnD,OAAO,IACT,EAEM,gBAA+C,EAAiB,EAAc,EAAW,MAC7F,SAAY,EAAM,CAAQ,CAAC,CAAC,OAAQ,GAAY,EAAQ,aAAa,KAAK,IAAM,CAAI,EAOtF,SAAgB,cAA2C,EAAe,EAA4B,CACpG,OAAO,EAAK,WAAa,MAAS,EAAK,WAAY,CAAQ,EAAI,IACjE,CAGA,SAAgB,iBAA8C,EAAe,EAAuB,CAClG,OAAO,EAAK,WAAa,SAAY,EAAK,WAAY,CAAQ,EAAI,CAAC,CACrE,CAUA,SAAgB,UAAuC,EAAe,EAAwB,CAC5F,OACE,iBAAoB,EAAM,QAAQ,CAAC,CAAC,KAAM,GAAY,EAAQ,aAAa,MAAM,CAAC,EAAE,MAAM,KAAK,CAAC,CAAC,SAAS,CAAI,CAAC,GAC/G,IAEJ,CAUA,SAAgB,WAAwC,EAAe,EAAwB,CAC7F,OAAO,MAAM,KAAK,EAAK,QAAQ,CAAC,CAAC,OAAQ,GACvC,IAAa,IAAA,GAAY,CAAC,EAAQ,aAAa,MAAM,EAAI,EAAQ,aAAa,MAAM,IAAM,CAC5F,CACF,CAWA,SAAgB,OAAO,EAA6B,CAClD,MAAO,CACL,IAAmC,GACjC,aAAa,MAAS,EAAM,CAAQ,EAAG,IAAI,EAAS,GAAI,CAAI,EAC9D,YAA2C,GACzC,aAAa,cAAiB,EAAM,CAAM,EAAG,gBAAgB,EAAO,GAAI,CAAI,EAC9E,WAAyC,EAAc,EAAW,MAChE,aAAa,YAAe,EAAM,EAAM,CAAQ,EAAG,SAAS,EAAK,cAAc,EAAS,GAAI,CAAI,EAClG,MAAqC,GAAqB,MAAS,EAAM,CAAQ,EACjF,SAAwC,GAAqB,SAAY,EAAM,CAAQ,EACvF,iBAAgD,GAAmB,iBAAoB,EAAM,CAAM,EACnG,gBAA8C,EAAc,EAAW,MACrE,eAAkB,EAAM,EAAM,CAAQ,EACxC,cAA6C,GAAmB,cAAiB,EAAM,CAAM,EAC7F,aAA2C,EAAc,EAAW,MAAQ,YAAe,EAAM,EAAM,CAAQ,CACjH,CACF"}
package/dist/query.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  /** DOM query helpers for test environments. */
2
- /** Scoped query helpers for any queryable DOM root — see {@link within}. */
2
+ /** Scoped query helpers for any queryable DOM root: see {@link within}. */
3
3
  export interface QueryScope {
4
4
  get<E extends Element = Element>(selector: string): E;
5
5
  getByTestId<E extends Element = Element>(testId: string): E;
@@ -14,14 +14,14 @@ export interface QueryScope {
14
14
  type QueryRoot = ParentNode;
15
15
  /**
16
16
  * Queries the shadow root of a custom element for a matching CSS selector.
17
- * Returns `null` if the element has no shadow root or no match is found — safe to call on
17
+ * Returns `null` if the element has no shadow root or no match is found: safe to call on
18
18
  * both open-shadow custom elements and plain elements without checking `shadowRoot` first.
19
19
  */
20
20
  export declare function queryInShadow<E extends Element = Element>(host: Element, selector: string): E | null;
21
21
  /** Queries all matching elements inside the shadow root of a custom element. */
22
22
  export declare function queryAllInShadow<E extends Element = Element>(host: Element, selector: string): E[];
23
23
  /**
24
- * Queries a shadow DOM element by its CSS `part` attribute — shorthand for
24
+ * Queries a shadow DOM element by its CSS `part` attribute: shorthand for
25
25
  * `queryInShadow(host, '[part="name"]')`.
26
26
  *
27
27
  * @example
@@ -39,7 +39,7 @@ export declare function queryPart<E extends Element = Element>(host: Element, pa
39
39
  */
40
40
  export declare function getSlotted<E extends Element = Element>(host: Element, slotName?: string): E[];
41
41
  /**
42
- * Create query helpers scoped to a DOM root — useful for light DOM and shadow roots.
42
+ * Create query helpers scoped to a DOM root: useful for light DOM and shadow roots.
43
43
  *
44
44
  * @example
45
45
  * const view = within(fixture.shadow!);
@@ -1 +1 @@
1
- {"version":3,"file":"query.d.ts","sourceRoot":"","sources":["../src/query.ts"],"names":[],"mappings":"AAEA,+CAA+C;AAI/C,4EAA4E;AAC5E,MAAM,WAAW,UAAU;IACzB,GAAG,CAAC,CAAC,SAAS,OAAO,GAAG,OAAO,EAAE,QAAQ,EAAE,MAAM,GAAG,CAAC,CAAC;IACtD,WAAW,CAAC,CAAC,SAAS,OAAO,GAAG,OAAO,EAAE,MAAM,EAAE,MAAM,GAAG,CAAC,CAAC;IAC5D,SAAS,CAAC,CAAC,SAAS,OAAO,GAAG,OAAO,EAAE,IAAI,EAAE,MAAM,EAAE,QAAQ,CAAC,EAAE,MAAM,GAAG,CAAC,CAAC;IAC3E,KAAK,CAAC,CAAC,SAAS,OAAO,GAAG,OAAO,EAAE,QAAQ,EAAE,MAAM,GAAG,CAAC,GAAG,IAAI,CAAC;IAC/D,QAAQ,CAAC,CAAC,SAAS,OAAO,GAAG,OAAO,EAAE,QAAQ,EAAE,MAAM,GAAG,CAAC,EAAE,CAAC;IAC7D,gBAAgB,CAAC,CAAC,SAAS,OAAO,GAAG,OAAO,EAAE,MAAM,EAAE,MAAM,GAAG,CAAC,EAAE,CAAC;IACnE,cAAc,CAAC,CAAC,SAAS,OAAO,GAAG,OAAO,EAAE,IAAI,EAAE,MAAM,EAAE,QAAQ,CAAC,EAAE,MAAM,GAAG,CAAC,EAAE,CAAC;IAClF,aAAa,CAAC,CAAC,SAAS,OAAO,GAAG,OAAO,EAAE,MAAM,EAAE,MAAM,GAAG,CAAC,GAAG,IAAI,CAAC;IACrE,WAAW,CAAC,CAAC,SAAS,OAAO,GAAG,OAAO,EAAE,IAAI,EAAE,MAAM,EAAE,QAAQ,CAAC,EAAE,MAAM,GAAG,CAAC,GAAG,IAAI,CAAC;CACrF;AAID,KAAK,SAAS,GAAG,UAAU,CAAC;AAwC5B;;;;GAIG;AACH,wBAAgB,aAAa,CAAC,CAAC,SAAS,OAAO,GAAG,OAAO,EAAE,IAAI,EAAE,OAAO,EAAE,QAAQ,EAAE,MAAM,GAAG,CAAC,GAAG,IAAI,CAEpG;AAED,gFAAgF;AAChF,wBAAgB,gBAAgB,CAAC,CAAC,SAAS,OAAO,GAAG,OAAO,EAAE,IAAI,EAAE,OAAO,EAAE,QAAQ,EAAE,MAAM,GAAG,CAAC,EAAE,CAElG;AAED;;;;;;;GAOG;AACH,wBAAgB,SAAS,CAAC,CAAC,SAAS,OAAO,GAAG,OAAO,EAAE,IAAI,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,GAAG,CAAC,GAAG,IAAI,CAK5F;AAED;;;;;;;GAOG;AACH,wBAAgB,UAAU,CAAC,CAAC,SAAS,OAAO,GAAG,OAAO,EAAE,IAAI,EAAE,OAAO,EAAE,QAAQ,CAAC,EAAE,MAAM,GAAG,CAAC,EAAE,CAI7F;AAID;;;;;;GAMG;AACH,wBAAgB,MAAM,CAAC,IAAI,EAAE,SAAS,GAAG,UAAU,CAgBlD"}
1
+ {"version":3,"file":"query.d.ts","sourceRoot":"","sources":["../src/query.ts"],"names":[],"mappings":"AAEA,+CAA+C;AAI/C,2EAA2E;AAC3E,MAAM,WAAW,UAAU;IACzB,GAAG,CAAC,CAAC,SAAS,OAAO,GAAG,OAAO,EAAE,QAAQ,EAAE,MAAM,GAAG,CAAC,CAAC;IACtD,WAAW,CAAC,CAAC,SAAS,OAAO,GAAG,OAAO,EAAE,MAAM,EAAE,MAAM,GAAG,CAAC,CAAC;IAC5D,SAAS,CAAC,CAAC,SAAS,OAAO,GAAG,OAAO,EAAE,IAAI,EAAE,MAAM,EAAE,QAAQ,CAAC,EAAE,MAAM,GAAG,CAAC,CAAC;IAC3E,KAAK,CAAC,CAAC,SAAS,OAAO,GAAG,OAAO,EAAE,QAAQ,EAAE,MAAM,GAAG,CAAC,GAAG,IAAI,CAAC;IAC/D,QAAQ,CAAC,CAAC,SAAS,OAAO,GAAG,OAAO,EAAE,QAAQ,EAAE,MAAM,GAAG,CAAC,EAAE,CAAC;IAC7D,gBAAgB,CAAC,CAAC,SAAS,OAAO,GAAG,OAAO,EAAE,MAAM,EAAE,MAAM,GAAG,CAAC,EAAE,CAAC;IACnE,cAAc,CAAC,CAAC,SAAS,OAAO,GAAG,OAAO,EAAE,IAAI,EAAE,MAAM,EAAE,QAAQ,CAAC,EAAE,MAAM,GAAG,CAAC,EAAE,CAAC;IAClF,aAAa,CAAC,CAAC,SAAS,OAAO,GAAG,OAAO,EAAE,MAAM,EAAE,MAAM,GAAG,CAAC,GAAG,IAAI,CAAC;IACrE,WAAW,CAAC,CAAC,SAAS,OAAO,GAAG,OAAO,EAAE,IAAI,EAAE,MAAM,EAAE,QAAQ,CAAC,EAAE,MAAM,GAAG,CAAC,GAAG,IAAI,CAAC;CACrF;AAID,KAAK,SAAS,GAAG,UAAU,CAAC;AAwC5B;;;;GAIG;AACH,wBAAgB,aAAa,CAAC,CAAC,SAAS,OAAO,GAAG,OAAO,EAAE,IAAI,EAAE,OAAO,EAAE,QAAQ,EAAE,MAAM,GAAG,CAAC,GAAG,IAAI,CAEpG;AAED,gFAAgF;AAChF,wBAAgB,gBAAgB,CAAC,CAAC,SAAS,OAAO,GAAG,OAAO,EAAE,IAAI,EAAE,OAAO,EAAE,QAAQ,EAAE,MAAM,GAAG,CAAC,EAAE,CAElG;AAED;;;;;;;GAOG;AACH,wBAAgB,SAAS,CAAC,CAAC,SAAS,OAAO,GAAG,OAAO,EAAE,IAAI,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,GAAG,CAAC,GAAG,IAAI,CAK5F;AAED;;;;;;;GAOG;AACH,wBAAgB,UAAU,CAAC,CAAC,SAAS,OAAO,GAAG,OAAO,EAAE,IAAI,EAAE,OAAO,EAAE,QAAQ,CAAC,EAAE,MAAM,GAAG,CAAC,EAAE,CAI7F;AAID;;;;;;GAMG;AACH,wBAAgB,MAAM,CAAC,IAAI,EAAE,SAAS,GAAG,UAAU,CAgBlD"}
package/dist/query.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"query.js","names":[],"sources":["../src/query.ts"],"sourcesContent":["import { AssayQueryError } from './errors';\n\n/** DOM query helpers for test environments. */\n\n// ─── Types ───────────────────────────────────────────────────────────────────\n\n/** Scoped query helpers for any queryable DOM root — see {@link within}. */\nexport interface QueryScope {\n get<E extends Element = Element>(selector: string): E;\n getByTestId<E extends Element = Element>(testId: string): E;\n getByText<E extends Element = Element>(text: string, selector?: string): E;\n query<E extends Element = Element>(selector: string): E | null;\n queryAll<E extends Element = Element>(selector: string): E[];\n queryAllByTestId<E extends Element = Element>(testId: string): E[];\n queryAllByText<E extends Element = Element>(text: string, selector?: string): E[];\n queryByTestId<E extends Element = Element>(testId: string): E | null;\n queryByText<E extends Element = Element>(text: string, selector?: string): E | null;\n}\n\n// ─── Helpers ─────────────────────────────────────────────────────────────────\n\ntype QueryRoot = ParentNode;\n\nconst describeRoot = (root: QueryRoot): string => {\n if (root instanceof Element) return root.outerHTML.slice(0, 500);\n\n if (root instanceof ShadowRoot) return `<shadow-root>${root.innerHTML.slice(0, 500)}</shadow-root>`;\n\n return root.nodeName;\n};\n\nconst requireMatch = <E extends Element>(element: E | null, description: string, root: QueryRoot): E => {\n if (element) return element;\n\n throw new AssayQueryError(`Unable to find ${description} within ${describeRoot(root)}.`);\n};\n\nconst query = <E extends Element = Element>(root: QueryRoot, selector: string): E | null =>\n root.querySelector<E>(selector);\n\nconst queryAll = <E extends Element = Element>(root: QueryRoot, selector: string): E[] => {\n return Array.from(root.querySelectorAll<E>(selector));\n};\n\nconst queryByTestId = <E extends Element = Element>(root: QueryRoot, testId: string): E | null =>\n queryAll<E>(root, '[data-testid]').find((element) => element.getAttribute('data-testid') === testId) ?? null;\n\nconst queryAllByTestId = <E extends Element = Element>(root: QueryRoot, testId: string): E[] =>\n queryAll<E>(root, '[data-testid]').filter((element) => element.getAttribute('data-testid') === testId);\n\nconst queryByText = <E extends Element = Element>(root: QueryRoot, text: string, selector = '*'): E | null => {\n for (const element of root.querySelectorAll<E>(selector)) {\n if (element.textContent?.trim() === text) return element;\n }\n\n return null;\n};\n\nconst queryAllByText = <E extends Element = Element>(root: QueryRoot, text: string, selector = '*'): E[] =>\n queryAll<E>(root, selector).filter((element) => element.textContent?.trim() === text);\n\n/**\n * Queries the shadow root of a custom element for a matching CSS selector.\n * Returns `null` if the element has no shadow root or no match is found — safe to call on\n * both open-shadow custom elements and plain elements without checking `shadowRoot` first.\n */\nexport function queryInShadow<E extends Element = Element>(host: Element, selector: string): E | null {\n return host.shadowRoot ? query<E>(host.shadowRoot, selector) : null;\n}\n\n/** Queries all matching elements inside the shadow root of a custom element. */\nexport function queryAllInShadow<E extends Element = Element>(host: Element, selector: string): E[] {\n return host.shadowRoot ? queryAll<E>(host.shadowRoot, selector) : [];\n}\n\n/**\n * Queries a shadow DOM element by its CSS `part` attribute — shorthand for\n * `queryInShadow(host, '[part=\"name\"]')`.\n *\n * @example\n * const btn = queryPart(carousel, 'prev-btn');\n * expect(btn).not.toBeNull();\n */\nexport function queryPart<E extends Element = Element>(host: Element, part: string): E | null {\n return (\n queryAllInShadow<E>(host, '[part]').find((element) => element.getAttribute('part')?.split(/\\s+/).includes(part)) ??\n null\n );\n}\n\n/**\n * Returns the light-DOM children assigned to a named slot, or every child assigned to the\n * default slot when no name is given.\n *\n * @example\n * const slides = getSlotted(carousel);\n * expect(slides).toHaveLength(3);\n */\nexport function getSlotted<E extends Element = Element>(host: Element, slotName?: string): E[] {\n return Array.from(host.children).filter((element) =>\n slotName === undefined ? !element.getAttribute('slot') : element.getAttribute('slot') === slotName,\n ) as E[];\n}\n\n// ─── API ─────────────────────────────────────────────────────────────────────\n\n/**\n * Create query helpers scoped to a DOM root — useful for light DOM and shadow roots.\n *\n * @example\n * const view = within(fixture.shadow!);\n * expect(view.get('.title').textContent).toBe('Hello');\n */\nexport function within(root: QueryRoot): QueryScope {\n return {\n get: <E extends Element = Element>(selector: string) =>\n requireMatch(query<E>(root, selector), `\"${selector}\"`, root),\n getByTestId: <E extends Element = Element>(testId: string) =>\n requireMatch(queryByTestId<E>(root, testId), `data-testid \"${testId}\"`, root),\n getByText: <E extends Element = Element>(text: string, selector = '*') =>\n requireMatch(queryByText<E>(root, text, selector), `text \"${text}\" matching \"${selector}\"`, root),\n query: <E extends Element = Element>(selector: string) => query<E>(root, selector),\n queryAll: <E extends Element = Element>(selector: string) => queryAll<E>(root, selector),\n queryAllByTestId: <E extends Element = Element>(testId: string) => queryAllByTestId<E>(root, testId),\n queryAllByText: <E extends Element = Element>(text: string, selector = '*') =>\n queryAllByText<E>(root, text, selector),\n queryByTestId: <E extends Element = Element>(testId: string) => queryByTestId<E>(root, testId),\n queryByText: <E extends Element = Element>(text: string, selector = '*') => queryByText<E>(root, text, selector),\n };\n}\n"],"mappings":"8CAuBA,IAAM,aAAgB,GAChB,aAAgB,QAAgB,EAAK,UAAU,MAAM,EAAG,GAAG,EAE3D,aAAgB,WAAmB,gBAAgB,EAAK,UAAU,MAAM,EAAG,GAAG,EAAE,gBAE7E,EAAK,SAGR,cAAmC,EAAmB,EAAqB,IAAuB,CACtG,GAAI,EAAS,OAAO,EAEpB,MAAM,IAAI,EAAgB,kBAAkB,EAAY,UAAU,aAAa,CAAI,EAAE,EAAE,CACzF,EAEM,OAAsC,EAAiB,IAC3D,EAAK,cAAiB,CAAQ,EAE1B,UAAyC,EAAiB,IACvD,MAAM,KAAK,EAAK,iBAAoB,CAAQ,CAAC,EAGhD,eAA8C,EAAiB,IACnE,SAAY,EAAM,eAAe,CAAC,CAAC,KAAM,GAAY,EAAQ,aAAa,aAAa,IAAM,CAAM,GAAK,KAEpG,kBAAiD,EAAiB,IACtE,SAAY,EAAM,eAAe,CAAC,CAAC,OAAQ,GAAY,EAAQ,aAAa,aAAa,IAAM,CAAM,EAEjG,aAA4C,EAAiB,EAAc,EAAW,MAAkB,CAC5G,IAAK,IAAM,KAAW,EAAK,iBAAoB,CAAQ,EACrD,GAAI,EAAQ,aAAa,KAAK,IAAM,EAAM,OAAO,EAGnD,OAAO,IACT,EAEM,gBAA+C,EAAiB,EAAc,EAAW,MAC7F,SAAY,EAAM,CAAQ,CAAC,CAAC,OAAQ,GAAY,EAAQ,aAAa,KAAK,IAAM,CAAI,EAOtF,SAAgB,cAA2C,EAAe,EAA4B,CACpG,OAAO,EAAK,WAAa,MAAS,EAAK,WAAY,CAAQ,EAAI,IACjE,CAGA,SAAgB,iBAA8C,EAAe,EAAuB,CAClG,OAAO,EAAK,WAAa,SAAY,EAAK,WAAY,CAAQ,EAAI,CAAC,CACrE,CAUA,SAAgB,UAAuC,EAAe,EAAwB,CAC5F,OACE,iBAAoB,EAAM,QAAQ,CAAC,CAAC,KAAM,GAAY,EAAQ,aAAa,MAAM,CAAC,EAAE,MAAM,KAAK,CAAC,CAAC,SAAS,CAAI,CAAC,GAC/G,IAEJ,CAUA,SAAgB,WAAwC,EAAe,EAAwB,CAC7F,OAAO,MAAM,KAAK,EAAK,QAAQ,CAAC,CAAC,OAAQ,GACvC,IAAa,IAAA,GAAY,CAAC,EAAQ,aAAa,MAAM,EAAI,EAAQ,aAAa,MAAM,IAAM,CAC5F,CACF,CAWA,SAAgB,OAAO,EAA6B,CAClD,MAAO,CACL,IAAmC,GACjC,aAAa,MAAS,EAAM,CAAQ,EAAG,IAAI,EAAS,GAAI,CAAI,EAC9D,YAA2C,GACzC,aAAa,cAAiB,EAAM,CAAM,EAAG,gBAAgB,EAAO,GAAI,CAAI,EAC9E,WAAyC,EAAc,EAAW,MAChE,aAAa,YAAe,EAAM,EAAM,CAAQ,EAAG,SAAS,EAAK,cAAc,EAAS,GAAI,CAAI,EAClG,MAAqC,GAAqB,MAAS,EAAM,CAAQ,EACjF,SAAwC,GAAqB,SAAY,EAAM,CAAQ,EACvF,iBAAgD,GAAmB,iBAAoB,EAAM,CAAM,EACnG,gBAA8C,EAAc,EAAW,MACrE,eAAkB,EAAM,EAAM,CAAQ,EACxC,cAA6C,GAAmB,cAAiB,EAAM,CAAM,EAC7F,aAA2C,EAAc,EAAW,MAAQ,YAAe,EAAM,EAAM,CAAQ,CACjH,CACF"}
1
+ {"version":3,"file":"query.js","names":[],"sources":["../src/query.ts"],"sourcesContent":["import { AssayQueryError } from './errors';\n\n/** DOM query helpers for test environments. */\n\n// ─── Types ───────────────────────────────────────────────────────────────────\n\n/** Scoped query helpers for any queryable DOM root: see {@link within}. */\nexport interface QueryScope {\n get<E extends Element = Element>(selector: string): E;\n getByTestId<E extends Element = Element>(testId: string): E;\n getByText<E extends Element = Element>(text: string, selector?: string): E;\n query<E extends Element = Element>(selector: string): E | null;\n queryAll<E extends Element = Element>(selector: string): E[];\n queryAllByTestId<E extends Element = Element>(testId: string): E[];\n queryAllByText<E extends Element = Element>(text: string, selector?: string): E[];\n queryByTestId<E extends Element = Element>(testId: string): E | null;\n queryByText<E extends Element = Element>(text: string, selector?: string): E | null;\n}\n\n// ─── Helpers ─────────────────────────────────────────────────────────────────\n\ntype QueryRoot = ParentNode;\n\nconst describeRoot = (root: QueryRoot): string => {\n if (root instanceof Element) return root.outerHTML.slice(0, 500);\n\n if (root instanceof ShadowRoot) return `<shadow-root>${root.innerHTML.slice(0, 500)}</shadow-root>`;\n\n return root.nodeName;\n};\n\nconst requireMatch = <E extends Element>(element: E | null, description: string, root: QueryRoot): E => {\n if (element) return element;\n\n throw new AssayQueryError(`Unable to find ${description} within ${describeRoot(root)}.`);\n};\n\nconst query = <E extends Element = Element>(root: QueryRoot, selector: string): E | null =>\n root.querySelector<E>(selector);\n\nconst queryAll = <E extends Element = Element>(root: QueryRoot, selector: string): E[] => {\n return Array.from(root.querySelectorAll<E>(selector));\n};\n\nconst queryByTestId = <E extends Element = Element>(root: QueryRoot, testId: string): E | null =>\n queryAll<E>(root, '[data-testid]').find((element) => element.getAttribute('data-testid') === testId) ?? null;\n\nconst queryAllByTestId = <E extends Element = Element>(root: QueryRoot, testId: string): E[] =>\n queryAll<E>(root, '[data-testid]').filter((element) => element.getAttribute('data-testid') === testId);\n\nconst queryByText = <E extends Element = Element>(root: QueryRoot, text: string, selector = '*'): E | null => {\n for (const element of root.querySelectorAll<E>(selector)) {\n if (element.textContent?.trim() === text) return element;\n }\n\n return null;\n};\n\nconst queryAllByText = <E extends Element = Element>(root: QueryRoot, text: string, selector = '*'): E[] =>\n queryAll<E>(root, selector).filter((element) => element.textContent?.trim() === text);\n\n/**\n * Queries the shadow root of a custom element for a matching CSS selector.\n * Returns `null` if the element has no shadow root or no match is found: safe to call on\n * both open-shadow custom elements and plain elements without checking `shadowRoot` first.\n */\nexport function queryInShadow<E extends Element = Element>(host: Element, selector: string): E | null {\n return host.shadowRoot ? query<E>(host.shadowRoot, selector) : null;\n}\n\n/** Queries all matching elements inside the shadow root of a custom element. */\nexport function queryAllInShadow<E extends Element = Element>(host: Element, selector: string): E[] {\n return host.shadowRoot ? queryAll<E>(host.shadowRoot, selector) : [];\n}\n\n/**\n * Queries a shadow DOM element by its CSS `part` attribute: shorthand for\n * `queryInShadow(host, '[part=\"name\"]')`.\n *\n * @example\n * const btn = queryPart(carousel, 'prev-btn');\n * expect(btn).not.toBeNull();\n */\nexport function queryPart<E extends Element = Element>(host: Element, part: string): E | null {\n return (\n queryAllInShadow<E>(host, '[part]').find((element) => element.getAttribute('part')?.split(/\\s+/).includes(part)) ??\n null\n );\n}\n\n/**\n * Returns the light-DOM children assigned to a named slot, or every child assigned to the\n * default slot when no name is given.\n *\n * @example\n * const slides = getSlotted(carousel);\n * expect(slides).toHaveLength(3);\n */\nexport function getSlotted<E extends Element = Element>(host: Element, slotName?: string): E[] {\n return Array.from(host.children).filter((element) =>\n slotName === undefined ? !element.getAttribute('slot') : element.getAttribute('slot') === slotName,\n ) as E[];\n}\n\n// ─── API ─────────────────────────────────────────────────────────────────────\n\n/**\n * Create query helpers scoped to a DOM root: useful for light DOM and shadow roots.\n *\n * @example\n * const view = within(fixture.shadow!);\n * expect(view.get('.title').textContent).toBe('Hello');\n */\nexport function within(root: QueryRoot): QueryScope {\n return {\n get: <E extends Element = Element>(selector: string) =>\n requireMatch(query<E>(root, selector), `\"${selector}\"`, root),\n getByTestId: <E extends Element = Element>(testId: string) =>\n requireMatch(queryByTestId<E>(root, testId), `data-testid \"${testId}\"`, root),\n getByText: <E extends Element = Element>(text: string, selector = '*') =>\n requireMatch(queryByText<E>(root, text, selector), `text \"${text}\" matching \"${selector}\"`, root),\n query: <E extends Element = Element>(selector: string) => query<E>(root, selector),\n queryAll: <E extends Element = Element>(selector: string) => queryAll<E>(root, selector),\n queryAllByTestId: <E extends Element = Element>(testId: string) => queryAllByTestId<E>(root, testId),\n queryAllByText: <E extends Element = Element>(text: string, selector = '*') =>\n queryAllByText<E>(root, text, selector),\n queryByTestId: <E extends Element = Element>(testId: string) => queryByTestId<E>(root, testId),\n queryByText: <E extends Element = Element>(text: string, selector = '*') => queryByText<E>(root, text, selector),\n };\n}\n"],"mappings":"8CAuBA,IAAM,aAAgB,GAChB,aAAgB,QAAgB,EAAK,UAAU,MAAM,EAAG,GAAG,EAE3D,aAAgB,WAAmB,gBAAgB,EAAK,UAAU,MAAM,EAAG,GAAG,EAAE,gBAE7E,EAAK,SAGR,cAAmC,EAAmB,EAAqB,IAAuB,CACtG,GAAI,EAAS,OAAO,EAEpB,MAAM,IAAI,EAAgB,kBAAkB,EAAY,UAAU,aAAa,CAAI,EAAE,EAAE,CACzF,EAEM,OAAsC,EAAiB,IAC3D,EAAK,cAAiB,CAAQ,EAE1B,UAAyC,EAAiB,IACvD,MAAM,KAAK,EAAK,iBAAoB,CAAQ,CAAC,EAGhD,eAA8C,EAAiB,IACnE,SAAY,EAAM,eAAe,CAAC,CAAC,KAAM,GAAY,EAAQ,aAAa,aAAa,IAAM,CAAM,GAAK,KAEpG,kBAAiD,EAAiB,IACtE,SAAY,EAAM,eAAe,CAAC,CAAC,OAAQ,GAAY,EAAQ,aAAa,aAAa,IAAM,CAAM,EAEjG,aAA4C,EAAiB,EAAc,EAAW,MAAkB,CAC5G,IAAK,IAAM,KAAW,EAAK,iBAAoB,CAAQ,EACrD,GAAI,EAAQ,aAAa,KAAK,IAAM,EAAM,OAAO,EAGnD,OAAO,IACT,EAEM,gBAA+C,EAAiB,EAAc,EAAW,MAC7F,SAAY,EAAM,CAAQ,CAAC,CAAC,OAAQ,GAAY,EAAQ,aAAa,KAAK,IAAM,CAAI,EAOtF,SAAgB,cAA2C,EAAe,EAA4B,CACpG,OAAO,EAAK,WAAa,MAAS,EAAK,WAAY,CAAQ,EAAI,IACjE,CAGA,SAAgB,iBAA8C,EAAe,EAAuB,CAClG,OAAO,EAAK,WAAa,SAAY,EAAK,WAAY,CAAQ,EAAI,CAAC,CACrE,CAUA,SAAgB,UAAuC,EAAe,EAAwB,CAC5F,OACE,iBAAoB,EAAM,QAAQ,CAAC,CAAC,KAAM,GAAY,EAAQ,aAAa,MAAM,CAAC,EAAE,MAAM,KAAK,CAAC,CAAC,SAAS,CAAI,CAAC,GAC/G,IAEJ,CAUA,SAAgB,WAAwC,EAAe,EAAwB,CAC7F,OAAO,MAAM,KAAK,EAAK,QAAQ,CAAC,CAAC,OAAQ,GACvC,IAAa,IAAA,GAAY,CAAC,EAAQ,aAAa,MAAM,EAAI,EAAQ,aAAa,MAAM,IAAM,CAC5F,CACF,CAWA,SAAgB,OAAO,EAA6B,CAClD,MAAO,CACL,IAAmC,GACjC,aAAa,MAAS,EAAM,CAAQ,EAAG,IAAI,EAAS,GAAI,CAAI,EAC9D,YAA2C,GACzC,aAAa,cAAiB,EAAM,CAAM,EAAG,gBAAgB,EAAO,GAAI,CAAI,EAC9E,WAAyC,EAAc,EAAW,MAChE,aAAa,YAAe,EAAM,EAAM,CAAQ,EAAG,SAAS,EAAK,cAAc,EAAS,GAAI,CAAI,EAClG,MAAqC,GAAqB,MAAS,EAAM,CAAQ,EACjF,SAAwC,GAAqB,SAAY,EAAM,CAAQ,EACvF,iBAAgD,GAAmB,iBAAoB,EAAM,CAAM,EACnG,gBAA8C,EAAc,EAAW,MACrE,eAAkB,EAAM,EAAM,CAAQ,EACxC,cAA6C,GAAmB,cAAiB,EAAM,CAAM,EAC7F,aAA2C,EAAc,EAAW,MAAQ,YAAe,EAAM,EAAM,CAAQ,CACjH,CACF"}
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@vielzeug/assay",
3
- "version": "26.10.0",
4
- "description": "Framework-agnostic DOM testing primitives — scoped queries, event dispatch, async waiting",
3
+ "version": "26.10.5",
4
+ "description": "Framework-agnostic DOM testing primitives: scoped queries, event dispatch, async waiting",
5
5
  "repository": {
6
6
  "type": "git",
7
7
  "url": "https://github.com/helmuthdu/vielzeug",
@@ -41,7 +41,7 @@
41
41
  "verify:exports": "node verify-exports.mjs"
42
42
  },
43
43
  "dependencies": {
44
- "@vielzeug/arsenal": "26.10.0"
44
+ "@vielzeug/arsenal": "26.10.5"
45
45
  },
46
46
  "devDependencies": {
47
47
  "@types/node": "^26.6.2",