@vielzeug/assay 1.0.0 → 2.0.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.cjs.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"wait.cjs","names":[],"sources":["../src/wait.ts"],"sourcesContent":["/**\n * Async waiting utilities for test environments.\n */\n\nimport { AssayTimeoutError } from './errors';\n\n// ─── Types ───────────────────────────────────────────────────────────────────\n\nexport interface WaitOptions {\n /** Maximum wait time in ms (default: 1000) */\n timeout?: number;\n /** Polling interval in ms (default: 50) */\n interval?: number;\n /** Message included in timeout error */\n message?: string;\n}\n\n// ─── API ─────────────────────────────────────────────────────────────────────\n\n/**\n * Poll until a callback returns truthy (or void) without throwing.\n * Supports both boolean conditions and `expect()` assertions.\n *\n * - Returns truthy → success\n * - Returns `undefined` (e.g. bare `expect()` call) → success\n * - Returns falsy value → retry\n * - Throws → retry\n *\n * Always rejects with `AssayTimeoutError` on timeout, regardless of which of the two retry\n * reasons above was last observed — `instanceof AssayTimeoutError` is a reliable way to catch\n * \"the condition never became true\" without caring whether the last attempt returned falsy or\n * threw. The original failure (an assertion's message, a thrown error) is preserved as `.cause`,\n * not merged into a mutated copy of it — `fn`'s thrown errors are caller-owned and never mutated.\n *\n * @example\n * await waitFor(() => query('.status')?.textContent === 'loaded');\n * await waitFor(() => expect(count).toBe(3));\n */\nexport async function waitFor(\n fn: () => unknown,\n { interval = 50, message, timeout = 1000 }: WaitOptions = {},\n): Promise<void> {\n const deadline = Date.now() + timeout;\n let lastError: unknown;\n\n // Single loop, one `fn()` call site — always tries once more before checking the deadline,\n // so a condition that becomes true exactly as the timeout elapses still succeeds.\n for (;;) {\n try {\n const result = await fn();\n\n if (result === undefined || result) return;\n } catch (e) {\n lastError = e;\n }\n\n if (Date.now() >= deadline) break;\n\n await wait(interval);\n }\n\n // The default timing summary always appears — a custom `message` adds context in front of it\n // rather than replacing it, so a debugging session never loses \"how long did this actually wait\".\n const timing = `waitFor timed out after ${timeout}ms`;\n const base = message ? `${message} (${timing})` : timing;\n const cause = lastError instanceof Error ? lastError.message : lastError != null ? String(lastError) : undefined;\n\n throw new AssayTimeoutError(cause ? `${base}\\n${cause}` : base, { cause: lastError });\n}\n\n/**\n * Resolve when the target element emits the given event.\n *\n * @example\n * const promise = waitForEvent(el, 'change');\n * fire.click(trigger);\n * const event = await promise;\n */\nexport function waitForEvent<T extends Event = Event>(element: Element, name: string, timeout = 1000): Promise<T> {\n return new Promise((resolve, reject) => {\n const onEvent = (e: Event): void => {\n clearTimeout(timer);\n resolve(e as T);\n };\n const timer = setTimeout(() => {\n element.removeEventListener(name, onEvent);\n reject(new AssayTimeoutError(`waitForEvent: \"${name}\" timed out after ${timeout}ms`));\n }, timeout);\n\n element.addEventListener(name, onEvent, { once: true });\n });\n}\n\n/**\n * Resolves after one microtask tick (`queueMicrotask`). Use to wait for reactivity (signal\n * effects, promise-chain continuations) to settle without moving into the macrotask queue —\n * cheaper and more precise than `wait(0)`, which yields to a full `setTimeout` turn instead.\n */\nexport const nextTick = (): Promise<void> =>\n new Promise((resolve) => {\n queueMicrotask(resolve);\n });\n\n/**\n * Resolves after `ms` milliseconds. Prefer `nextTick()` for reactive updates and `waitFor()`\n * for polling a condition — reach for this only when a real macrotask delay is what the code\n * under test actually depends on (e.g. a debounce timer).\n */\nexport const wait = (ms = 0): Promise<void> =>\n new Promise((resolve) => {\n setTimeout(resolve, ms);\n });\n"],"mappings":"gCAsCA,eAAsB,EACpB,EACA,CAAE,WAAW,GAAI,UAAS,UAAU,KAAsB,CAAC,EAC5C,CACf,IAAM,EAAW,KAAK,IAAI,EAAI,EAC1B,EAIJ,OAAS,CACP,GAAI,CACF,IAAM,EAAS,MAAM,EAAG,EAExB,GAAI,IAAW,IAAA,IAAa,EAAQ,MACtC,OAAS,EAAG,CACV,EAAY,CACd,CAEA,GAAI,KAAK,IAAI,GAAK,EAAU,MAE5B,MAAM,EAAK,CAAQ,CACrB,CAIA,IAAM,EAAS,2BAA2B,EAAQ,IAC5C,EAAO,EAAU,GAAG,EAAQ,IAAI,EAAO,GAAK,EAC5C,EAAQ,aAAqB,MAAQ,EAAU,QAAU,GAAa,KAA2B,IAAA,GAApB,OAAO,CAAS,EAEnG,MAAM,IAAI,EAAA,kBAAkB,EAAQ,GAAG,EAAK,IAAI,IAAU,EAAM,CAAE,MAAO,CAAU,CAAC,CACtF,CAUA,SAAgB,EAAsC,EAAkB,EAAc,EAAU,IAAkB,CAChH,OAAO,IAAI,SAAS,EAAS,IAAW,CACtC,IAAM,EAAW,GAAmB,CAClC,aAAa,CAAK,EAClB,EAAQ,CAAM,CAChB,EACM,EAAQ,eAAiB,CAC7B,EAAQ,oBAAoB,EAAM,CAAO,EACzC,EAAO,IAAI,EAAA,kBAAkB,kBAAkB,EAAK,oBAAoB,EAAQ,GAAG,CAAC,CACtF,EAAG,CAAO,EAEV,EAAQ,iBAAiB,EAAM,EAAS,CAAE,KAAM,EAAK,CAAC,CACxD,CAAC,CACH,CAOA,IAAa,MACX,IAAI,QAAS,GAAY,CACvB,eAAe,CAAO,CACxB,CAAC,EAOU,GAAQ,EAAK,IACxB,IAAI,QAAS,GAAY,CACvB,WAAW,EAAS,CAAE,CACxB,CAAC"}
1
+ {"version":3,"file":"wait.cjs","names":[],"sources":["../src/wait.ts"],"sourcesContent":["import { AssayTimeoutError } from './errors';\n\nexport interface WaitOptions {\n /** Cancel the pending wait. */\n signal?: AbortSignal;\n /** Maximum wait time in ms (default: 1000). */\n timeout?: number;\n /** Polling interval in ms (default: 50). */\n interval?: number;\n}\n\nexport interface RetryOptions extends WaitOptions {\n /** Context included in the timeout error. */\n message?: string;\n}\n\nexport interface DelayOptions {\n /** Cancel the pending delay. */\n signal?: AbortSignal;\n}\n\nconst abortReason = (signal: AbortSignal): unknown =>\n signal.reason ?? new DOMException('The operation was aborted.', 'AbortError');\n\n/**\n * Resolve after `ms` milliseconds. Rejects with the abort reason when cancelled.\n */\nexport const delay = (ms = 0, { signal }: DelayOptions = {}): Promise<void> =>\n new Promise((resolve, reject) => {\n if (signal?.aborted) {\n reject(abortReason(signal));\n\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(abortReason(signal!));\n }\n\n signal?.addEventListener('abort', abort, { once: true });\n });\n\n/** Poll until a synchronous or asynchronous predicate returns `true`. */\nexport async function waitUntil(\n predicate: () => boolean | Promise<boolean>,\n { interval = 50, signal, timeout = 1000 }: WaitOptions = {},\n): Promise<void> {\n const deadline = Date.now() + timeout;\n\n for (;;) {\n if (signal?.aborted) throw abortReason(signal);\n\n if (await predicate()) return;\n\n if (Date.now() >= deadline) break;\n\n await delay(interval, { signal });\n }\n\n throw new AssayTimeoutError(`waitUntil timed out after ${timeout}ms`);\n}\n\n/** Retry an assertion until it stops throwing. */\nexport async function retry(\n assertion: () => void | Promise<void>,\n { interval = 50, message, signal, timeout = 1000 }: RetryOptions = {},\n): Promise<void> {\n const deadline = Date.now() + timeout;\n let lastError: unknown;\n\n for (;;) {\n if (signal?.aborted) throw abortReason(signal);\n\n try {\n await assertion();\n\n return;\n } catch (error) {\n lastError = error;\n }\n\n if (Date.now() >= deadline) break;\n\n await delay(interval, { signal });\n }\n\n const timing = `retry 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 return new Promise((resolve, reject) => {\n if (signal?.aborted) {\n reject(abortReason(signal));\n\n return;\n }\n\n const dispose = () => {\n clearTimeout(timer);\n signal?.removeEventListener('abort', onAbort);\n target.removeEventListener(type, onEvent);\n };\n const onEvent = (e: Event): void => {\n dispose();\n resolve(e 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(abortReason(signal!));\n };\n\n target.addEventListener(type, onEvent, { once: true });\n signal?.addEventListener('abort', onAbort, { once: true });\n });\n}\n\n/** Resolve after one microtask tick. */\nexport const nextTick = (): Promise<void> =>\n new Promise((resolve) => {\n queueMicrotask(resolve);\n });\n"],"mappings":"gCAqBA,IAAM,EAAe,GACnB,EAAO,QAAU,IAAI,aAAa,6BAA8B,YAAY,EAKjE,GAAS,EAAK,EAAG,CAAE,UAAyB,CAAC,IACxD,IAAI,SAAS,EAAS,IAAW,CAC/B,GAAI,GAAQ,QAAS,CACnB,EAAO,EAAY,CAAM,CAAC,EAE1B,MACF,CAEA,IAAM,EAAQ,WAAW,EAAM,CAAE,EAEjC,SAAS,GAAa,CACpB,GAAQ,oBAAoB,QAAS,CAAK,EAC1C,EAAQ,CACV,CAEA,SAAS,GAAc,CACrB,aAAa,CAAK,EAClB,EAAO,EAAY,CAAO,CAAC,CAC7B,CAEA,GAAQ,iBAAiB,QAAS,EAAO,CAAE,KAAM,EAAK,CAAC,CACzD,CAAC,EAGH,eAAsB,EACpB,EACA,CAAE,WAAW,GAAI,SAAQ,UAAU,KAAsB,CAAC,EAC3C,CACf,IAAM,EAAW,KAAK,IAAI,EAAI,EAE9B,OAAS,CACP,GAAI,GAAQ,QAAS,MAAM,EAAY,CAAM,EAE7C,GAAI,MAAM,EAAU,EAAG,OAEvB,GAAI,KAAK,IAAI,GAAK,EAAU,MAE5B,MAAM,EAAM,EAAU,CAAE,QAAO,CAAC,CAClC,CAEA,MAAM,IAAI,EAAA,kBAAkB,6BAA6B,EAAQ,GAAG,CACtE,CAGA,eAAsB,EACpB,EACA,CAAE,WAAW,GAAI,UAAS,SAAQ,UAAU,KAAuB,CAAC,EACrD,CACf,IAAM,EAAW,KAAK,IAAI,EAAI,EAC1B,EAEJ,OAAS,CACP,GAAI,GAAQ,QAAS,MAAM,EAAY,CAAM,EAE7C,GAAI,CACF,MAAM,EAAU,EAEhB,MACF,OAAS,EAAO,CACd,EAAY,CACd,CAEA,GAAI,KAAK,IAAI,GAAK,EAAU,MAE5B,MAAM,EAAM,EAAU,CAAE,QAAO,CAAC,CAClC,CAEA,IAAM,EAAS,yBAAyB,EAAQ,IAC1C,EAAU,EAAU,GAAG,EAAQ,IAAI,EAAO,GAAK,EAC/C,EAAS,aAAqB,MAAQ,EAAU,QAAU,GAAa,KAAO,GAAK,OAAO,CAAS,EAEzG,MAAM,IAAI,EAAA,kBAAkB,EAAS,GAAG,EAAQ,IAAI,IAAW,EAAS,CAAE,MAAO,CAAU,CAAC,CAC9F,CAGA,SAAgB,EACd,EACA,EACA,CAAE,SAAQ,UAAU,KAAwC,CAAC,EACjD,CACZ,OAAO,IAAI,SAAS,EAAS,IAAW,CACtC,GAAI,GAAQ,QAAS,CACnB,EAAO,EAAY,CAAM,CAAC,EAE1B,MACF,CAEA,IAAM,MAAgB,CACpB,aAAa,CAAK,EAClB,GAAQ,oBAAoB,QAAS,CAAO,EAC5C,EAAO,oBAAoB,EAAM,CAAO,CAC1C,EACM,EAAW,GAAmB,CAClC,EAAQ,EACR,EAAQ,CAAM,CAChB,EACM,EAAQ,eAAiB,CAC7B,EAAQ,EACR,EAAO,IAAI,EAAA,kBAAkB,kBAAkB,EAAK,oBAAoB,EAAQ,GAAG,CAAC,CACtF,EAAG,CAAO,EACJ,MAAgB,CACpB,EAAQ,EACR,EAAO,EAAY,CAAO,CAAC,CAC7B,EAEA,EAAO,iBAAiB,EAAM,EAAS,CAAE,KAAM,EAAK,CAAC,EACrD,GAAQ,iBAAiB,QAAS,EAAS,CAAE,KAAM,EAAK,CAAC,CAC3D,CAAC,CACH,CAGA,IAAa,MACX,IAAI,QAAS,GAAY,CACvB,eAAe,CAAO,CACxB,CAAC"}
package/dist/wait.d.ts CHANGED
@@ -1,53 +1,29 @@
1
- /**
2
- * Async waiting utilities for test environments.
3
- */
4
1
  export interface WaitOptions {
5
- /** Maximum wait time in ms (default: 1000) */
2
+ /** Cancel the pending wait. */
3
+ signal?: AbortSignal;
4
+ /** Maximum wait time in ms (default: 1000). */
6
5
  timeout?: number;
7
- /** Polling interval in ms (default: 50) */
6
+ /** Polling interval in ms (default: 50). */
8
7
  interval?: number;
9
- /** Message included in timeout error */
8
+ }
9
+ export interface RetryOptions extends WaitOptions {
10
+ /** Context included in the timeout error. */
10
11
  message?: string;
11
12
  }
