@vielzeug/assay 3.0.0 → 26.10.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/wait.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"wait.js","names":[],"sources":["../src/wait.ts"],"sourcesContent":["import { abortError } 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 sleep = (ms: number, signal?: AbortSignal): Promise<void> =>\n new Promise((resolve, reject) => {\n if (signal?.aborted) {\n reject(abortError(signal, 'The operation was aborted.'));\n return;\n }\n\n const timer = setTimeout(done, ms);\n\n function done(): void {\n signal?.removeEventListener('abort', abort);\n resolve();\n }\n\n function abort(): void {\n clearTimeout(timer);\n reject(abortError(signal, 'The operation was aborted.'));\n }\n\n signal?.addEventListener('abort', abort, { once: 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 const nextTick = (): Promise<void> => new Promise((resolve) => queueMicrotask(resolve));\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":"wait.js","names":[],"sources":["../src/wait.ts"],"sourcesContent":["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/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@vielzeug/assay",
3
- "version": "3.0.0",
4
- "description": "Framework-agnostic DOM testing primitives — scoped queries, event dispatch, and async waiting",
3
+ "version": "26.10.1",
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,13 +41,13 @@
41
41
  "verify:exports": "node verify-exports.mjs"
42
42
  },
43
43
  "dependencies": {
44
- "@vielzeug/arsenal": "3.0.0"
44
+ "@vielzeug/arsenal": "26.10.1"
45
45
  },
46
46
  "devDependencies": {
47
- "@types/node": "^26.4.1",
48
- "jsdom": "^30.0.1",
47
+ "@types/node": "^26.6.2",
48
+ "jsdom": "^30.1.1",
49
49
  "typescript": "^6.0.3",
50
- "vite": "^8.2.2",
51
- "vitest": "^5.0.0"
50
+ "vite": "^8.3.0",
51
+ "vitest": "^5.0.1"
52
52
  }
53
53
  }
