@itmetsam/samurai-framework 0.21.3 → 0.21.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/dist/api.d.ts +3 -1
- package/dist/assert/asymmetric.d.ts +29 -0
- package/dist/assert/asymmetric.js +166 -0
- package/dist/assert/element-probes.d.ts +20 -0
- package/dist/assert/element-probes.js +45 -0
- package/dist/assert/expect.d.ts +20 -9
- package/dist/assert/expect.js +12 -4
- package/dist/assert/locator-assertion-base.d.ts +52 -0
- package/dist/assert/locator-assertion-base.js +78 -0
- package/dist/assert/locator-assertions.d.ts +26 -49
- package/dist/assert/locator-assertions.js +35 -78
- package/dist/assert/more-locator-assertions.d.ts +30 -0
- package/dist/assert/more-locator-assertions.js +86 -0
- package/dist/assert/retry-assertions.d.ts +28 -0
- package/dist/assert/retry-assertions.js +113 -0
- package/dist/assert/text-match.d.ts +2 -0
- package/dist/assert/text-match.js +8 -0
- package/dist/assert/value-assertions.d.ts +29 -1
- package/dist/assert/value-assertions.js +84 -5
- package/dist/assert/value-matchers.d.ts +19 -0
- package/dist/assert/value-matchers.js +53 -0
- package/dist/browser/browser.d.ts +9 -0
- package/dist/browser/browser.js +25 -5
- package/dist/browser/cdp.d.ts +11 -0
- package/dist/browser/cdp.js +28 -0
- package/dist/browser/chrome-launch.d.ts +4 -0
- package/dist/browser/chrome-launch.js +2 -2
- package/dist/browser/index.d.ts +5 -0
- package/dist/browser/index.js +4 -0
- package/dist/browser/launch-firefox.js +1 -1
- package/dist/browser/launch-flags.d.ts +4 -1
- package/dist/browser/launch-flags.js +18 -5
- package/dist/browser/navigation-error.d.ts +5 -1
- package/dist/browser/navigation-error.js +10 -2
- package/dist/browser/page-content.d.ts +5 -0
- package/dist/browser/page-content.js +43 -0
- package/dist/browser/page-evaluate.d.ts +9 -0
- package/dist/browser/page-evaluate.js +20 -0
- package/dist/browser/page-locators.d.ts +1 -1
- package/dist/browser/page-locators.js +1 -1
- package/dist/browser/page-navigation.d.ts +18 -0
- package/dist/browser/page-navigation.js +52 -2
- package/dist/browser/page-settings.d.ts +1 -1
- package/dist/browser/page.d.ts +52 -1
- package/dist/browser/page.js +66 -2
- package/dist/browser/session-capabilities.d.ts +10 -0
- package/dist/browser/session-capabilities.js +11 -0
- package/dist/browser/session.d.ts +2 -1
- package/dist/browser/session.js +3 -3
- package/dist/browser/storage-state.d.ts +6 -0
- package/dist/browser/storage-state.js +37 -0
- package/dist/browser/use-applier.d.ts +27 -0
- package/dist/browser/use-applier.js +94 -0
- package/dist/browser/viewport.d.ts +3 -2
- package/dist/browser/viewport.js +15 -2
- package/dist/config/config.js +2 -0
- package/dist/config/use-options.d.ts +16 -0
- package/dist/config/use-options.js +128 -0
- package/dist/locator/element-access.d.ts +2 -0
- package/dist/locator/element-reads.d.ts +17 -0
- package/dist/locator/element-reads.js +31 -0
- package/dist/locator/evaluate.d.ts +12 -0
- package/dist/locator/evaluate.js +36 -0
- package/dist/locator/locator.d.ts +23 -0
- package/dist/locator/locator.js +36 -1
- package/dist/logger/warn-once.d.ts +4 -0
- package/dist/logger/warn-once.js +13 -0
- package/dist/runner/attachments.d.ts +16 -0
- package/dist/runner/attachments.js +60 -0
- package/dist/runner/fixture-args.d.ts +15 -0
- package/dist/runner/fixture-args.js +95 -0
- package/dist/runner/fixture-defs.d.ts +22 -0
- package/dist/runner/fixture-defs.js +103 -0
- package/dist/runner/fixture-scope.d.ts +33 -0
- package/dist/runner/fixture-scope.js +156 -0
- package/dist/runner/fixtures.d.ts +34 -0
- package/dist/runner/fixtures.js +39 -0
- package/dist/runner/hooks.d.ts +6 -1
- package/dist/runner/hooks.js +15 -3
- package/dist/runner/registry.d.ts +3 -39
- package/dist/runner/registry.js +93 -19
- package/dist/runner/reporter.d.ts +7 -1
- package/dist/runner/reporter.js +3 -0
- package/dist/runner/run-test.d.ts +1 -3
- package/dist/runner/run-test.js +92 -34
- package/dist/runner/steps.d.ts +13 -0
- package/dist/runner/steps.js +67 -0
- package/dist/runner/suite.d.ts +5 -0
- package/dist/runner/suite.js +8 -0
- package/dist/runner/test-annotations.d.ts +16 -0
- package/dist/runner/test-annotations.js +47 -0
- package/dist/runner/test-function.d.ts +103 -0
- package/dist/runner/test-function.js +1 -0
- package/dist/runner/test-info.d.ts +104 -0
- package/dist/runner/test-info.js +169 -0
- package/dist/runner/test-runner.js +2 -1
- package/dist/runner/test-timeout.d.ts +22 -0
- package/dist/runner/test-timeout.js +45 -0
- package/dist/runner/use-options.d.ts +7 -0
- package/dist/runner/use-options.js +18 -0
- package/dist/runner/worker-session.js +2 -1
- package/dist/script/call-function.d.ts +7 -0
- package/dist/script/call-function.js +21 -3
- package/dist/script/element-handle.d.ts +1 -1
- package/dist/script/element-handle.js +1 -1
- package/dist/types/config.d.ts +3 -2
- package/dist/types/extend.d.ts +30 -0
- package/dist/types/test.d.ts +120 -0
- package/dist/types/use.d.ts +47 -0
- package/dist/wait/wait-until.d.ts +3 -1
- package/dist/wait/wait-until.js +7 -2
- package/package.json +1 -1
|
@@ -1,93 +1,44 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
import { captured, matchesText, normalizedCheck, textCheck, } from "./text-match.js";
|
|
7
|
-
/**
|
|
8
|
-
* Re-reads the element until `check` passes or the timeout expires, and resolves with the read that passed. A
|
|
9
|
-
* timeout fails as an `AssertionError` with the last read, or as "could not read" when no read completed.
|
|
10
|
-
*/
|
|
11
|
-
export async function readEventually({ name, locator, expected, read, check, timeout: perCall, mustExist = false, call, }) {
|
|
12
|
-
const timeout = await resolveTimeout(perCall);
|
|
13
|
-
try {
|
|
14
|
-
return await waitUntil(read, (actual) => (mustExist && actual === null ? false : check(actual)), { timeout });
|
|
15
|
-
}
|
|
16
|
-
catch (err) {
|
|
17
|
-
if (!(err instanceof WaitTimeoutError))
|
|
18
|
-
throw err;
|
|
19
|
-
// Reads never resolve to undefined, so an undefined `last` means no read completed
|
|
20
|
-
if (err.last === undefined || (mustExist && err.last === null)) {
|
|
21
|
-
throw new Error(`${call ?? `expect(locator).${name}`}: could not read ${locator.selector} within ${timeout}ms`);
|
|
22
|
-
}
|
|
23
|
-
throw new AssertionError({
|
|
24
|
-
matcher: name,
|
|
25
|
-
expected,
|
|
26
|
-
actual: err.last,
|
|
27
|
-
locator: locator.selector,
|
|
28
|
-
call,
|
|
29
|
-
});
|
|
30
|
-
}
|
|
31
|
-
}
|
|
32
|
-
export class LocatorAssertions {
|
|
33
|
-
constructor(locator, negated = false) {
|
|
34
|
-
this.locator = locator;
|
|
35
|
-
this.negated = negated;
|
|
36
|
-
}
|
|
1
|
+
import { MoreLocatorAssertions } from "./more-locator-assertions.js";
|
|
2
|
+
import { readCheckedState } from "./element-probes.js";
|
|
3
|
+
import { matchesText, normalizedCheck } from "./text-match.js";
|
|
4
|
+
export { readEventually } from "./locator-assertion-base.js";
|
|
5
|
+
export class LocatorAssertions extends MoreLocatorAssertions {
|
|
37
6
|
/** Inverts the next matcher: it retries until the condition is false */
|
|
38
7
|
get not() {
|
|
39
8
|
return new LocatorAssertions(this.locator, !this.negated);
|
|
40
9
|
}
|
|
41
|
-
/**
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
assertEventually(matcher, expected, read, check, options, mustExist = false, save) {
|
|
46
|
-
const name = this.negated ? `not.${matcher}` : matcher;
|
|
47
|
-
return track(`expect(locator).${name}()`, readEventually({
|
|
48
|
-
name,
|
|
49
|
-
locator: this.locator,
|
|
50
|
-
expected,
|
|
51
|
-
read,
|
|
52
|
-
check: (actual) => check(actual) !== this.negated,
|
|
53
|
-
timeout: options?.timeout,
|
|
54
|
-
mustExist,
|
|
55
|
-
}).then((actual) => save?.(actual)));
|
|
10
|
+
/** The element exists, has a box and isn't hidden by CSS; `visible: false` expects the opposite, as `toBeHidden` */
|
|
11
|
+
toBeVisible(options) {
|
|
12
|
+
const want = options?.visible ?? true;
|
|
13
|
+
return this.assertEventually("toBeVisible", want, () => this.locator.isVisible(), (visible) => visible === want, options);
|
|
56
14
|
}
|
|
57
15
|
/**
|
|
58
|
-
*
|
|
59
|
-
*
|
|
16
|
+
* Trimmed text content equals `expected` (a string, compared with whitespace collapsed on both sides) or matches it (RegExp).
|
|
17
|
+
* An array expects one entry per matched element, in order, and as many entries as elements
|
|
60
18
|
*/
|
|
61
|
-
assertText(matcher, expected, read, mode, options) {
|
|
62
|
-
let save;
|
|
63
|
-
if (options?.output !== undefined) {
|
|
64
|
-
const call = `expect(locator).${this.negated ? "not." : ""}${matcher}`;
|
|
65
|
-
if (this.negated)
|
|
66
|
-
throw new Error(`${call}: output can't be saved from a negated check`);
|
|
67
|
-
const keep = bindOutput(call, options.output);
|
|
68
|
-
save = (actual) => keep(captured(expected, actual));
|
|
69
|
-
}
|
|
70
|
-
return this.assertEventually(matcher, expected, read, textCheck(expected, mode), options, false, save);
|
|
71
|
-
}
|
|
72
|
-
/** For matchers that need an element: `read` answers null when there is none, which never passes */
|
|
73
|
-
assertExisting(matcher, expected, read, check, options) {
|
|
74
|
-
return this.assertEventually(matcher, expected, read, (actual) => actual !== null && check(actual), options, true);
|
|
75
|
-
}
|
|
76
|
-
toBeVisible(options) {
|
|
77
|
-
return this.assertEventually("toBeVisible", true, () => this.locator.isVisible(), (visible) => visible, options);
|
|
78
|
-
}
|
|
79
|
-
/** Trimmed text content equals `expected` (string) or matches it (RegExp) */
|
|
80
19
|
toHaveText(expected, options) {
|
|
81
|
-
|
|
20
|
+
if (Array.isArray(expected))
|
|
21
|
+
return this.assertTexts("toHaveText", expected, "exact", options);
|
|
22
|
+
return this.assertText("toHaveText", expected, () => this.locator.textContent(), "exact", options, true);
|
|
82
23
|
}
|
|
83
|
-
/**
|
|
24
|
+
/**
|
|
25
|
+
* Text content contains `expected` (a string, compared with whitespace collapsed on both sides) or matches it (RegExp).
|
|
26
|
+
* An array expects its entries, in order, in some of the matched elements (one entry per element, others may sit between)
|
|
27
|
+
*/
|
|
84
28
|
toContainText(expected, options) {
|
|
85
|
-
|
|
29
|
+
if (Array.isArray(expected))
|
|
30
|
+
return this.assertTexts("toContainText", expected, "contains", options);
|
|
31
|
+
return this.assertText("toContainText", expected, () => this.locator.textContent(), "contains", options, true);
|
|
86
32
|
}
|
|
87
33
|
toHaveValue(expected, options) {
|
|
88
34
|
return this.assertText("toHaveValue", expected, () => this.locator.inputValue(), "exact", options);
|
|
89
35
|
}
|
|
90
|
-
toHaveAttribute(name,
|
|
36
|
+
toHaveAttribute(name, expectedOrOptions, options) {
|
|
37
|
+
if (typeof expectedOrOptions !== "string" &&
|
|
38
|
+
!(expectedOrOptions instanceof RegExp)) {
|
|
39
|
+
return this.assertEventually("toHaveAttribute", `attribute ${name} present`, () => this.locator.getAttribute(name), (value) => value !== null, expectedOrOptions);
|
|
40
|
+
}
|
|
41
|
+
const expected = expectedOrOptions;
|
|
91
42
|
return this.assertText("toHaveAttribute", expected, () => this.locator.getAttribute(name), "exact", options);
|
|
92
43
|
}
|
|
93
44
|
toHaveCount(expected, options) {
|
|
@@ -99,11 +50,17 @@ export class LocatorAssertions {
|
|
|
99
50
|
}
|
|
100
51
|
/** The element exists and is neither `:disabled` nor `aria-disabled="true"`; a missing element never passes, not even with `.not` */
|
|
101
52
|
toBeEnabled(options) {
|
|
102
|
-
|
|
53
|
+
const want = options?.enabled ?? true;
|
|
54
|
+
return this.assertExisting("toBeEnabled", want, () => this.locator.enabledState(), (enabled) => enabled === want, options);
|
|
103
55
|
}
|
|
104
56
|
/** A checked checkbox or radio input, or an element with `aria-checked="true"`; a missing element never passes, not even with `.not` */
|
|
105
57
|
toBeChecked(options) {
|
|
106
|
-
|
|
58
|
+
const want = options?.indeterminate ? "mixed" : (options?.checked ?? true);
|
|
59
|
+
// Only "mixed" needs the three-state read; true and false use the plain one
|
|
60
|
+
if (want === "mixed") {
|
|
61
|
+
return this.assertExisting("toBeChecked", want, () => readCheckedState(this.locator), (state) => state === "mixed", options);
|
|
62
|
+
}
|
|
63
|
+
return this.assertExisting("toBeChecked", want, () => this.locator.isChecked(), (checked) => checked === want, options);
|
|
107
64
|
}
|
|
108
65
|
/** The class attribute equals `expected` (string, whitespace collapsed and trimmed on both sides) or matches it (RegExp, tested on the attribute as written) */
|
|
109
66
|
toHaveClass(expected, options) {
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { LocatorAssertionBase, type LocatorAssertionOptions, type TextAssertionOptions } from "./locator-assertion-base.js";
|
|
2
|
+
export interface InViewportOptions extends LocatorAssertionOptions {
|
|
3
|
+
/** The least share (0 to 1) of the element that must be inside the viewport. Default: any part */
|
|
4
|
+
ratio?: number;
|
|
5
|
+
}
|
|
6
|
+
/** The locator matchers that read the element with a small script of their own; `LocatorAssertions` adds the rest */
|
|
7
|
+
export declare class MoreLocatorAssertions extends LocatorAssertionBase {
|
|
8
|
+
/** An array of texts, one entry per matched element; `read` answers the texts of every match */
|
|
9
|
+
protected assertTexts(matcher: string, expected: Array<string | RegExp>, mode: "exact" | "contains", options?: TextAssertionOptions): Promise<void>;
|
|
10
|
+
/** The element has the focus; a missing element never passes, not even with `.not` */
|
|
11
|
+
toBeFocused(options?: LocatorAssertionOptions): Promise<void>;
|
|
12
|
+
/** The element's JS property `property` deep-equals `expected`; a missing element never passes, not even with `.not` */
|
|
13
|
+
toHaveJSProperty(property: string, expected: unknown, options?: LocatorAssertionOptions): Promise<void>;
|
|
14
|
+
/** Part of the element (or at least `ratio` of it) is inside the viewport; a missing element never passes, not even with `.not` */
|
|
15
|
+
toBeInViewport(options?: InViewportOptions): Promise<void>;
|
|
16
|
+
/** At least one element matches (`attached: false`, or `.not`, means none does) */
|
|
17
|
+
toBeAttached(options?: LocatorAssertionOptions & {
|
|
18
|
+
attached?: boolean;
|
|
19
|
+
}): Promise<void>;
|
|
20
|
+
/** The element is disabled (the opposite of `toBeEnabled`); a missing element never passes, not even with `.not` */
|
|
21
|
+
toBeDisabled(options?: LocatorAssertionOptions): Promise<void>;
|
|
22
|
+
/** The element accepts typing (`editable: false` expects the opposite); a missing element never passes, not even with `.not` */
|
|
23
|
+
toBeEditable(options?: LocatorAssertionOptions & {
|
|
24
|
+
editable?: boolean;
|
|
25
|
+
}): Promise<void>;
|
|
26
|
+
/** The `id` equals `expected` (string) or matches it (RegExp); a missing element never passes, not even with `.not` */
|
|
27
|
+
toHaveId(expected: string | RegExp, options?: LocatorAssertionOptions): Promise<void>;
|
|
28
|
+
/** An input or textarea with an empty value, or any other element without element children and with no text besides whitespace; a missing element never passes, not even with `.not` */
|
|
29
|
+
toBeEmpty(options?: LocatorAssertionOptions): Promise<void>;
|
|
30
|
+
}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import { isDeepStrictEqual } from "node:util";
|
|
2
|
+
import { track } from "./pending-assertions.js";
|
|
3
|
+
import { LocatorAssertionBase, readEventually, } from "./locator-assertion-base.js";
|
|
4
|
+
import { readEmpty, readFocused, readId, readJSProperty, readViewportRatio, } from "./element-probes.js";
|
|
5
|
+
import { matchesText } from "./text-match.js";
|
|
6
|
+
/** Floating point slack when a ratio is compared */
|
|
7
|
+
const RATIO_EPSILON = 1e-6;
|
|
8
|
+
const normalize = (value) => value.replace(/\s+/g, " ").trim();
|
|
9
|
+
/** One text expectation against one element's text: a string after normalising both, a RegExp as written (trimmed text) */
|
|
10
|
+
function textMatches(actual, expected, mode) {
|
|
11
|
+
if (expected instanceof RegExp)
|
|
12
|
+
return matchesText(actual, expected, mode);
|
|
13
|
+
return matchesText(normalize(actual), normalize(expected), mode);
|
|
14
|
+
}
|
|
15
|
+
/** Whether `texts` (one per element) satisfy `expected`: all in order for "exact", in order but not necessarily adjacent for "contains" */
|
|
16
|
+
function arrayTextMatches(texts, expected, mode) {
|
|
17
|
+
if (mode === "exact") {
|
|
18
|
+
return (texts.length === expected.length &&
|
|
19
|
+
expected.every((item, i) => textMatches(texts[i], item, "exact")));
|
|
20
|
+
}
|
|
21
|
+
let next = 0;
|
|
22
|
+
for (const text of texts) {
|
|
23
|
+
if (next < expected.length &&
|
|
24
|
+
textMatches(text, expected[next], "contains"))
|
|
25
|
+
next++;
|
|
26
|
+
}
|
|
27
|
+
return next === expected.length;
|
|
28
|
+
}
|
|
29
|
+
/** The locator matchers that read the element with a small script of their own; `LocatorAssertions` adds the rest */
|
|
30
|
+
export class MoreLocatorAssertions extends LocatorAssertionBase {
|
|
31
|
+
/** An array of texts, one entry per matched element; `read` answers the texts of every match */
|
|
32
|
+
assertTexts(matcher, expected, mode, options) {
|
|
33
|
+
if (options?.output !== undefined) {
|
|
34
|
+
const call = `expect(locator).${this.negated ? "not." : ""}${matcher}`;
|
|
35
|
+
throw new Error(`${call}: output can't be saved from an array of texts`);
|
|
36
|
+
}
|
|
37
|
+
return this.assertEventually(matcher, expected, async () => (await this.locator.allTextContents()).map((t) => t.trim()), (texts) => arrayTextMatches(texts, expected, mode), options);
|
|
38
|
+
}
|
|
39
|
+
/** The element has the focus; a missing element never passes, not even with `.not` */
|
|
40
|
+
toBeFocused(options) {
|
|
41
|
+
return this.assertExisting("toBeFocused", true, () => readFocused(this.locator), (focused) => focused, options);
|
|
42
|
+
}
|
|
43
|
+
/** The element's JS property `property` deep-equals `expected`; a missing element never passes, not even with `.not` */
|
|
44
|
+
toHaveJSProperty(property, expected, options) {
|
|
45
|
+
const name = this.matcherName("toHaveJSProperty");
|
|
46
|
+
return track(`expect(locator).${name}()`, readEventually({
|
|
47
|
+
name,
|
|
48
|
+
locator: this.locator,
|
|
49
|
+
expected,
|
|
50
|
+
read: () => readJSProperty(this.locator, property),
|
|
51
|
+
check: (read) => isDeepStrictEqual(read?.value, expected) !== this.negated,
|
|
52
|
+
timeout: options?.timeout,
|
|
53
|
+
mustExist: true,
|
|
54
|
+
display: (read) => read?.value,
|
|
55
|
+
}).then(() => undefined));
|
|
56
|
+
}
|
|
57
|
+
/** Part of the element (or at least `ratio` of it) is inside the viewport; a missing element never passes, not even with `.not` */
|
|
58
|
+
toBeInViewport(options) {
|
|
59
|
+
const ratio = options?.ratio ?? 0;
|
|
60
|
+
return this.assertExisting("toBeInViewport", ratio > 0 ? `ratio >= ${ratio}` : "ratio > 0", () => readViewportRatio(this.locator), (actual) => (ratio > 0 ? actual >= ratio - RATIO_EPSILON : actual > 0), options);
|
|
61
|
+
}
|
|
62
|
+
/** At least one element matches (`attached: false`, or `.not`, means none does) */
|
|
63
|
+
toBeAttached(options) {
|
|
64
|
+
const want = options?.attached ?? true;
|
|
65
|
+
return this.assertEventually("toBeAttached", want, async () => (await this.locator.count()) > 0, (attached) => attached === want, options);
|
|
66
|
+
}
|
|
67
|
+
/** The element is disabled (the opposite of `toBeEnabled`); a missing element never passes, not even with `.not` */
|
|
68
|
+
toBeDisabled(options) {
|
|
69
|
+
return this.assertExisting("toBeDisabled", true, () => this.locator.enabledState(), (enabled) => !enabled, options);
|
|
70
|
+
}
|
|
71
|
+
/** The element accepts typing (`editable: false` expects the opposite); a missing element never passes, not even with `.not` */
|
|
72
|
+
toBeEditable(options) {
|
|
73
|
+
const want = options?.editable ?? true;
|
|
74
|
+
return this.assertExisting("toBeEditable", want, async () => (await this.locator.count()) === 0
|
|
75
|
+
? null
|
|
76
|
+
: await this.locator.isEditable(), (editable) => editable === want, options);
|
|
77
|
+
}
|
|
78
|
+
/** The `id` equals `expected` (string) or matches it (RegExp); a missing element never passes, not even with `.not` */
|
|
79
|
+
toHaveId(expected, options) {
|
|
80
|
+
return this.assertExisting("toHaveId", expected, () => readId(this.locator), (id) => matchesText(id, expected, "exact"), options);
|
|
81
|
+
}
|
|
82
|
+
/** An input or textarea with an empty value, or any other element without element children and with no text besides whitespace; a missing element never passes, not even with `.not` */
|
|
83
|
+
toBeEmpty(options) {
|
|
84
|
+
return this.assertExisting("toBeEmpty", true, () => readEmpty(this.locator), (empty) => empty, options);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import type { ValueAssertions } from "./value-assertions.js";
|
|
2
|
+
export interface RetryOptions {
|
|
3
|
+
/** Time (ms) to keep trying; 0 tries once */
|
|
4
|
+
timeout?: number;
|
|
5
|
+
/** Pauses (ms) between tries; the last one repeats. Default: 100, 250, 500, then 1000 */
|
|
6
|
+
intervals?: number[];
|
|
7
|
+
}
|
|
8
|
+
export interface PollOptions extends RetryOptions {
|
|
9
|
+
/** Replaces the first line of a failure (`expect.poll().<matcher>`) */
|
|
10
|
+
message?: string;
|
|
11
|
+
}
|
|
12
|
+
/** The value matchers of `expect(value)`, each returning a promise that settles once the polled value passes (or the time is up) */
|
|
13
|
+
export type PollAssertions<T> = {
|
|
14
|
+
[K in keyof Omit<ValueAssertions<T>, "not">]: ValueAssertions<T>[K] extends (...args: infer A) => unknown ? (...args: A) => Promise<void> : never;
|
|
15
|
+
} & {
|
|
16
|
+
readonly not: PollAssertions<T>;
|
|
17
|
+
};
|
|
18
|
+
/**
|
|
19
|
+
* `expect(block).toPass()`: runs `block` (sync or async) until it stops throwing. The default timeout is 0, one try.
|
|
20
|
+
* With `negated`, until it throws. A failure rethrows the last error as it was.
|
|
21
|
+
*/
|
|
22
|
+
export declare function toPass(block: () => unknown, options: RetryOptions | undefined, negated: boolean): Promise<void>;
|
|
23
|
+
/**
|
|
24
|
+
* `expect.poll(fn)`: the value matchers of `expect(value)`, each calling `fn` (sync or async) again and again until
|
|
25
|
+
* the matcher passes or the time is up. The timeout defaults to the expect timeout; 0 tries once.
|
|
26
|
+
* A function that throws counts as a failed try.
|
|
27
|
+
*/
|
|
28
|
+
export declare function poll<T>(fn: () => T | Promise<T>, options?: PollOptions, negated?: boolean): PollAssertions<Awaited<T>>;
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import { resolveTimeout } from "../config/timeouts.js";
|
|
2
|
+
import { waitUntil, WaitTimeoutError } from "../wait/wait-until.js";
|
|
3
|
+
import { AssertionError } from "./assertion-error.js";
|
|
4
|
+
import { track } from "./pending-assertions.js";
|
|
5
|
+
/** Pauses (ms) between tries; the last one repeats */
|
|
6
|
+
const DEFAULT_INTERVALS = [100, 250, 500, 1000];
|
|
7
|
+
function asError(thrown) {
|
|
8
|
+
return thrown instanceof Error ? thrown : new Error(String(thrown));
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* `expect(block).toPass()`: runs `block` (sync or async) until it stops throwing. The default timeout is 0, one try.
|
|
12
|
+
* With `negated`, until it throws. A failure rethrows the last error as it was.
|
|
13
|
+
*/
|
|
14
|
+
export function toPass(block, options, negated) {
|
|
15
|
+
const timeout = options?.timeout ?? 0;
|
|
16
|
+
const name = negated ? "not.toPass" : "toPass";
|
|
17
|
+
const run = async () => {
|
|
18
|
+
try {
|
|
19
|
+
await waitUntil(async () => {
|
|
20
|
+
try {
|
|
21
|
+
await block();
|
|
22
|
+
return {};
|
|
23
|
+
}
|
|
24
|
+
catch (error) {
|
|
25
|
+
return { error: error ?? new Error("undefined") };
|
|
26
|
+
}
|
|
27
|
+
}, (attempt) => (attempt.error === undefined) !== negated, { timeout, intervals: options?.intervals ?? DEFAULT_INTERVALS });
|
|
28
|
+
}
|
|
29
|
+
catch (err) {
|
|
30
|
+
if (!(err instanceof WaitTimeoutError))
|
|
31
|
+
throw err;
|
|
32
|
+
const last = err.last;
|
|
33
|
+
if (last === undefined) {
|
|
34
|
+
throw new Error(`expect(value).${name}: timed out after ${timeout}ms`);
|
|
35
|
+
}
|
|
36
|
+
if (negated) {
|
|
37
|
+
throw new AssertionError({
|
|
38
|
+
matcher: name,
|
|
39
|
+
expected: "the block to throw",
|
|
40
|
+
actual: "it did not throw",
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
throw asError(last.error);
|
|
44
|
+
}
|
|
45
|
+
};
|
|
46
|
+
return track(`expect(value).${negated ? "not." : ""}toPass()`, run());
|
|
47
|
+
}
|
|
48
|
+
/** Runs one matcher of `ValueAssertions` against `value` once; an `AssertionError` is a failed try, anything else (a misused matcher) propagates */
|
|
49
|
+
async function tryMatcher(value, negated, matcher, args) {
|
|
50
|
+
const { ValueAssertions } = await import("./value-assertions.js");
|
|
51
|
+
try {
|
|
52
|
+
const assertions = new ValueAssertions(value, negated);
|
|
53
|
+
assertions[matcher](...args);
|
|
54
|
+
return {};
|
|
55
|
+
}
|
|
56
|
+
catch (error) {
|
|
57
|
+
if (error instanceof AssertionError)
|
|
58
|
+
return { error };
|
|
59
|
+
throw error;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
async function pollMatcher(fn, options, negated, matcher, args) {
|
|
63
|
+
const timeout = await resolveTimeout(options?.timeout);
|
|
64
|
+
const name = negated ? `not.${matcher}` : matcher;
|
|
65
|
+
const call = options?.message ?? `expect.poll().${name}`;
|
|
66
|
+
try {
|
|
67
|
+
await waitUntil(async () => {
|
|
68
|
+
let value;
|
|
69
|
+
try {
|
|
70
|
+
value = await fn();
|
|
71
|
+
}
|
|
72
|
+
catch (error) {
|
|
73
|
+
return { error };
|
|
74
|
+
}
|
|
75
|
+
return tryMatcher(value, negated, matcher, args);
|
|
76
|
+
}, (attempt) => attempt.error === undefined, { timeout, intervals: options?.intervals ?? DEFAULT_INTERVALS });
|
|
77
|
+
}
|
|
78
|
+
catch (err) {
|
|
79
|
+
if (!(err instanceof WaitTimeoutError))
|
|
80
|
+
throw err;
|
|
81
|
+
const last = err.last;
|
|
82
|
+
if (last === undefined) {
|
|
83
|
+
throw new Error(`expect.poll().${name}: timed out after ${timeout}ms`);
|
|
84
|
+
}
|
|
85
|
+
if (last.error instanceof AssertionError) {
|
|
86
|
+
throw new AssertionError({
|
|
87
|
+
matcher: last.error.matcher,
|
|
88
|
+
expected: last.error.expected,
|
|
89
|
+
actual: last.error.actual,
|
|
90
|
+
call,
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
throw new Error(`expect.poll().${name}: the function threw: ${asError(last.error).message}`);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* `expect.poll(fn)`: the value matchers of `expect(value)`, each calling `fn` (sync or async) again and again until
|
|
98
|
+
* the matcher passes or the time is up. The timeout defaults to the expect timeout; 0 tries once.
|
|
99
|
+
* A function that throws counts as a failed try.
|
|
100
|
+
*/
|
|
101
|
+
export function poll(fn, options, negated = false) {
|
|
102
|
+
const target = {};
|
|
103
|
+
return new Proxy(target, {
|
|
104
|
+
get(_target, property) {
|
|
105
|
+
if (property === "not")
|
|
106
|
+
return poll(fn, options, !negated);
|
|
107
|
+
// Only matchers: anything else (`then`, which would make the object awaitable) is not there
|
|
108
|
+
if (typeof property !== "string" || !/^to[A-Z]/.test(property))
|
|
109
|
+
return undefined;
|
|
110
|
+
return (...args) => track(`expect.poll().${negated ? "not." : ""}${property}()`, pollMatcher(fn, options, negated, property, args));
|
|
111
|
+
},
|
|
112
|
+
});
|
|
113
|
+
}
|
|
@@ -10,3 +10,5 @@ export declare function captured(matcher: string | RegExp, actual: string): stri
|
|
|
10
10
|
export declare function textCheck(expected: string | RegExp, mode: "exact" | "contains"): (actual: string | null) => boolean;
|
|
11
11
|
/** A check for a text compared after normalising whitespace: a string equals it with whitespace collapsed and both sides trimmed, a RegExp tests it as written */
|
|
12
12
|
export declare function normalizedCheck(expected: string | RegExp): (actual: string) => boolean;
|
|
13
|
+
/** Like `textCheck`, but a string is compared with whitespace runs collapsed and both sides trimmed; a RegExp tests the text as written */
|
|
14
|
+
export declare function normalizedTextCheck(expected: string | RegExp, mode: "exact" | "contains"): (actual: string | null) => boolean;
|
|
@@ -28,3 +28,11 @@ export function normalizedCheck(expected) {
|
|
|
28
28
|
const normalize = (value) => value.replace(/\s+/g, " ").trim();
|
|
29
29
|
return (actual) => normalize(actual) === normalize(expected);
|
|
30
30
|
}
|
|
31
|
+
/** Like `textCheck`, but a string is compared with whitespace runs collapsed and both sides trimmed; a RegExp tests the text as written */
|
|
32
|
+
export function normalizedTextCheck(expected, mode) {
|
|
33
|
+
if (expected instanceof RegExp)
|
|
34
|
+
return textCheck(expected, mode);
|
|
35
|
+
const normalize = (value) => value.replace(/\s+/g, " ").trim();
|
|
36
|
+
const wanted = normalize(expected);
|
|
37
|
+
return (actual) => actual !== null && matchesText(normalize(actual), wanted, mode);
|
|
38
|
+
}
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { type RetryOptions } from "./retry-assertions.js";
|
|
2
|
+
import { type ThrowExpectation } from "./value-matchers.js";
|
|
1
3
|
export declare class ValueAssertions<T> {
|
|
2
4
|
private readonly actual;
|
|
3
5
|
private readonly negated;
|
|
@@ -8,8 +10,29 @@ export declare class ValueAssertions<T> {
|
|
|
8
10
|
private compare;
|
|
9
11
|
/** Strict identity (`Object.is`) */
|
|
10
12
|
toBe(expected: unknown): void;
|
|
11
|
-
/**
|
|
13
|
+
/**
|
|
14
|
+
* Deep structural equality, strict: a property that is `undefined` differs from a missing one (Jest's `toEqual`
|
|
15
|
+
* ignores it). `expected` may hold asymmetric matchers (`expect.any(Number)`, ...)
|
|
16
|
+
*/
|
|
12
17
|
toEqual(expected: unknown): void;
|
|
18
|
+
/** The object has the properties of `expected` (nested objects likewise; arrays need the same length); `expected` may hold asymmetric matchers */
|
|
19
|
+
toMatchObject(expected: object): void;
|
|
20
|
+
/** The array has an item that `toEqual` `item` */
|
|
21
|
+
toContainEqual(item: unknown): void;
|
|
22
|
+
toBeNull(): void;
|
|
23
|
+
toBeUndefined(): void;
|
|
24
|
+
/** Anything but `undefined` (`null` is defined) */
|
|
25
|
+
toBeDefined(): void;
|
|
26
|
+
/** A string, array or array-like with this `length` */
|
|
27
|
+
toHaveLength(expected: number): void;
|
|
28
|
+
/** The number is within half a unit of the `digits`-th decimal (default 2) of `expected` */
|
|
29
|
+
toBeCloseTo(expected: number, digits?: number): void;
|
|
30
|
+
toBeInstanceOf(expected: abstract new (...args: any[]) => unknown): void;
|
|
31
|
+
/**
|
|
32
|
+
* The function throws, optionally an error whose message contains a string or matches a RegExp, that is an
|
|
33
|
+
* instance of a class, or whose message equals an error's. Synchronous functions only
|
|
34
|
+
*/
|
|
35
|
+
toThrow(expected?: ThrowExpectation): void;
|
|
13
36
|
toBeTruthy(): void;
|
|
14
37
|
toBeFalsy(): void;
|
|
15
38
|
/** Array includes `item`, or string contains substring `item` */
|
|
@@ -20,4 +43,9 @@ export declare class ValueAssertions<T> {
|
|
|
20
43
|
toBeGreaterThanOrEqual(expected: number | bigint): void;
|
|
21
44
|
toBeLessThan(expected: number | bigint): void;
|
|
22
45
|
toBeLessThanOrEqual(expected: number | bigint): void;
|
|
46
|
+
/**
|
|
47
|
+
* Runs the function (sync or async) until it stops throwing, then resolves; with `.not`, until it throws.
|
|
48
|
+
* The default timeout is 0: one try. A failure rethrows the last error
|
|
49
|
+
*/
|
|
50
|
+
toPass(options?: RetryOptions): Promise<void>;
|
|
23
51
|
}
|
|
@@ -1,6 +1,8 @@
|
|
|
1
|
-
import { isDeepStrictEqual } from "node:util";
|
|
2
1
|
import { AssertionError } from "./assertion-error.js";
|
|
2
|
+
import { equalsWith, subsetMatches } from "./asymmetric.js";
|
|
3
|
+
import { toPass } from "./retry-assertions.js";
|
|
3
4
|
import { matchesText } from "./text-match.js";
|
|
5
|
+
import { callAndCatch, describeOutcome, isCloseTo, lengthOf, thrownMatches, } from "./value-matchers.js";
|
|
4
6
|
function isNumeric(value) {
|
|
5
7
|
return typeof value === "number" || typeof value === "bigint";
|
|
6
8
|
}
|
|
@@ -13,13 +15,13 @@ export class ValueAssertions {
|
|
|
13
15
|
get not() {
|
|
14
16
|
return new ValueAssertions(this.actual, !this.negated);
|
|
15
17
|
}
|
|
16
|
-
assert(pass, matcher, expected) {
|
|
18
|
+
assert(pass, matcher, expected, actual = this.actual) {
|
|
17
19
|
if (pass !== this.negated)
|
|
18
20
|
return;
|
|
19
21
|
throw new AssertionError({
|
|
20
22
|
matcher: this.negated ? `not.${matcher}` : matcher,
|
|
21
23
|
expected,
|
|
22
|
-
actual
|
|
24
|
+
actual,
|
|
23
25
|
});
|
|
24
26
|
}
|
|
25
27
|
compare(matcher, expected, check) {
|
|
@@ -33,9 +35,75 @@ export class ValueAssertions {
|
|
|
33
35
|
toBe(expected) {
|
|
34
36
|
this.assert(Object.is(this.actual, expected), "toBe", expected);
|
|
35
37
|
}
|
|
36
|
-
/**
|
|
38
|
+
/**
|
|
39
|
+
* Deep structural equality, strict: a property that is `undefined` differs from a missing one (Jest's `toEqual`
|
|
40
|
+
* ignores it). `expected` may hold asymmetric matchers (`expect.any(Number)`, ...)
|
|
41
|
+
*/
|
|
37
42
|
toEqual(expected) {
|
|
38
|
-
this.assert(
|
|
43
|
+
this.assert(equalsWith(this.actual, expected), "toEqual", expected);
|
|
44
|
+
}
|
|
45
|
+
/** The object has the properties of `expected` (nested objects likewise; arrays need the same length); `expected` may hold asymmetric matchers */
|
|
46
|
+
toMatchObject(expected) {
|
|
47
|
+
const actual = this.actual;
|
|
48
|
+
if (typeof actual !== "object" || actual === null) {
|
|
49
|
+
throw new TypeError(`toMatchObject expects an object, received ${actual === null ? "null" : typeof actual}`);
|
|
50
|
+
}
|
|
51
|
+
if (typeof expected !== "object" || expected === null) {
|
|
52
|
+
throw new TypeError(`toMatchObject expects an object to match, received ${typeof expected}`);
|
|
53
|
+
}
|
|
54
|
+
this.assert(subsetMatches(actual, expected), "toMatchObject", expected);
|
|
55
|
+
}
|
|
56
|
+
/** The array has an item that `toEqual` `item` */
|
|
57
|
+
toContainEqual(item) {
|
|
58
|
+
const actual = this.actual;
|
|
59
|
+
if (!Array.isArray(actual)) {
|
|
60
|
+
throw new TypeError(`toContainEqual expects an array, received ${typeof actual}`);
|
|
61
|
+
}
|
|
62
|
+
this.assert(actual.some((entry) => equalsWith(entry, item)), "toContainEqual", item);
|
|
63
|
+
}
|
|
64
|
+
toBeNull() {
|
|
65
|
+
this.assert(this.actual === null, "toBeNull", null);
|
|
66
|
+
}
|
|
67
|
+
toBeUndefined() {
|
|
68
|
+
this.assert(this.actual === undefined, "toBeUndefined", undefined);
|
|
69
|
+
}
|
|
70
|
+
/** Anything but `undefined` (`null` is defined) */
|
|
71
|
+
toBeDefined() {
|
|
72
|
+
this.assert(this.actual !== undefined, "toBeDefined", "defined");
|
|
73
|
+
}
|
|
74
|
+
/** A string, array or array-like with this `length` */
|
|
75
|
+
toHaveLength(expected) {
|
|
76
|
+
const length = lengthOf(this.actual);
|
|
77
|
+
if (length === undefined) {
|
|
78
|
+
throw new TypeError(`toHaveLength expects a value with a length, received ${this.actual === null ? "null" : typeof this.actual}`);
|
|
79
|
+
}
|
|
80
|
+
this.assert(length === expected, "toHaveLength", expected, length);
|
|
81
|
+
}
|
|
82
|
+
/** The number is within half a unit of the `digits`-th decimal (default 2) of `expected` */
|
|
83
|
+
toBeCloseTo(expected, digits = 2) {
|
|
84
|
+
const actual = this.actual;
|
|
85
|
+
if (typeof actual !== "number" || typeof expected !== "number") {
|
|
86
|
+
throw new TypeError(`toBeCloseTo expects numbers, received ${typeof actual} and ${typeof expected}`);
|
|
87
|
+
}
|
|
88
|
+
this.assert(isCloseTo(actual, expected, digits), "toBeCloseTo", expected);
|
|
89
|
+
}
|
|
90
|
+
toBeInstanceOf(expected) {
|
|
91
|
+
if (typeof expected !== "function") {
|
|
92
|
+
throw new TypeError(`toBeInstanceOf expects a class, received ${typeof expected}`);
|
|
93
|
+
}
|
|
94
|
+
this.assert(this.actual instanceof expected, "toBeInstanceOf", expected);
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* The function throws, optionally an error whose message contains a string or matches a RegExp, that is an
|
|
98
|
+
* instance of a class, or whose message equals an error's. Synchronous functions only
|
|
99
|
+
*/
|
|
100
|
+
toThrow(expected) {
|
|
101
|
+
const fn = this.actual;
|
|
102
|
+
if (typeof fn !== "function") {
|
|
103
|
+
throw new TypeError(`toThrow expects a function, received ${typeof fn}`);
|
|
104
|
+
}
|
|
105
|
+
const outcome = callAndCatch(fn);
|
|
106
|
+
this.assert(thrownMatches(outcome, expected), "toThrow", expected ?? "an error", describeOutcome(outcome));
|
|
39
107
|
}
|
|
40
108
|
toBeTruthy() {
|
|
41
109
|
this.assert(Boolean(this.actual), "toBeTruthy", "truthy");
|
|
@@ -79,4 +147,15 @@ export class ValueAssertions {
|
|
|
79
147
|
toBeLessThanOrEqual(expected) {
|
|
80
148
|
this.compare("toBeLessThanOrEqual", expected, (a, b) => a <= b);
|
|
81
149
|
}
|
|
150
|
+
/**
|
|
151
|
+
* Runs the function (sync or async) until it stops throwing, then resolves; with `.not`, until it throws.
|
|
152
|
+
* The default timeout is 0: one try. A failure rethrows the last error
|
|
153
|
+
*/
|
|
154
|
+
toPass(options) {
|
|
155
|
+
const block = this.actual;
|
|
156
|
+
if (typeof block !== "function") {
|
|
157
|
+
throw new TypeError(`toPass expects a function, received ${typeof block}`);
|
|
158
|
+
}
|
|
159
|
+
return toPass(block, options, this.negated);
|
|
160
|
+
}
|
|
82
161
|
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/** What `toThrow` accepts: a message part, a RegExp for the message, an error class, or an error whose message must be equal */
|
|
2
|
+
export type ThrowExpectation = string | RegExp | (new (...args: any[]) => unknown) | Error;
|
|
3
|
+
/** The result of calling a function under `toThrow` */
|
|
4
|
+
export type Outcome = {
|
|
5
|
+
threw: false;
|
|
6
|
+
} | {
|
|
7
|
+
threw: true;
|
|
8
|
+
error: unknown;
|
|
9
|
+
};
|
|
10
|
+
/** Calls `fn` and says whether it threw */
|
|
11
|
+
export declare function callAndCatch(fn: () => unknown): Outcome;
|
|
12
|
+
/** Whether what was thrown fits `expected`; with no expectation, throwing at all fits */
|
|
13
|
+
export declare function thrownMatches(outcome: Outcome, expected: ThrowExpectation | undefined): boolean;
|
|
14
|
+
/** What a `toThrow` failure shows as the received value */
|
|
15
|
+
export declare function describeOutcome(outcome: Outcome): string;
|
|
16
|
+
/** Jest's rule: the difference is under half a unit of the last digit asked for */
|
|
17
|
+
export declare function isCloseTo(actual: number, expected: number, digits: number): boolean;
|
|
18
|
+
/** The `length` of a string, array or array-like; undefined for anything else */
|
|
19
|
+
export declare function lengthOf(value: unknown): number | undefined;
|