13
+ export interface DelayOptions {
14
+ /** Cancel the pending delay. */
15
+ signal?: AbortSignal;
16
+ }
12
17
  /**
13
- * Poll until a callback returns truthy (or void) without throwing.
14
- * Supports both boolean conditions and `expect()` assertions.
15
- *
16
- * - Returns truthy → success
17
- * - Returns `undefined` (e.g. bare `expect()` call) → success
18
- * - Returns falsy value → retry
19
- * - Throws → retry
20
- *
21
- * Always rejects with `AssayTimeoutError` on timeout, regardless of which of the two retry
22
- * reasons above was last observed — `instanceof AssayTimeoutError` is a reliable way to catch
23
- * "the condition never became true" without caring whether the last attempt returned falsy or
24
- * threw. The original failure (an assertion's message, a thrown error) is preserved as `.cause`,
25
- * not merged into a mutated copy of it — `fn`'s thrown errors are caller-owned and never mutated.
26
- *
27
- * @example
28
- * await waitFor(() => query('.status')?.textContent === 'loaded');
29
- * await waitFor(() => expect(count).toBe(3));
30
- */
31
- export declare function waitFor(fn: () => unknown, { interval, message, timeout }?: WaitOptions): Promise<void>;
32
- /**
33
- * Resolve when the target element emits the given event.
34
- *
35
- * @example
36
- * const promise = waitForEvent(el, 'change');
37
- * fire.click(trigger);
38
- * const event = await promise;
39
- */
40
- export declare function waitForEvent<T extends Event = Event>(element: Element, name: string, timeout?: number): Promise<T>;
41
- /**
42
- * Resolves after one microtask tick (`queueMicrotask`). Use to wait for reactivity (signal
43
- * effects, promise-chain continuations) to settle without moving into the macrotask queue —
44
- * cheaper and more precise than `wait(0)`, which yields to a full `setTimeout` turn instead.
18
+ * Resolve after `ms` milliseconds. Rejects with the abort reason when cancelled.
45
19
  */