@@ -1,2 +0,0 @@
1
- const e=require("./errors.cjs"),t=require("./wait.cjs");var n={alert:`assertive`,log:`polite`,marquee:`off`,status:`polite`,timer:`off`},r=`[aria-live], [role="alert"], [role="log"], [role="marquee"], [role="status"], [role="timer"]`;function i(e){let t=e.getAttribute(`aria-live`);if(t)return t;let r=e.getAttribute(`role`);return r&&r in n?n[r]:null}function a(e){let t=Array.from(e.querySelectorAll(r));return e.matches?.(r)?[e,...t]:t}function o(e,t,n){return i(e)===t&&(n===void 0||e.getAttribute(`role`)===n)}function s(e={}){let t=e.document??(typeof document<`u`?document:null),n=e.politeness??`polite`,r=e.root??t?.body??null;return r?a(r).find(t=>o(t,n,e.role))??null:null}function c(e={}){let t=e.document??(typeof document<`u`?document:null),n=e.politeness??`polite`,r=e.root??t?.body??null;return r?a(r).filter(t=>o(t,n,e.role)):[]}async function l(n,r={}){let i=r.politeness??`polite`,a=null;return await t.eventually(()=>{let t=c({document:r.document,politeness:i,role:r.role,root:r.root}),o=t.find(e=>e.textContent?.includes(n));if(o){a=o;return}throw t.length===0?new e.AssayQueryError(`No ${i} live region found.`):new e.AssayQueryError(`No ${i} live region includes "${n}".`)},{interval:r.interval,message:`waitForLiveRegion("${n}")`,signal:r.signal,timeout:r.timeout}),a}async function u(n={}){let r=n.politeness??`polite`;await t.eventually(()=>{let t=c({document:n.document,politeness:r,role:n.role,root:n.root});if(!t.some(e=>e.textContent===``))throw t.length===0?new e.AssayQueryError(`No ${r} live region found.`):new e.AssayQueryError(`No ${r} live region is cleared.`)},{interval:n.interval,message:`waitForLiveRegionCleared`,signal:n.signal,timeout:n.timeout})}exports.queryAllLiveRegions=c,exports.queryLiveRegion=s,exports.waitForLiveRegion=l,exports.waitForLiveRegionCleared=u;
2
- //# sourceMappingURL=live-region.cjs.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"live-region.cjs","names":[],"sources":["../src/live-region.ts"],"sourcesContent":["import { AssayQueryError } from './errors';\nimport { eventually, type WaitOptions } from './wait';\n\n/** Politeness levels for ARIA live regions. */\nexport type LiveRegionPoliteness = 'assertive' | 'off' | 'polite';\n\n/**\n * ARIA roles that carry an implicit `aria-live` value per the WAI-ARIA spec.\n * `alert` is assertive; `log` and `status` are polite; `marquee` and `timer` are\n * off. Elements with these roles are live regions without explicit `aria-live`.\n */\nconst IMPLICIT_ROLE_POLITENESS: Record<string, LiveRegionPoliteness> = {\n alert: 'assertive',\n log: 'polite',\n marquee: 'off',\n status: 'polite',\n timer: 'off',\n};\nconst LIVE_REGION_SELECTOR =\n '[aria-live], [role=\"alert\"], [role=\"log\"], [role=\"marquee\"], [role=\"status\"], [role=\"timer\"]';\n\nexport interface LiveRegionQueryOptions {\n /** The document to search. Defaults to the global `document`. */\n document?: Document;\n /** Politeness to match via `aria-live` (or implicit role). Defaults to `'polite'`. */\n politeness?: LiveRegionPoliteness;\n /** ARIA role to match (e.g. `'status'`, `'alert'`). Omit to match any role. */\n role?: string;\n /** Root to scope the query. Defaults to `document.body`. */\n root?: ParentNode;\n}\n\n/**\n * Resolves the effective politeness of an element: explicit `aria-live` wins,\n * otherwise falls back to the implicit value from `role=\"status\"`/`role=\"alert\"`.\n * Returns `null` when the element is not a live region.\n */\nfunction resolvePoliteness(el: HTMLElement): LiveRegionPoliteness | null {\n const explicit = el.getAttribute('aria-live');\n\n if (explicit) return explicit as LiveRegionPoliteness;\n\n const role = el.getAttribute('role');\n\n if (role && role in IMPLICIT_ROLE_POLITENESS) return IMPLICIT_ROLE_POLITENESS[role];\n\n return null;\n}\n\nfunction candidates(root: ParentNode): HTMLElement[] {\n const descendants = Array.from(root.querySelectorAll<HTMLElement>(LIVE_REGION_SELECTOR));\n const rootElement = root as ParentNode & { matches?: (selector: string) => boolean };\n\n return rootElement.matches?.(LIVE_REGION_SELECTOR) ? [root as HTMLElement, ...descendants] : descendants;\n}\n\nfunction matchesOptions(element: HTMLElement, politeness: LiveRegionPoliteness, role?: string): boolean {\n return resolvePoliteness(element) === politeness && (role === undefined || element.getAttribute('role') === role);\n}\n\n/**\n * Queries the first live region matching the given politeness (and optional role)\n * within `root`. Returns `null` when none exists — safe to call before the region\n * has been lazily created.\n *\n * Matches explicit `aria-live` attributes and implicit live-region roles.\n *\n * @example\n * const region = queryLiveRegion({ politeness: 'polite' });\n * expect(region).not.toBeNull();\n */\nexport function queryLiveRegion(options: LiveRegionQueryOptions = {}): HTMLElement | null {\n const doc = options.document ?? (typeof document !== 'undefined' ? document : null);\n const politeness = options.politeness ?? 'polite';\n const root = options.root ?? doc?.body ?? null;\n\n if (!root) return null;\n\n return candidates(root).find((element) => matchesOptions(element, politeness, options.role)) ?? null;\n}\n\n/**\n * Queries all live regions matching the given politeness (and optional role)\n * within `root`. Useful for asserting no duplicate regions were created.\n *\n * @example\n * expect(queryAllLiveRegions({ politeness: 'polite' })).toHaveLength(1);\n */\nexport function queryAllLiveRegions(options: LiveRegionQueryOptions = {}): HTMLElement[] {\n const doc = options.document ?? (typeof document !== 'undefined' ? document : null);\n const politeness = options.politeness ?? 'polite';\n const root = options.root ?? doc?.body ?? null;\n\n if (!root) return [];\n\n return candidates(root).filter((element) => matchesOptions(element, politeness, options.role));\n}\n\nexport interface WaitForLiveRegionOptions extends WaitOptions {\n /** The document to search. Defaults to the global `document`. */\n document?: Document;\n /** Politeness to match via `aria-live`. Defaults to `'polite'`. */\n politeness?: LiveRegionPoliteness;\n /** ARIA role to match. Omit to match any role. */\n role?: string;\n /** Root to scope the query. Defaults to `document.body`. */\n root?: ParentNode;\n}\n\n/**\n * Retries until a live region matching the politeness/role exists and contains\n * `text` (substring match). Throws `AssayTimeoutError` on timeout.\n *\n * Handles the clear-then-set announce pattern: the region may briefly be empty\n * before the message is written, so polling until the text appears is the\n * correct approach rather than a single snapshot assertion.\n *\n * @example\n * await waitForLiveRegion('3 results found');\n * await waitForLiveRegion('Session expired', { politeness: 'assertive' });\n */\nexport async function waitForLiveRegion(text: string, options: WaitForLiveRegionOptions = {}): Promise<HTMLElement> {\n const politeness = options.politeness ?? 'polite';\n let matched: HTMLElement | null = null;\n\n await eventually(\n () => {\n const regions = queryAllLiveRegions({\n document: options.document,\n politeness,\n role: options.role,\n root: options.root,\n });\n const region = regions.find((candidate) => candidate.textContent?.includes(text));\n\n if (region) {\n matched = region;\n return;\n }\n\n if (regions.length === 0) throw new AssayQueryError(`No ${politeness} live region found.`);\n throw new AssayQueryError(`No ${politeness} live region includes \"${text}\".`);\n },\n {\n interval: options.interval,\n message: `waitForLiveRegion(\"${text}\")`,\n signal: options.signal,\n timeout: options.timeout,\n },\n );\n\n // `eventually` only resolves once the assertion stops throwing, so `matched` is set.\n return matched!;\n}\n\n/**\n * Retries until a live region matching the politeness/role exists and is empty\n * (cleared). Throws `AssayTimeoutError` on timeout.\n *\n * @example\n * await waitForLiveRegionCleared();\n */\nexport async function waitForLiveRegionCleared(options: WaitForLiveRegionOptions = {}): Promise<void> {\n const politeness = options.politeness ?? 'polite';\n\n await eventually(\n () => {\n const regions = queryAllLiveRegions({\n document: options.document,\n politeness,\n role: options.role,\n root: options.root,\n });\n\n if (regions.some((region) => region.textContent === '')) return;\n if (regions.length === 0) throw new AssayQueryError(`No ${politeness} live region found.`);\n throw new AssayQueryError(`No ${politeness} live region is cleared.`);\n },\n {\n interval: options.interval,\n message: 'waitForLiveRegionCleared',\n signal: options.signal,\n timeout: options.timeout,\n },\n );\n}\n"],"mappings":"wDAWA,IAAM,EAAiE,CACrE,MAAO,YACP,IAAK,SACL,QAAS,MACT,OAAQ,SACR,MAAO,KACT,EACM,EACJ,+FAkBF,SAAS,EAAkB,EAA8C,CACvE,IAAM,EAAW,EAAG,aAAa,WAAW,EAE5C,GAAI,EAAU,OAAO,EAErB,IAAM,EAAO,EAAG,aAAa,MAAM,EAInC,OAFI,GAAQ,KAAQ,EAAiC,EAAyB,GAEvE,IACT,CAEA,SAAS,EAAW,EAAiC,CACnD,IAAM,EAAc,MAAM,KAAK,EAAK,iBAA8B,CAAoB,CAAC,EAGvF,OAAO,EAAY,UAAU,CAAoB,EAAI,CAAC,EAAqB,GAAG,CAAW,EAAI,CAC/F,CAEA,SAAS,EAAe,EAAsB,EAAkC,EAAwB,CACtG,OAAO,EAAkB,CAAO,IAAM,IAAe,IAAS,IAAA,IAAa,EAAQ,aAAa,MAAM,IAAM,EAC9G,CAaA,SAAgB,EAAgB,EAAkC,CAAC,EAAuB,CACxF,IAAM,EAAM,EAAQ,WAAa,OAAO,SAAa,IAAc,SAAW,MACxE,EAAa,EAAQ,YAAc,SACnC,EAAO,EAAQ,MAAQ,GAAK,MAAQ,KAI1C,OAFK,EAEE,EAAW,CAAI,CAAC,CAAC,KAAM,GAAY,EAAe,EAAS,EAAY,EAAQ,IAAI,CAAC,GAAK,KAF9E,IAGpB,CASA,SAAgB,EAAoB,EAAkC,CAAC,EAAkB,CACvF,IAAM,EAAM,EAAQ,WAAa,OAAO,SAAa,IAAc,SAAW,MACxE,EAAa,EAAQ,YAAc,SACnC,EAAO,EAAQ,MAAQ,GAAK,MAAQ,KAI1C,OAFK,EAEE,EAAW,CAAI,CAAC,CAAC,OAAQ,GAAY,EAAe,EAAS,EAAY,EAAQ,IAAI,CAAC,EAF3E,CAAC,CAGrB,CAyBA,eAAsB,EAAkB,EAAc,EAAoC,CAAC,EAAyB,CAClH,IAAM,EAAa,EAAQ,YAAc,SACrC,EAA8B,KA6BlC,OA3BA,MAAM,EAAA,eACE,CACJ,IAAM,EAAU,EAAoB,CAClC,SAAU,EAAQ,SAClB,aACA,KAAM,EAAQ,KACd,KAAM,EAAQ,IAChB,CAAC,EACK,EAAS,EAAQ,KAAM,GAAc,EAAU,aAAa,SAAS,CAAI,CAAC,EAEhF,GAAI,EAAQ,CACV,EAAU,EACV,MACF,CAGA,MADI,EAAQ,SAAW,EAAS,IAAI,EAAA,gBAAgB,MAAM,EAAW,oBAAoB,EACnF,IAAI,EAAA,gBAAgB,MAAM,EAAW,yBAAyB,EAAK,GAAG,CAC9E,EACA,CACE,SAAU,EAAQ,SAClB,QAAS,sBAAsB,EAAK,IACpC,OAAQ,EAAQ,OAChB,QAAS,EAAQ,OACnB,CACF,EAGO,CACT,CASA,eAAsB,EAAyB,EAAoC,CAAC,EAAkB,CACpG,IAAM,EAAa,EAAQ,YAAc,SAEzC,MAAM,EAAA,eACE,CACJ,IAAM,EAAU,EAAoB,CAClC,SAAU,EAAQ,SAClB,aACA,KAAM,EAAQ,KACd,KAAM,EAAQ,IAChB,CAAC,EAEG,MAAQ,KAAM,GAAW,EAAO,cAAgB,EAAE,EAEtD,MADI,EAAQ,SAAW,EAAS,IAAI,EAAA,gBAAgB,MAAM,EAAW,oBAAoB,EACnF,IAAI,EAAA,gBAAgB,MAAM,EAAW,yBAAyB,CACtE,EACA,CACE,SAAU,EAAQ,SAClB,QAAS,2BACT,OAAQ,EAAQ,OAChB,QAAS,EAAQ,OACnB,CACF,CACF"}
@@ -1,65 +0,0 @@
1
- import { type WaitOptions } from './wait';
2
- /** Politeness levels for ARIA live regions. */
3
- export type LiveRegionPoliteness = 'assertive' | 'off' | 'polite';
4
- export interface LiveRegionQueryOptions {
5
- /** The document to search. Defaults to the global `document`. */
6
- document?: Document;
7
- /** Politeness to match via `aria-live` (or implicit role). Defaults to `'polite'`. */
8
- politeness?: LiveRegionPoliteness;
9
- /** ARIA role to match (e.g. `'status'`, `'alert'`). Omit to match any role. */
10
- role?: string;
11
- /** Root to scope the query. Defaults to `document.body`. */
12
- root?: ParentNode;
13
- }
14
- /**
15
- * Queries the first live region matching the given politeness (and optional role)
16
- * within `root`. Returns `null` when none exists — safe to call before the region
17
- * has been lazily created.
18
- *
19
- * Matches explicit `aria-live` attributes and implicit live-region roles.
20
- *
21
- * @example
22
- * const region = queryLiveRegion({ politeness: 'polite' });
23
- * expect(region).not.toBeNull();
24
- */
25
- export declare function queryLiveRegion(options?: LiveRegionQueryOptions): HTMLElement | null;
26
- /**
27
- * Queries all live regions matching the given politeness (and optional role)
28
- * within `root`. Useful for asserting no duplicate regions were created.
29
- *
30
- * @example
31
- * expect(queryAllLiveRegions({ politeness: 'polite' })).toHaveLength(1);
32
- */
33
- export declare function queryAllLiveRegions(options?: LiveRegionQueryOptions): HTMLElement[];
34
- export interface WaitForLiveRegionOptions extends WaitOptions {
35
- /** The document to search. Defaults to the global `document`. */
36
- document?: Document;
37
- /** Politeness to match via `aria-live`. Defaults to `'polite'`. */
38
- politeness?: LiveRegionPoliteness;
39
- /** ARIA role to match. Omit to match any role. */
40
- role?: string;
41
- /** Root to scope the query. Defaults to `document.body`. */
42
- root?: ParentNode;
43
- }
44
- /**
45
- * Retries until a live region matching the politeness/role exists and contains
46
- * `text` (substring match). Throws `AssayTimeoutError` on timeout.
47
- *
48
- * Handles the clear-then-set announce pattern: the region may briefly be empty
49
- * before the message is written, so polling until the text appears is the
50
- * correct approach rather than a single snapshot assertion.
51
- *
52
- * @example
53
- * await waitForLiveRegion('3 results found');
54
- * await waitForLiveRegion('Session expired', { politeness: 'assertive' });
55
- */
56
- export declare function waitForLiveRegion(text: string, options?: WaitForLiveRegionOptions): Promise<HTMLElement>;
57
- /**
58
- * Retries until a live region matching the politeness/role exists and is empty
59
- * (cleared). Throws `AssayTimeoutError` on timeout.
60
- *
61
- * @example
62
- * await waitForLiveRegionCleared();
63
- */
64
- export declare function waitForLiveRegionCleared(options?: WaitForLiveRegionOptions): Promise<void>;
65
- //# sourceMappingURL=live-region.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"live-region.d.ts","sourceRoot":"","sources":["../src/live-region.ts"],"names":[],"mappings":"AACA,OAAO,EAAc,KAAK,WAAW,EAAE,MAAM,QAAQ,CAAC;AAEtD,+CAA+C;AAC/C,MAAM,MAAM,oBAAoB,GAAG,WAAW,GAAG,KAAK,GAAG,QAAQ,CAAC;AAiBlE,MAAM,WAAW,sBAAsB;IACrC,iEAAiE;IACjE,QAAQ,CAAC,EAAE,QAAQ,CAAC;IACpB,sFAAsF;IACtF,UAAU,CAAC,EAAE,oBAAoB,CAAC;IAClC,+EAA+E;IAC/E,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,4DAA4D;IAC5D,IAAI,CAAC,EAAE,UAAU,CAAC;CACnB;AA8BD;;;;;;;;;;GAUG;AACH,wBAAgB,eAAe,CAAC,OAAO,GAAE,sBAA2B,GAAG,WAAW,GAAG,IAAI,CAQxF;AAED;;;;;;GAMG;AACH,wBAAgB,mBAAmB,CAAC,OAAO,GAAE,sBAA2B,GAAG,WAAW,EAAE,CAQvF;AAED,MAAM,WAAW,wBAAyB,SAAQ,WAAW;IAC3D,iEAAiE;IACjE,QAAQ,CAAC,EAAE,QAAQ,CAAC;IACpB,mEAAmE;IACnE,UAAU,CAAC,EAAE,oBAAoB,CAAC;IAClC,kDAAkD;IAClD,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,4DAA4D;IAC5D,IAAI,CAAC,EAAE,UAAU,CAAC;CACnB;AAED;;;;;;;;;;;GAWG;AACH,wBAAsB,iBAAiB,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,GAAE,wBAA6B,GAAG,OAAO,CAAC,WAAW,CAAC,CAgClH;AAED;;;;;;GAMG;AACH,wBAAsB,wBAAwB,CAAC,OAAO,GAAE,wBAA6B,GAAG,OAAO,CAAC,IAAI,CAAC,CAuBpG"}
@@ -1,73 +0,0 @@
1
- import { AssayQueryError as e } from "./errors.js";
2
- import { eventually as t } from "./wait.js";
3
- //#region src/live-region.ts
4
- var n = {
5
- alert: "assertive",
6
- log: "polite",
7
- marquee: "off",
8
- status: "polite",
9
- timer: "off"
10
- }, r = "[aria-live], [role=\"alert\"], [role=\"log\"], [role=\"marquee\"], [role=\"status\"], [role=\"timer\"]";
11
- function i(e) {
12
- let t = e.getAttribute("aria-live");
13
- if (t) return t;
14
- let r = e.getAttribute("role");
15
- return r && r in n ? n[r] : null;
16
- }
17
- function a(e) {
18
- let t = Array.from(e.querySelectorAll(r));
19
- return e.matches?.(r) ? [e, ...t] : t;
20
- }
21
- function o(e, t, n) {
22
- return i(e) === t && (n === void 0 || e.getAttribute("role") === n);
23
- }
24
- function s(e = {}) {
25
- let t = e.document ?? (typeof document < "u" ? document : null), n = e.politeness ?? "polite", r = e.root ?? t?.body ?? null;
26
- return r ? a(r).find((t) => o(t, n, e.role)) ?? null : null;
27
- }
28
- function c(e = {}) {
29
- let t = e.document ?? (typeof document < "u" ? document : null), n = e.politeness ?? "polite", r = e.root ?? t?.body ?? null;
30
- return r ? a(r).filter((t) => o(t, n, e.role)) : [];
31
- }
32
- async function l(n, r = {}) {
33
- let i = r.politeness ?? "polite", a = null;
34
- return await t(() => {
35
- let t = c({
36
- document: r.document,
37
- politeness: i,
38
- role: r.role,
39
- root: r.root
40
- }), o = t.find((e) => e.textContent?.includes(n));
41
- if (o) {
42
- a = o;
43
- return;
44
- }
45
- throw t.length === 0 ? new e(`No ${i} live region found.`) : new e(`No ${i} live region includes "${n}".`);
46
- }, {
47
- interval: r.interval,
48
- message: `waitForLiveRegion("${n}")`,
49
- signal: r.signal,
50
- timeout: r.timeout
51
- }), a;
52
- }
53
- async function u(n = {}) {
54
- let r = n.politeness ?? "polite";
55
- await t(() => {
56
- let t = c({
57
- document: n.document,
58
- politeness: r,
59
- role: n.role,
60
- root: n.root
61
- });
62
- if (!t.some((e) => e.textContent === "")) throw t.length === 0 ? new e(`No ${r} live region found.`) : new e(`No ${r} live region is cleared.`);
63
- }, {
64
- interval: n.interval,
65
- message: "waitForLiveRegionCleared",
66
- signal: n.signal,
67
- timeout: n.timeout
68
- });
69
- }
70
- //#endregion
71
- export { c as queryAllLiveRegions, s as queryLiveRegion, l as waitForLiveRegion, u as waitForLiveRegionCleared };
72
-
73
- //# sourceMappingURL=live-region.js.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"live-region.js","names":[],"sources":["../src/live-region.ts"],"sourcesContent":["import { AssayQueryError } from './errors';\nimport { eventually, type WaitOptions } from './wait';\n\n/** Politeness levels for ARIA live regions. */\nexport type LiveRegionPoliteness = 'assertive' | 'off' | 'polite';\n\n/**\n * ARIA roles that carry an implicit `aria-live` value per the WAI-ARIA spec.\n * `alert` is assertive; `log` and `status` are polite; `marquee` and `timer` are\n * off. Elements with these roles are live regions without explicit `aria-live`.\n */\nconst IMPLICIT_ROLE_POLITENESS: Record<string, LiveRegionPoliteness> = {\n alert: 'assertive',\n log: 'polite',\n marquee: 'off',\n status: 'polite',\n timer: 'off',\n};\nconst LIVE_REGION_SELECTOR =\n '[aria-live], [role=\"alert\"], [role=\"log\"], [role=\"marquee\"], [role=\"status\"], [role=\"timer\"]';\n\nexport interface LiveRegionQueryOptions {\n /** The document to search. Defaults to the global `document`. */\n document?: Document;\n /** Politeness to match via `aria-live` (or implicit role). Defaults to `'polite'`. */\n politeness?: LiveRegionPoliteness;\n /** ARIA role to match (e.g. `'status'`, `'alert'`). Omit to match any role. */\n role?: string;\n /** Root to scope the query. Defaults to `document.body`. */\n root?: ParentNode;\n}\n\n/**\n * Resolves the effective politeness of an element: explicit `aria-live` wins,\n * otherwise falls back to the implicit value from `role=\"status\"`/`role=\"alert\"`.\n * Returns `null` when the element is not a live region.\n */\nfunction resolvePoliteness(el: HTMLElement): LiveRegionPoliteness | null {\n const explicit = el.getAttribute('aria-live');\n\n if (explicit) return explicit as LiveRegionPoliteness;\n\n const role = el.getAttribute('role');\n\n if (role && role in IMPLICIT_ROLE_POLITENESS) return IMPLICIT_ROLE_POLITENESS[role];\n\n return null;\n}\n\nfunction candidates(root: ParentNode): HTMLElement[] {\n const descendants = Array.from(root.querySelectorAll<HTMLElement>(LIVE_REGION_SELECTOR));\n const rootElement = root as ParentNode & { matches?: (selector: string) => boolean };\n\n return rootElement.matches?.(LIVE_REGION_SELECTOR) ? [root as HTMLElement, ...descendants] : descendants;\n}\n\nfunction matchesOptions(element: HTMLElement, politeness: LiveRegionPoliteness, role?: string): boolean {\n return resolvePoliteness(element) === politeness && (role === undefined || element.getAttribute('role') === role);\n}\n\n/**\n * Queries the first live region matching the given politeness (and optional role)\n * within `root`. Returns `null` when none exists — safe to call before the region\n * has been lazily created.\n *\n * Matches explicit `aria-live` attributes and implicit live-region roles.\n *\n * @example\n * const region = queryLiveRegion({ politeness: 'polite' });\n * expect(region).not.toBeNull();\n */\nexport function queryLiveRegion(options: LiveRegionQueryOptions = {}): HTMLElement | null {\n const doc = options.document ?? (typeof document !== 'undefined' ? document : null);\n const politeness = options.politeness ?? 'polite';\n const root = options.root ?? doc?.body ?? null;\n\n if (!root) return null;\n\n return candidates(root).find((element) => matchesOptions(element, politeness, options.role)) ?? null;\n}\n\n/**\n * Queries all live regions matching the given politeness (and optional role)\n * within `root`. Useful for asserting no duplicate regions were created.\n *\n * @example\n * expect(queryAllLiveRegions({ politeness: 'polite' })).toHaveLength(1);\n */\nexport function queryAllLiveRegions(options: LiveRegionQueryOptions = {}): HTMLElement[] {\n const doc = options.document ?? (typeof document !== 'undefined' ? document : null);\n const politeness = options.politeness ?? 'polite';\n const root = options.root ?? doc?.body ?? null;\n\n if (!root) return [];\n\n return candidates(root).filter((element) => matchesOptions(element, politeness, options.role));\n}\n\nexport interface WaitForLiveRegionOptions extends WaitOptions {\n /** The document to search. Defaults to the global `document`. */\n document?: Document;\n /** Politeness to match via `aria-live`. Defaults to `'polite'`. */\n politeness?: LiveRegionPoliteness;\n /** ARIA role to match. Omit to match any role. */\n role?: string;\n /** Root to scope the query. Defaults to `document.body`. */\n root?: ParentNode;\n}\n\n/**\n * Retries until a live region matching the politeness/role exists and contains\n * `text` (substring match). Throws `AssayTimeoutError` on timeout.\n *\n * Handles the clear-then-set announce pattern: the region may briefly be empty\n * before the message is written, so polling until the text appears is the\n * correct approach rather than a single snapshot assertion.\n *\n * @example\n * await waitForLiveRegion('3 results found');\n * await waitForLiveRegion('Session expired', { politeness: 'assertive' });\n */\nexport async function waitForLiveRegion(text: string, options: WaitForLiveRegionOptions = {}): Promise<HTMLElement> {\n const politeness = options.politeness ?? 'polite';\n let matched: HTMLElement | null = null;\n\n await eventually(\n () => {\n const regions = queryAllLiveRegions({\n document: options.document,\n politeness,\n role: options.role,\n root: options.root,\n });\n const region = regions.find((candidate) => candidate.textContent?.includes(text));\n\n if (region) {\n matched = region;\n return;\n }\n\n if (regions.length === 0) throw new AssayQueryError(`No ${politeness} live region found.`);\n throw new AssayQueryError(`No ${politeness} live region includes \"${text}\".`);\n },\n {\n interval: options.interval,\n message: `waitForLiveRegion(\"${text}\")`,\n signal: options.signal,\n timeout: options.timeout,\n },\n );\n\n // `eventually` only resolves once the assertion stops throwing, so `matched` is set.\n return matched!;\n}\n\n/**\n * Retries until a live region matching the politeness/role exists and is empty\n * (cleared). Throws `AssayTimeoutError` on timeout.\n *\n * @example\n * await waitForLiveRegionCleared();\n */\nexport async function waitForLiveRegionCleared(options: WaitForLiveRegionOptions = {}): Promise<void> {\n const politeness = options.politeness ?? 'polite';\n\n await eventually(\n () => {\n const regions = queryAllLiveRegions({\n document: options.document,\n politeness,\n role: options.role,\n root: options.root,\n });\n\n if (regions.some((region) => region.textContent === '')) return;\n if (regions.length === 0) throw new AssayQueryError(`No ${politeness} live region found.`);\n throw new AssayQueryError(`No ${politeness} live region is cleared.`);\n },\n {\n interval: options.interval,\n message: 'waitForLiveRegionCleared',\n signal: options.signal,\n timeout: options.timeout,\n },\n );\n}\n"],"mappings":";;;AAWA,IAAM,IAAiE;CACrE,OAAO;CACP,KAAK;CACL,SAAS;CACT,QAAQ;CACR,OAAO;AACT,GACM,IACJ;AAkBF,SAAS,EAAkB,GAA8C;CACvE,IAAM,IAAW,EAAG,aAAa,WAAW;CAE5C,IAAI,GAAU,OAAO;CAErB,IAAM,IAAO,EAAG,aAAa,MAAM;CAInC,OAFI,KAAQ,KAAQ,IAAiC,EAAyB,KAEvE;AACT;AAEA,SAAS,EAAW,GAAiC;CACnD,IAAM,IAAc,MAAM,KAAK,EAAK,iBAA8B,CAAoB,CAAC;CAGvF,OAAO,EAAY,UAAU,CAAoB,IAAI,CAAC,GAAqB,GAAG,CAAW,IAAI;AAC/F;AAEA,SAAS,EAAe,GAAsB,GAAkC,GAAwB;CACtG,OAAO,EAAkB,CAAO,MAAM,MAAe,MAAS,KAAA,KAAa,EAAQ,aAAa,MAAM,MAAM;AAC9G;AAaA,SAAgB,EAAgB,IAAkC,CAAC,GAAuB;CACxF,IAAM,IAAM,EAAQ,aAAa,OAAO,WAAa,MAAc,WAAW,OACxE,IAAa,EAAQ,cAAc,UACnC,IAAO,EAAQ,QAAQ,GAAK,QAAQ;CAI1C,OAFK,IAEE,EAAW,CAAI,CAAC,CAAC,MAAM,MAAY,EAAe,GAAS,GAAY,EAAQ,IAAI,CAAC,KAAK,OAF9E;AAGpB;AASA,SAAgB,EAAoB,IAAkC,CAAC,GAAkB;CACvF,IAAM,IAAM,EAAQ,aAAa,OAAO,WAAa,MAAc,WAAW,OACxE,IAAa,EAAQ,cAAc,UACnC,IAAO,EAAQ,QAAQ,GAAK,QAAQ;CAI1C,OAFK,IAEE,EAAW,CAAI,CAAC,CAAC,QAAQ,MAAY,EAAe,GAAS,GAAY,EAAQ,IAAI,CAAC,IAF3E,CAAC;AAGrB;AAyBA,eAAsB,EAAkB,GAAc,IAAoC,CAAC,GAAyB;CAClH,IAAM,IAAa,EAAQ,cAAc,UACrC,IAA8B;CA6BlC,OA3BA,MAAM,QACE;EACJ,IAAM,IAAU,EAAoB;GAClC,UAAU,EAAQ;GAClB;GACA,MAAM,EAAQ;GACd,MAAM,EAAQ;EAChB,CAAC,GACK,IAAS,EAAQ,MAAM,MAAc,EAAU,aAAa,SAAS,CAAI,CAAC;EAEhF,IAAI,GAAQ;GACV,IAAU;GACV;EACF;EAGA,MADI,EAAQ,WAAW,IAAS,IAAI,EAAgB,MAAM,EAAW,oBAAoB,IACnF,IAAI,EAAgB,MAAM,EAAW,yBAAyB,EAAK,GAAG;CAC9E,GACA;EACE,UAAU,EAAQ;EAClB,SAAS,sBAAsB,EAAK;EACpC,QAAQ,EAAQ;EAChB,SAAS,EAAQ;CACnB,CACF,GAGO;AACT;AASA,eAAsB,EAAyB,IAAoC,CAAC,GAAkB;CACpG,IAAM,IAAa,EAAQ,cAAc;CAEzC,MAAM,QACE;EACJ,IAAM,IAAU,EAAoB;GAClC,UAAU,EAAQ;GAClB;GACA,MAAM,EAAQ;GACd,MAAM,EAAQ;EAChB,CAAC;EAEG,OAAQ,MAAM,MAAW,EAAO,gBAAgB,EAAE,GAEtD,MADI,EAAQ,WAAW,IAAS,IAAI,EAAgB,MAAM,EAAW,oBAAoB,IACnF,IAAI,EAAgB,MAAM,EAAW,yBAAyB;CACtE,GACA;EACE,UAAU,EAAQ;EAClB,SAAS;EACT,QAAQ,EAAQ;EAChB,SAAS,EAAQ;CACnB,CACF;AACF"}