20
+ export declare const delay: (ms?: number, { signal }?: DelayOptions) => Promise<void>;
21
+ /** Poll until a synchronous or asynchronous predicate returns `true`. */
22
+ export declare function waitUntil(predicate: () => boolean | Promise<boolean>, { interval, signal, timeout }?: WaitOptions): Promise<void>;
23
+ /** Retry an assertion until it stops throwing. */
24
+ export declare function retry(assertion: () => void | Promise<void>, { interval, message, signal, timeout }?: RetryOptions): Promise<void>;
25
+ /** Resolve with the next event of `type`, or reject when it times out or is aborted. */
26
+ export declare function waitForEvent<T extends Event = Event>(target: EventTarget, type: string, { signal, timeout }?: Omit<WaitOptions, 'interval'>): Promise<T>;
27
+ /** Resolve after one microtask tick. */
46
28
  export declare const nextTick: () => Promise<void>;
47
- /**
48
- * Resolves after `ms` milliseconds. Prefer `nextTick()` for reactive updates and `waitFor()`
49
- * for polling a condition — reach for this only when a real macrotask delay is what the code
50
- * under test actually depends on (e.g. a debounce timer).
51
- */
52
- export declare const wait: (ms?: number) => Promise<void>;
53
29
  //# sourceMappingURL=wait.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"wait.d.ts","sourceRoot":"","sources":["../src/wait.ts"],"names":[],"mappings":"AAAA;;GAEG;AAMH,MAAM,WAAW,WAAW;IAC1B,8CAA8C;IAC9C,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,2CAA2C;IAC3C,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,wCAAwC;IACxC,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AAID;;;;;;;;;;;;;;;;;;GAkBG;AACH,wBAAsB,OAAO,CAC3B,EAAE,EAAE,MAAM,OAAO,EACjB,EAAE,QAAa,EAAE,OAAO,EAAE,OAAc,EAAE,GAAE,WAAgB,GAC3D,OAAO,CAAC,IAAI,CAAC,CA2Bf;AAED;;;;;;;GAOG;AACH,wBAAgB,YAAY,CAAC,CAAC,SAAS,KAAK,GAAG,KAAK,EAAE,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,EAAE,OAAO,SAAO,GAAG,OAAO,CAAC,CAAC,CAAC,CAahH;AAED;;;;GAIG;AACH,eAAO,MAAM,QAAQ,QAAO,OAAO,CAAC,IAAI,CAGpC,CAAC;AAEL;;;;GAIG;AACH,eAAO,MAAM,IAAI,GAAI,WAAM,KAAG,OAAO,CAAC,IAAI,CAGtC,CAAC"}
1
+ {"version":3,"file":"wait.d.ts","sourceRoot":"","sources":["../src/wait.ts"],"names":[],"mappings":"AAEA,MAAM,WAAW,WAAW;IAC1B,+BAA+B;IAC/B,MAAM,CAAC,EAAE,WAAW,CAAC;IACrB,+CAA+C;IAC/C,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,4CAA4C;IAC5C,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB;AAED,MAAM,WAAW,YAAa,SAAQ,WAAW;IAC/C,6CAA6C;IAC7C,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AAED,MAAM,WAAW,YAAY;IAC3B,gCAAgC;IAChC,MAAM,CAAC,EAAE,WAAW,CAAC;CACtB;AAKD;;GAEG;AACH,eAAO,MAAM,KAAK,GAAI,WAAM,EAAE,aAAY,YAAiB,KAAG,OAAO,CAAC,IAAI,CAqBtE,CAAC;AAEL,yEAAyE;AACzE,wBAAsB,SAAS,CAC7B,SAAS,EAAE,MAAM,OAAO,GAAG,OAAO,CAAC,OAAO,CAAC,EAC3C,EAAE,QAAa,EAAE,MAAM,EAAE,OAAc,EAAE,GAAE,WAAgB,GAC1D,OAAO,CAAC,IAAI,CAAC,CAcf;AAED,kDAAkD;AAClD,wBAAsB,KAAK,CACzB,SAAS,EAAE,MAAM,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,EACrC,EAAE,QAAa,EAAE,OAAO,EAAE,MAAM,EAAE,OAAc,EAAE,GAAE,YAAiB,GACpE,OAAO,CAAC,IAAI,CAAC,CAyBf;AAED,wFAAwF;AACxF,wBAAgB,YAAY,CAAC,CAAC,SAAS,KAAK,GAAG,KAAK,EAClD,MAAM,EAAE,WAAW,EACnB,IAAI,EAAE,MAAM,EACZ,EAAE,MAAM,EAAE,OAAc,EAAE,GAAE,IAAI,CAAC,WAAW,EAAE,UAAU,CAAM,GAC7D,OAAO,CAAC,CAAC,CAAC,CA6BZ;AAED,wCAAwC;AACxC,eAAO,MAAM,QAAQ,QAAO,OAAO,CAAC,IAAI,CAGpC,CAAC"}
package/dist/wait.js CHANGED
@@ -1,36 +1,67 @@
1
1
  import { AssayTimeoutError as e } from "./errors.js";
2
2
  //#region src/wait.ts
3
- async function t(t, { interval: n = 50, message: r, timeout: a = 1e3 } = {}) {
4
- let o = Date.now() + a, s;
3
+ var t = (e) => e.reason ?? new DOMException("The operation was aborted.", "AbortError"), n = (e = 0, { signal: n } = {}) => new Promise((r, i) => {
4
+ if (n?.aborted) {
5
+ i(t(n));
6
+ return;
7
+ }
8
+ let a = setTimeout(o, e);
9
+ function o() {
10
+ n?.removeEventListener("abort", s), r();
11
+ }
12
+ function s() {
13
+ clearTimeout(a), i(t(n));
14
+ }
15
+ n?.addEventListener("abort", s, { once: !0 });
16
+ });
17
+ async function r(r, { interval: i = 50, signal: a, timeout: o = 1e3 } = {}) {
18
+ let s = Date.now() + o;
5
19
  for (;;) {
20
+ if (a?.aborted) throw t(a);
21
+ if (await r()) return;
22
+ if (Date.now() >= s) break;
23
+ await n(i, { signal: a });
24
+ }
25
+ throw new e(`waitUntil timed out after ${o}ms`);
26
+ }
27
+ async function i(r, { interval: i = 50, message: a, signal: o, timeout: s = 1e3 } = {}) {
28
+ let c = Date.now() + s, l;
29
+ for (;;) {
30
+ if (o?.aborted) throw t(o);
6
31
  try {
7
- let e = await t();
8
- if (e === void 0 || e) return;
32
+ await r();
33
+ return;
9
34
  } catch (e) {
10
- s = e;
35
+ l = e;
11
36
  }
12
- if (Date.now() >= o) break;
13
- await i(n);
37
+ if (Date.now() >= c) break;
38
+ await n(i, { signal: o });
14
39
  }
15
- let c = `waitFor timed out after ${a}ms`, l = r ? `${r} (${c})` : c, u = s instanceof Error ? s.message : s == null ? void 0 : String(s);
16
- throw new e(u ? `${l}\n${u}` : l, { cause: s });
40
+ let u = `retry timed out after ${s}ms`, d = a ? `${a} (${u})` : u, f = l instanceof Error ? l.message : l == null ? "" : String(l);
41
+ throw new e(f ? `${d}\n${f}` : d, { cause: l });
17
42
  }
18
- function n(t, n, r = 1e3) {
19
- return new Promise((i, a) => {
20
- let o = (e) => {
21
- clearTimeout(s), i(e);
22
- }, s = setTimeout(() => {
23
- t.removeEventListener(n, o), a(new e(`waitForEvent: "${n}" timed out after ${r}ms`));
24
- }, r);
25
- t.addEventListener(n, o, { once: !0 });
43
+ function a(n, r, { signal: i, timeout: a = 1e3 } = {}) {
44
+ return new Promise((o, s) => {
45
+ if (i?.aborted) {
46
+ s(t(i));
47
+ return;
48
+ }
49
+ let c = () => {
50
+ clearTimeout(u), i?.removeEventListener("abort", d), n.removeEventListener(r, l);
51
+ }, l = (e) => {
52
+ c(), o(e);
53
+ }, u = setTimeout(() => {
54
+ c(), s(new e(`waitForEvent: "${r}" timed out after ${a}ms`));
55
+ }, a), d = () => {
56
+ c(), s(t(i));
57
+ };
58
+ n.addEventListener(r, l, { once: !0 }), i?.addEventListener("abort", d, { once: !0 });
26
59
  });
27
60
  }
28
- var r = () => new Promise((e) => {
61
+ var o = () => new Promise((e) => {
29
62
  queueMicrotask(e);
30
- }), i = (e = 0) => new Promise((t) => {
31
- setTimeout(t, e);
32
63
  });
33
64
  //#endregion
34
- export { r as nextTick, i as wait, t as waitFor, n as waitForEvent };
65
+ export { n as delay, o as nextTick, i as retry, a as waitForEvent, r as waitUntil };
35
66
 
36
67
  //# sourceMappingURL=wait.js.map
package/dist/wait.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"wait.js","names":[],"sources":["../src/wait.ts"],"sourcesContent":["/**\n * Async waiting utilities for test environments.\n */\n\nimport { AssayTimeoutError } from './errors';\n\n// ─── Types ───────────────────────────────────────────────────────────────────\n\nexport interface WaitOptions {\n /** Maximum wait time in ms (default: 1000) */\n timeout?: number;\n /** Polling interval in ms (default: 50) */\n interval?: number;\n /** Message included in timeout error */\n message?: string;\n}\n\n// ─── API ─────────────────────────────────────────────────────────────────────\n\n/**\n * Poll until a callback returns truthy (or void) without throwing.\n * Supports both boolean conditions and `expect()` assertions.\n *\n * - Returns truthy → success\n * - Returns `undefined` (e.g. bare `expect()` call) → success\n * - Returns falsy value → retry\n * - Throws → retry\n *\n * Always rejects with `AssayTimeoutError` on timeout, regardless of which of the two retry\n * reasons above was last observed — `instanceof AssayTimeoutError` is a reliable way to catch\n * \"the condition never became true\" without caring whether the last attempt returned falsy or\n * threw. The original failure (an assertion's message, a thrown error) is preserved as `.cause`,\n * not merged into a mutated copy of it — `fn`'s thrown errors are caller-owned and never mutated.\n *\n * @example\n * await waitFor(() => query('.status')?.textContent === 'loaded');\n * await waitFor(() => expect(count).toBe(3));\n */\nexport async function waitFor(\n fn: () => unknown,\n { interval = 50, message, timeout = 1000 }: WaitOptions = {},\n): Promise<void> {\n const deadline = Date.now() + timeout;\n let lastError: unknown;\n\n // Single loop, one `fn()` call site — always tries once more before checking the deadline,\n // so a condition that becomes true exactly as the timeout elapses still succeeds.\n for (;;) {\n try {\n const result = await fn();\n\n if (result === undefined || result) return;\n } catch (e) {\n lastError = e;\n }\n\n if (Date.now() >= deadline) break;\n\n await wait(interval);\n }\n\n // The default timing summary always appears — a custom `message` adds context in front of it\n // rather than replacing it, so a debugging session never loses \"how long did this actually wait\".\n const timing = `waitFor timed out after ${timeout}ms`;\n const base = message ? `${message} (${timing})` : timing;\n const cause = lastError instanceof Error ? lastError.message : lastError != null ? String(lastError) : undefined;\n\n throw new AssayTimeoutError(cause ? `${base}\\n${cause}` : base, { cause: lastError });\n}\n\n/**\n * Resolve when the target element emits the given event.\n *\n * @example\n * const promise = waitForEvent(el, 'change');\n * fire.click(trigger);\n * const event = await promise;\n */\nexport function waitForEvent<T extends Event = Event>(element: Element, name: string, timeout = 1000): Promise<T> {\n return new Promise((resolve, reject) => {\n const onEvent = (e: Event): void => {\n clearTimeout(timer);\n resolve(e as T);\n };\n const timer = setTimeout(() => {\n element.removeEventListener(name, onEvent);\n reject(new AssayTimeoutError(`waitForEvent: \"${name}\" timed out after ${timeout}ms`));\n }, timeout);\n\n element.addEventListener(name, onEvent, { once: true });\n });\n}\n\n/**\n * Resolves after one microtask tick (`queueMicrotask`). Use to wait for reactivity (signal\n * effects, promise-chain continuations) to settle without moving into the macrotask queue —\n * cheaper and more precise than `wait(0)`, which yields to a full `setTimeout` turn instead.\n */\nexport const nextTick = (): Promise<void> =>\n new Promise((resolve) => {\n queueMicrotask(resolve);\n });\n\n/**\n * Resolves after `ms` milliseconds. Prefer `nextTick()` for reactive updates and `waitFor()`\n * for polling a condition — reach for this only when a real macrotask delay is what the code\n * under test actually depends on (e.g. a debounce timer).\n */\nexport const wait = (ms = 0): Promise<void> =>\n new Promise((resolve) => {\n setTimeout(resolve, ms);\n });\n"],"mappings":";;AAsCA,eAAsB,EACpB,GACA,EAAE,cAAW,IAAI,YAAS,aAAU,QAAsB,CAAC,GAC5C;CACf,IAAM,IAAW,KAAK,IAAI,IAAI,GAC1B;CAIJ,SAAS;EACP,IAAI;GACF,IAAM,IAAS,MAAM,EAAG;GAExB,IAAI,MAAW,KAAA,KAAa,GAAQ;EACtC,SAAS,GAAG;GACV,IAAY;EACd;EAEA,IAAI,KAAK,IAAI,KAAK,GAAU;EAE5B,MAAM,EAAK,CAAQ;CACrB;CAIA,IAAM,IAAS,2BAA2B,EAAQ,KAC5C,IAAO,IAAU,GAAG,EAAQ,IAAI,EAAO,KAAK,GAC5C,IAAQ,aAAqB,QAAQ,EAAU,UAAU,KAAa,OAA2B,KAAA,IAApB,OAAO,CAAS;CAEnG,MAAM,IAAI,EAAkB,IAAQ,GAAG,EAAK,IAAI,MAAU,GAAM,EAAE,OAAO,EAAU,CAAC;AACtF;AAUA,SAAgB,EAAsC,GAAkB,GAAc,IAAU,KAAkB;CAChH,OAAO,IAAI,SAAS,GAAS,MAAW;EACtC,IAAM,KAAW,MAAmB;GAElC,AADA,aAAa,CAAK,GAClB,EAAQ,CAAM;EAChB,GACM,IAAQ,iBAAiB;GAE7B,AADA,EAAQ,oBAAoB,GAAM,CAAO,GACzC,EAAO,IAAI,EAAkB,kBAAkB,EAAK,oBAAoB,EAAQ,GAAG,CAAC;EACtF,GAAG,CAAO;EAEV,EAAQ,iBAAiB,GAAM,GAAS,EAAE,MAAM,GAAK,CAAC;CACxD,CAAC;AACH;AAOA,IAAa,UACX,IAAI,SAAS,MAAY;CACvB,eAAe,CAAO;AACxB,CAAC,GAOU,KAAQ,IAAK,MACxB,IAAI,SAAS,MAAY;CACvB,WAAW,GAAS,CAAE;AACxB,CAAC"}
1
+ {"version":3,"file":"wait.js","names":[],"sources":["../src/wait.ts"],"sourcesContent":["import { AssayTimeoutError } from './errors';\n\nexport interface WaitOptions {\n /** Cancel the pending wait. */\n signal?: AbortSignal;\n /** Maximum wait time in ms (default: 1000). */\n timeout?: number;\n /** Polling interval in ms (default: 50). */\n interval?: number;\n}\n\nexport interface RetryOptions extends WaitOptions {\n /** Context included in the timeout error. */\n message?: string;\n}\n\nexport interface DelayOptions {\n /** Cancel the pending delay. */\n signal?: AbortSignal;\n}\n\nconst abortReason = (signal: AbortSignal): unknown =>\n signal.reason ?? new DOMException('The operation was aborted.', 'AbortError');\n\n/**\n * Resolve after `ms` milliseconds. Rejects with the abort reason when cancelled.\n */\nexport const delay = (ms = 0, { signal }: DelayOptions = {}): Promise<void> =>\n new Promise((resolve, reject) => {\n if (signal?.aborted) {\n reject(abortReason(signal));\n\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(abortReason(signal!));\n }\n\n signal?.addEventListener('abort', abort, { once: true });\n });\n\n/** Poll until a synchronous or asynchronous predicate returns `true`. */\nexport async function waitUntil(\n predicate: () => boolean | Promise<boolean>,\n { interval = 50, signal, timeout = 1000 }: WaitOptions = {},\n): Promise<void> {\n const deadline = Date.now() + timeout;\n\n for (;;) {\n if (signal?.aborted) throw abortReason(signal);\n\n if (await predicate()) return;\n\n if (Date.now() >= deadline) break;\n\n await delay(interval, { signal });\n }\n\n throw new AssayTimeoutError(`waitUntil timed out after ${timeout}ms`);\n}\n\n/** Retry an assertion until it stops throwing. */\nexport async function retry(\n assertion: () => void | Promise<void>,\n { interval = 50, message, signal, timeout = 1000 }: RetryOptions = {},\n): Promise<void> {\n const deadline = Date.now() + timeout;\n let lastError: unknown;\n\n for (;;) {\n if (signal?.aborted) throw abortReason(signal);\n\n try {\n await assertion();\n\n return;\n } catch (error) {\n lastError = error;\n }\n\n if (Date.now() >= deadline) break;\n\n await delay(interval, { signal });\n }\n\n const timing = `retry 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 return new Promise((resolve, reject) => {\n if (signal?.aborted) {\n reject(abortReason(signal));\n\n return;\n }\n\n const dispose = () => {\n clearTimeout(timer);\n signal?.removeEventListener('abort', onAbort);\n target.removeEventListener(type, onEvent);\n };\n const onEvent = (e: Event): void => {\n dispose();\n resolve(e 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(abortReason(signal!));\n };\n\n target.addEventListener(type, onEvent, { once: true });\n signal?.addEventListener('abort', onAbort, { once: true });\n });\n}\n\n/** Resolve after one microtask tick. */\nexport const nextTick = (): Promise<void> =>\n new Promise((resolve) => {\n queueMicrotask(resolve);\n });\n"],"mappings":";;AAqBA,IAAM,KAAe,MACnB,EAAO,UAAU,IAAI,aAAa,8BAA8B,YAAY,GAKjE,KAAS,IAAK,GAAG,EAAE,cAAyB,CAAC,MACxD,IAAI,SAAS,GAAS,MAAW;CAC/B,IAAI,GAAQ,SAAS;EACnB,EAAO,EAAY,CAAM,CAAC;EAE1B;CACF;CAEA,IAAM,IAAQ,WAAW,GAAM,CAAE;CAEjC,SAAS,IAAa;EAEpB,AADA,GAAQ,oBAAoB,SAAS,CAAK,GAC1C,EAAQ;CACV;CAEA,SAAS,IAAc;EAErB,AADA,aAAa,CAAK,GAClB,EAAO,EAAY,CAAO,CAAC;CAC7B;CAEA,GAAQ,iBAAiB,SAAS,GAAO,EAAE,MAAM,GAAK,CAAC;AACzD,CAAC;AAGH,eAAsB,EACpB,GACA,EAAE,cAAW,IAAI,WAAQ,aAAU,QAAsB,CAAC,GAC3C;CACf,IAAM,IAAW,KAAK,IAAI,IAAI;CAE9B,SAAS;EACP,IAAI,GAAQ,SAAS,MAAM,EAAY,CAAM;EAE7C,IAAI,MAAM,EAAU,GAAG;EAEvB,IAAI,KAAK,IAAI,KAAK,GAAU;EAE5B,MAAM,EAAM,GAAU,EAAE,UAAO,CAAC;CAClC;CAEA,MAAM,IAAI,EAAkB,6BAA6B,EAAQ,GAAG;AACtE;AAGA,eAAsB,EACpB,GACA,EAAE,cAAW,IAAI,YAAS,WAAQ,aAAU,QAAuB,CAAC,GACrD;CACf,IAAM,IAAW,KAAK,IAAI,IAAI,GAC1B;CAEJ,SAAS;EACP,IAAI,GAAQ,SAAS,MAAM,EAAY,CAAM;EAE7C,IAAI;GACF,MAAM,EAAU;GAEhB;EACF,SAAS,GAAO;GACd,IAAY;EACd;EAEA,IAAI,KAAK,IAAI,KAAK,GAAU;EAE5B,MAAM,EAAM,GAAU,EAAE,UAAO,CAAC;CAClC;CAEA,IAAM,IAAS,yBAAyB,EAAQ,KAC1C,IAAU,IAAU,GAAG,EAAQ,IAAI,EAAO,KAAK,GAC/C,IAAS,aAAqB,QAAQ,EAAU,UAAU,KAAa,OAAO,KAAK,OAAO,CAAS;CAEzG,MAAM,IAAI,EAAkB,IAAS,GAAG,EAAQ,IAAI,MAAW,GAAS,EAAE,OAAO,EAAU,CAAC;AAC9F;AAGA,SAAgB,EACd,GACA,GACA,EAAE,WAAQ,aAAU,QAAwC,CAAC,GACjD;CACZ,OAAO,IAAI,SAAS,GAAS,MAAW;EACtC,IAAI,GAAQ,SAAS;GACnB,EAAO,EAAY,CAAM,CAAC;GAE1B;EACF;EAEA,IAAM,UAAgB;GAGpB,AAFA,aAAa,CAAK,GAClB,GAAQ,oBAAoB,SAAS,CAAO,GAC5C,EAAO,oBAAoB,GAAM,CAAO;EAC1C,GACM,KAAW,MAAmB;GAElC,AADA,EAAQ,GACR,EAAQ,CAAM;EAChB,GACM,IAAQ,iBAAiB;GAE7B,AADA,EAAQ,GACR,EAAO,IAAI,EAAkB,kBAAkB,EAAK,oBAAoB,EAAQ,GAAG,CAAC;EACtF,GAAG,CAAO,GACJ,UAAgB;GAEpB,AADA,EAAQ,GACR,EAAO,EAAY,CAAO,CAAC;EAC7B;EAGA,AADA,EAAO,iBAAiB,GAAM,GAAS,EAAE,MAAM,GAAK,CAAC,GACrD,GAAQ,iBAAiB,SAAS,GAAS,EAAE,MAAM,GAAK,CAAC;CAC3D,CAAC;AACH;AAGA,IAAa,UACX,IAAI,SAAS,MAAY;CACvB,eAAe,CAAO;AACxB,CAAC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vielzeug/assay",
3
- "version": "1.0.0",
3
+ "version": "2.0.1",
4
4
  "description": "Framework-agnostic DOM testing primitives — scoped queries and synchronous event dispatchers",
5
5
  "type": "module",
6
6
  "files": [
@@ -11,7 +11,6 @@
11
11
  "types": "dist/index.d.ts",
12
12
  "exports": {
13
13
  ".": {
14
- "source": "./src/index.ts",
15
14
  "types": "./dist/index.d.ts",
16
15
  "import": "./dist/index.js",
17
16
  "require": "./dist/index.cjs"
@@ -35,10 +34,10 @@
35
34
  "node": ">=22"
36
35
  },
37
36
  "devDependencies": {
38
- "@types/node": "^26.1.0",
39
- "jsdom": "^29.1.1",
37
+ "@types/node": "^26.1.2",
38
+ "jsdom": "^30.0.1",
40
39
  "typescript": "^6.0.3",
41
- "vite": "^8.1.3",
40
+ "vite": "^8.2.0",
42
41
  "vitest": "^4.1.10"
43
42
  }
44
43
  }
package/dist/_dev.cjs DELETED
@@ -1,2 +0,0 @@
1
- var e=!globalThis.__ASSAY_PROD__,t=new Set;function n(n,r){!e||t.has(n)||(t.add(n),console.warn(`[@vielzeug/assay] ${r}`))}exports.warnOnce=n;
2
- //# sourceMappingURL=_dev.cjs.map
package/dist/_dev.cjs.map DELETED
@@ -1 +0,0 @@
1
- {"version":3,"file":"_dev.cjs","names":[],"sources":["../src/_dev.ts"],"sourcesContent":["const isDev = !(globalThis as { __ASSAY_PROD__?: boolean }).__ASSAY_PROD__;\n\nconst warnedKeys = new Set<string>();\n\n/**\n * @internal Warn at most once per `key` for the lifetime of the module — used for\n * environment-capability fallbacks (e.g. missing `PointerEvent`/`TouchEvent`), which would\n * otherwise log once per dispatched event in a suite that fires hundreds of them.\n */\nexport function warnOnce(key: string, msg: string): void {\n if (!isDev || warnedKeys.has(key)) return;\n\n warnedKeys.add(key);\n console.warn(`[@vielzeug/assay] ${msg}`);\n}\n\n/** @internal Test-only — clears \"warned once\" state so fallback-warning tests can run in any order. */\nexport function _resetWarnings(): void {\n warnedKeys.clear();\n}\n"],"mappings":"AAAA,IAAM,EAAQ,CAAE,WAA4C,eAEtD,EAAa,IAAI,IAOvB,SAAgB,EAAS,EAAa,EAAmB,CACnD,CAAC,GAAS,EAAW,IAAI,CAAG,IAEhC,EAAW,IAAI,CAAG,EAClB,QAAQ,KAAK,qBAAqB,GAAK,EACzC"}
package/dist/_dev.js DELETED
@@ -1,9 +0,0 @@
1
- //#region src/_dev.ts
2
- var e = !globalThis.__ASSAY_PROD__, t = /* @__PURE__ */ new Set();
3
- function n(n, r) {
4
- !e || t.has(n) || (t.add(n), console.warn(`[@vielzeug/assay] ${r}`));
5
- }
6
- //#endregion
7
- export { n as warnOnce };
8
-
9
- //# sourceMappingURL=_dev.js.map
package/dist/_dev.js.map DELETED
@@ -1 +0,0 @@
1
- {"version":3,"file":"_dev.js","names":[],"sources":["../src/_dev.ts"],"sourcesContent":["const isDev = !(globalThis as { __ASSAY_PROD__?: boolean }).__ASSAY_PROD__;\n\nconst warnedKeys = new Set<string>();\n\n/**\n * @internal Warn at most once per `key` for the lifetime of the module — used for\n * environment-capability fallbacks (e.g. missing `PointerEvent`/`TouchEvent`), which would\n * otherwise log once per dispatched event in a suite that fires hundreds of them.\n */\nexport function warnOnce(key: string, msg: string): void {\n if (!isDev || warnedKeys.has(key)) return;\n\n warnedKeys.add(key);\n console.warn(`[@vielzeug/assay] ${msg}`);\n}\n\n/** @internal Test-only — clears \"warned once\" state so fallback-warning tests can run in any order. */\nexport function _resetWarnings(): void {\n warnedKeys.clear();\n}\n"],"mappings":";AAAA,IAAM,IAAQ,CAAE,WAA4C,gBAEtD,oBAAa,IAAI,IAAY;AAOnC,SAAgB,EAAS,GAAa,GAAmB;CACnD,CAAC,KAAS,EAAW,IAAI,CAAG,MAEhC,EAAW,IAAI,CAAG,GAClB,QAAQ,KAAK,qBAAqB,GAAK;AACzC"}