@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.
Files changed (112) hide show
  1. package/dist/api.d.ts +3 -1
  2. package/dist/assert/asymmetric.d.ts +29 -0
  3. package/dist/assert/asymmetric.js +166 -0
  4. package/dist/assert/element-probes.d.ts +20 -0
  5. package/dist/assert/element-probes.js +45 -0
  6. package/dist/assert/expect.d.ts +20 -9
  7. package/dist/assert/expect.js +12 -4
  8. package/dist/assert/locator-assertion-base.d.ts +52 -0
  9. package/dist/assert/locator-assertion-base.js +78 -0
  10. package/dist/assert/locator-assertions.d.ts +26 -49
  11. package/dist/assert/locator-assertions.js +35 -78
  12. package/dist/assert/more-locator-assertions.d.ts +30 -0
  13. package/dist/assert/more-locator-assertions.js +86 -0
  14. package/dist/assert/retry-assertions.d.ts +28 -0
  15. package/dist/assert/retry-assertions.js +113 -0
  16. package/dist/assert/text-match.d.ts +2 -0
  17. package/dist/assert/text-match.js +8 -0
  18. package/dist/assert/value-assertions.d.ts +29 -1
  19. package/dist/assert/value-assertions.js +84 -5
  20. package/dist/assert/value-matchers.d.ts +19 -0
  21. package/dist/assert/value-matchers.js +53 -0
  22. package/dist/browser/browser.d.ts +9 -0
  23. package/dist/browser/browser.js +25 -5
  24. package/dist/browser/cdp.d.ts +11 -0
  25. package/dist/browser/cdp.js +28 -0
  26. package/dist/browser/chrome-launch.d.ts +4 -0
  27. package/dist/browser/chrome-launch.js +2 -2
  28. package/dist/browser/index.d.ts +5 -0
  29. package/dist/browser/index.js +4 -0
  30. package/dist/browser/launch-firefox.js +1 -1
  31. package/dist/browser/launch-flags.d.ts +4 -1
  32. package/dist/browser/launch-flags.js +18 -5
  33. package/dist/browser/navigation-error.d.ts +5 -1
  34. package/dist/browser/navigation-error.js +10 -2
  35. package/dist/browser/page-content.d.ts +5 -0
  36. package/dist/browser/page-content.js +43 -0
  37. package/dist/browser/page-evaluate.d.ts +9 -0
  38. package/dist/browser/page-evaluate.js +20 -0
  39. package/dist/browser/page-locators.d.ts +1 -1
  40. package/dist/browser/page-locators.js +1 -1
  41. package/dist/browser/page-navigation.d.ts +18 -0
  42. package/dist/browser/page-navigation.js +52 -2
  43. package/dist/browser/page-settings.d.ts +1 -1
  44. package/dist/browser/page.d.ts +52 -1
  45. package/dist/browser/page.js +66 -2
  46. package/dist/browser/session-capabilities.d.ts +10 -0
  47. package/dist/browser/session-capabilities.js +11 -0
  48. package/dist/browser/session.d.ts +2 -1
  49. package/dist/browser/session.js +3 -3
  50. package/dist/browser/storage-state.d.ts +6 -0
  51. package/dist/browser/storage-state.js +37 -0
  52. package/dist/browser/use-applier.d.ts +27 -0
  53. package/dist/browser/use-applier.js +94 -0
  54. package/dist/browser/viewport.d.ts +3 -2
  55. package/dist/browser/viewport.js +15 -2
  56. package/dist/config/config.js +2 -0
  57. package/dist/config/use-options.d.ts +16 -0
  58. package/dist/config/use-options.js +128 -0
  59. package/dist/locator/element-access.d.ts +2 -0
  60. package/dist/locator/element-reads.d.ts +17 -0
  61. package/dist/locator/element-reads.js +31 -0
  62. package/dist/locator/evaluate.d.ts +12 -0
  63. package/dist/locator/evaluate.js +36 -0
  64. package/dist/locator/locator.d.ts +23 -0
  65. package/dist/locator/locator.js +36 -1
  66. package/dist/logger/warn-once.d.ts +4 -0
  67. package/dist/logger/warn-once.js +13 -0
  68. package/dist/runner/attachments.d.ts +16 -0
  69. package/dist/runner/attachments.js +60 -0
  70. package/dist/runner/fixture-args.d.ts +15 -0
  71. package/dist/runner/fixture-args.js +95 -0
  72. package/dist/runner/fixture-defs.d.ts +22 -0
  73. package/dist/runner/fixture-defs.js +103 -0
  74. package/dist/runner/fixture-scope.d.ts +33 -0
  75. package/dist/runner/fixture-scope.js +156 -0
  76. package/dist/runner/fixtures.d.ts +34 -0
  77. package/dist/runner/fixtures.js +39 -0
  78. package/dist/runner/hooks.d.ts +6 -1
  79. package/dist/runner/hooks.js +15 -3
  80. package/dist/runner/registry.d.ts +3 -39
  81. package/dist/runner/registry.js +93 -19
  82. package/dist/runner/reporter.d.ts +7 -1
  83. package/dist/runner/reporter.js +3 -0
  84. package/dist/runner/run-test.d.ts +1 -3
  85. package/dist/runner/run-test.js +92 -34
  86. package/dist/runner/steps.d.ts +13 -0
  87. package/dist/runner/steps.js +67 -0
  88. package/dist/runner/suite.d.ts +5 -0
  89. package/dist/runner/suite.js +8 -0
  90. package/dist/runner/test-annotations.d.ts +16 -0
  91. package/dist/runner/test-annotations.js +47 -0
  92. package/dist/runner/test-function.d.ts +103 -0
  93. package/dist/runner/test-function.js +1 -0
  94. package/dist/runner/test-info.d.ts +104 -0
  95. package/dist/runner/test-info.js +169 -0
  96. package/dist/runner/test-runner.js +2 -1
  97. package/dist/runner/test-timeout.d.ts +22 -0
  98. package/dist/runner/test-timeout.js +45 -0
  99. package/dist/runner/use-options.d.ts +7 -0
  100. package/dist/runner/use-options.js +18 -0
  101. package/dist/runner/worker-session.js +2 -1
  102. package/dist/script/call-function.d.ts +7 -0
  103. package/dist/script/call-function.js +21 -3
  104. package/dist/script/element-handle.d.ts +1 -1
  105. package/dist/script/element-handle.js +1 -1
  106. package/dist/types/config.d.ts +3 -2
  107. package/dist/types/extend.d.ts +30 -0
  108. package/dist/types/test.d.ts +120 -0
  109. package/dist/types/use.d.ts +47 -0
  110. package/dist/wait/wait-until.d.ts +3 -1
  111. package/dist/wait/wait-until.js +7 -2
  112. package/package.json +1 -1
@@ -0,0 +1,169 @@
1
+ import { AsyncLocalStorage } from "node:async_hooks";
2
+ import path from "node:path";
3
+ import { saveAttachment } from "./attachments.js";
4
+ import { isSkipSignal, SkipSignal } from "./skip.js";
5
+ import { StepLog } from "./steps.js";
6
+ import { toTestError } from "./test-error.js";
7
+ import { checkTimeout, TestTimeout } from "./test-timeout.js";
8
+ /** The `TestInfo` of one attempt of a test. The runner creates it, the test only reads it. */
9
+ export class TestInfoImpl {
10
+ constructor(from) {
11
+ this.annotations = [];
12
+ this.expectedStatus = "passed";
13
+ /** The steps the test ran with `test.step` */
14
+ this.stepLog = new StepLog();
15
+ this.errors = [];
16
+ this.saved = [];
17
+ this.used = new Set();
18
+ this.started = Date.now();
19
+ const { test } = from;
20
+ this.title = test.titlePath[test.titlePath.length - 1] ?? test.name;
21
+ this.titlePath = [...test.titlePath];
22
+ this.file = test.file;
23
+ if (test.line !== undefined)
24
+ this.line = test.line;
25
+ if (test.column !== undefined)
26
+ this.column = test.column;
27
+ this.retry = from.retry;
28
+ this.workerIndex = from.workerIndex;
29
+ this.project = { name: from.browserName };
30
+ this.config = { ...from.settings };
31
+ this.resultDir = from.resultDir;
32
+ this.outputDir = from.outputDir;
33
+ this.limit =
34
+ from.timeout ?? new TestTimeout(from.settings.timeout, () => { });
35
+ }
36
+ get timeout() {
37
+ return this.limit.ms;
38
+ }
39
+ setTimeout(ms) {
40
+ this.limit.set(checkTimeout("setTimeout()", ms));
41
+ }
42
+ slow(condition = true, description) {
43
+ if (!condition || this.annotated("slow"))
44
+ return;
45
+ this.note("slow", description);
46
+ this.limit.set(this.limit.ms * 3);
47
+ }
48
+ fail(condition = true, description) {
49
+ if (!condition)
50
+ return;
51
+ this.expectedStatus = "failed";
52
+ if (!this.annotated("fail"))
53
+ this.note("fail", description);
54
+ }
55
+ skip(condition = true, description) {
56
+ if (condition)
57
+ throw new SkipSignal("skip", description);
58
+ }
59
+ fixme(condition = true, description) {
60
+ if (condition)
61
+ throw new SkipSignal("fixme", description);
62
+ }
63
+ annotated(type) {
64
+ return this.annotations.some((a) => a.type === type);
65
+ }
66
+ note(type, description) {
67
+ this.annotations.push({
68
+ type,
69
+ ...(description !== undefined && { description }),
70
+ });
71
+ }
72
+ get status() {
73
+ return this.result;
74
+ }
75
+ get duration() {
76
+ return (this.ended ?? Date.now()) - this.started;
77
+ }
78
+ get attachments() {
79
+ return this.saved;
80
+ }
81
+ async attach(name, options) {
82
+ this.saved.push(await saveAttachment(this.outputDir, this.used, name, options));
83
+ }
84
+ /** The body (with its `beforeEach` hooks) ended: `error` is what it threw, if anything. Runs before the `afterEach` hooks. */
85
+ bodyDone(error) {
86
+ this.ended = Date.now();
87
+ if (error === undefined)
88
+ this.result = "passed";
89
+ else if (isSkipSignal(error))
90
+ this.result = "skipped";
91
+ else {
92
+ this.result = "failed";
93
+ this.errors.push(toTestError(error));
94
+ }
95
+ }
96
+ /** The fields a report entry adds for this attempt: nothing when there is nothing to list */
97
+ reportExtras() {
98
+ return {
99
+ ...(this.saved.length > 0 && {
100
+ attachments: this.saved.map((a) => ({
101
+ name: a.name,
102
+ contentType: a.contentType,
103
+ path: path.relative(this.resultDir, a.path).split(path.sep).join("/"),
104
+ })),
105
+ }),
106
+ ...(this.annotations.length > 0 && {
107
+ annotations: this.annotations.map((a) => ({ ...a })),
108
+ }),
109
+ ...(this.stepLog.entries.length > 0 && {
110
+ steps: structuredClone(this.stepLog.entries),
111
+ }),
112
+ };
113
+ }
114
+ }
115
+ const scopes = new AsyncLocalStorage();
116
+ /** Runs `fn` (the body and hooks of one test) so that `test.info()` and `test.skip(fn)` find the test */
117
+ export function runInTestScope(scope, fn) {
118
+ return scopes.run(scope, fn);
119
+ }
120
+ /** `test.info()` */
121
+ export function currentTestInfo() {
122
+ const scope = scopes.getStore();
123
+ if (!scope)
124
+ throw new Error("info(): no test is running");
125
+ return scope.info;
126
+ }
127
+ /** The running test's scope, if a test (or one of its hooks) is running */
128
+ export function runningScope() {
129
+ return scopes.getStore();
130
+ }
131
+ /** The running test's info for `label` (`setTimeout()`); throws when no test is running */
132
+ export function infoFor(label) {
133
+ const scope = scopes.getStore();
134
+ if (!scope)
135
+ throw new Error(`${label}: no test is running`);
136
+ return scope.info;
137
+ }
138
+ /** The running test's steps; throws when no test is running */
139
+ export function currentStepLog() {
140
+ const info = infoFor("step()");
141
+ if (!(info instanceof TestInfoImpl))
142
+ throw new Error("step(): no test is running");
143
+ return info.stepLog;
144
+ }
145
+ /**
146
+ * Whether a condition of `test.skip`, `test.fail` or `test.slow` holds: a boolean as it is, a function called with
147
+ * the running test's fixtures (it must be synchronous). `label` starts the errors (`test.skip()`).
148
+ */
149
+ export function conditionHolds(label, condition) {
150
+ if (typeof condition !== "function")
151
+ return Boolean(condition);
152
+ const scope = scopes.getStore();
153
+ if (!scope)
154
+ throw new Error(`${label}: a condition function needs a running test`);
155
+ const result = condition(scope.fixtures);
156
+ if (result instanceof Promise) {
157
+ result.catch(() => { });
158
+ throw new Error(`${label}: the condition function must be synchronous`);
159
+ }
160
+ return Boolean(result);
161
+ }
162
+ /**
163
+ * `test.skip(condition, reason)` and `test.fixme(...)` from inside a test or its hooks: throws the skip signal
164
+ * when `condition` is true, or when it is a function and returns something truthy.
165
+ */
166
+ export function skipFromCondition(annotation, condition, reason) {
167
+ if (conditionHolds(`test.${annotation}()`, condition))
168
+ throw new SkipSignal(annotation, reason);
169
+ }
@@ -11,7 +11,8 @@ import { DEFAULT_SRC_DIR, DEFAULT_TEST_MATCH } from "../config/defaults.js";
11
11
  import { DEFAULT_PORT } from "./ports.js";
12
12
  import TestReporter from "./reporter.js";
13
13
  import { concurrency, planItems, poolMode, runPooled, serialKeyOf, } from "./pooled-run.js";
14
- import { allHookFixtures, runOneTest } from "./run-test.js";
14
+ import { allHookFixtures } from "./fixtures.js";
15
+ import { runOneTest } from "./run-test.js";
15
16
  import { staticSkip } from "./skip.js";
16
17
  import { browsersOfRun, loadSpecs, selectTests } from "./select.js";
17
18
  import { resolveRetries } from "./retries-option.js";
@@ -0,0 +1,22 @@
1
+ /**
2
+ * The time limit of one test. It starts once, and can be changed while the test runs: the new limit counts from
3
+ * the start of the test, so the time already spent counts against it (a limit already passed ends the test now).
4
+ */
5
+ export declare class TestTimeout {
6
+ private limit;
7
+ private readonly onExpire;
8
+ private timer;
9
+ private startedAt;
10
+ private expired;
11
+ constructor(limit: number, onExpire: () => void);
12
+ /** The limit in milliseconds; 0 means none */
13
+ get ms(): number;
14
+ /** Starts the clock; nothing can expire before this */
15
+ start(): void;
16
+ /** Changes the limit, re-arming the timer from the start of the test */
17
+ set(ms: number): void;
18
+ stop(): void;
19
+ private arm;
20
+ }
21
+ /** Checks a timeout in milliseconds; `label` starts the error (`setTimeout()`) */
22
+ export declare function checkTimeout(label: string, ms: unknown): number;
@@ -0,0 +1,45 @@
1
+ /**
2
+ * The time limit of one test. It starts once, and can be changed while the test runs: the new limit counts from
3
+ * the start of the test, so the time already spent counts against it (a limit already passed ends the test now).
4
+ */
5
+ export class TestTimeout {
6
+ constructor(limit, onExpire) {
7
+ this.limit = limit;
8
+ this.onExpire = onExpire;
9
+ this.expired = false;
10
+ }
11
+ /** The limit in milliseconds; 0 means none */
12
+ get ms() {
13
+ return this.limit;
14
+ }
15
+ /** Starts the clock; nothing can expire before this */
16
+ start() {
17
+ this.startedAt = Date.now();
18
+ this.arm();
19
+ }
20
+ /** Changes the limit, re-arming the timer from the start of the test */
21
+ set(ms) {
22
+ this.limit = ms;
23
+ if (this.startedAt !== undefined && !this.expired)
24
+ this.arm();
25
+ }
26
+ stop() {
27
+ clearTimeout(this.timer);
28
+ }
29
+ arm() {
30
+ clearTimeout(this.timer);
31
+ if (this.limit === 0 || this.startedAt === undefined)
32
+ return;
33
+ const left = Math.max(0, this.limit - (Date.now() - this.startedAt));
34
+ this.timer = setTimeout(() => {
35
+ this.expired = true;
36
+ this.onExpire();
37
+ }, left);
38
+ }
39
+ }
40
+ /** Checks a timeout in milliseconds; `label` starts the error (`setTimeout()`) */
41
+ export function checkTimeout(label, ms) {
42
+ if (typeof ms !== "number" || !Number.isFinite(ms) || ms < 0)
43
+ throw new Error(`${label}: the timeout must be a number of milliseconds, 0 or more`);
44
+ return ms;
45
+ }
@@ -0,0 +1,7 @@
1
+ import type { RegisteredTestCase } from "../types/test.js";
2
+ import type { UseOptions } from "../types/use.js";
3
+ /**
4
+ * The `use` options one test runs with: config `use`, then the `test.use()` calls around the test, outermost
5
+ * first, a later one winning key by key
6
+ */
7
+ export declare function effectiveUse(test: RegisteredTestCase): Promise<UseOptions>;
@@ -0,0 +1,18 @@
1
+ import { readConfig } from "../config/config.js";
2
+ import { mergeUse, useOptionsOf } from "../config/use-options.js";
3
+ import logger from "../logger/index.js";
4
+ import { useOf } from "./suite.js";
5
+ /**
6
+ * The `use` options one test runs with: config `use`, then the `test.use()` calls around the test, outermost
7
+ * first, a later one winning key by key
8
+ */
9
+ export async function effectiveUse(test) {
10
+ let config;
11
+ try {
12
+ config = await readConfig("use");
13
+ }
14
+ catch (err) {
15
+ logger.debug("Could not read use config, using defaults", { err });
16
+ }
17
+ return mergeUse([useOptionsOf(config), ...useOf(test.suite)]);
18
+ }
@@ -3,7 +3,8 @@ import logger from "../logger/index.js";
3
3
  import { withProject } from "./project.js";
4
4
  import TestReporter from "./reporter.js";
5
5
  import { AllHooks } from "./all-hooks.js";
6
- import { allHookFixtures, runOneTest } from "./run-test.js";
6
+ import { allHookFixtures } from "./fixtures.js";
7
+ import { runOneTest } from "./run-test.js";
7
8
  import { runWithRetries } from "./retry.js";
8
9
  import { loadSpecs } from "./select.js";
9
10
  /**
@@ -20,7 +20,14 @@ export declare class ScriptError extends Error {
20
20
  export declare class ScriptSerializationError extends Error {
21
21
  constructor(message: string);
22
22
  }
23
+ /** A function to run in the page, or its source (a string is used as written) */
24
+ export type PageFunction<A extends unknown[], R> = string | ((...args: A) => R | Promise<R>);
23
25
  /** The source to send for `fn`; a function object must not depend on transpiler helpers */
24
26
  export declare function toFunctionDeclaration(fn: string | ((...args: any[]) => unknown)): string;
25
27
  /** Calls `fn` in the page with serialized arguments and returns its deserialized result */
26
28
  export declare function callFunction<T = unknown>(connector: BiDiConnector, context: string, fn: string | ((...args: any[]) => unknown), args?: Arg[], options?: CallOptions): Promise<T>;
29
+ /**
30
+ * Evaluates `expression` in the page and returns its deserialized result; a promise is awaited.
31
+ * Throws `ScriptError` with the page's message when it throws.
32
+ */
33
+ export declare function evaluateExpression<T = unknown>(connector: BiDiConnector, context: string, expression: string): Promise<T>;
@@ -26,10 +26,12 @@ export function toFunctionDeclaration(fn) {
26
26
  if (typeof fn === "string")
27
27
  return fn;
28
28
  const source = fn.toString();
29
- // tsx/esbuild wrap named functions in a __name() helper that does not exist in the page
29
+ // tsx/esbuild (keepNames) wrap named functions in a __name() helper that does not exist in the page
30
30
  if (source.includes("__name(")) {
31
- throw new ScriptSerializationError("The function's source uses the transpiler's __name helper, which does not exist in the page. " +
32
- "Use an arrow function without inner named functions, or pass the function as a string.");
31
+ throw new ScriptSerializationError("The function's source uses __name(), a helper that tsx and esbuild add around named functions " +
32
+ "(also an arrow function assigned to a variable) and that does not exist in the page. " +
33
+ "Write the function without inner named functions (pass callbacks inline as anonymous arrow functions), " +
34
+ "or pass it as a string.");
33
35
  }
34
36
  return source;
35
37
  }
@@ -52,3 +54,19 @@ export async function callFunction(connector, context, fn, args = [], options =
52
54
  }
53
55
  return fromRemoteValue(result.result);
54
56
  }
57
+ /**
58
+ * Evaluates `expression` in the page and returns its deserialized result; a promise is awaited.
59
+ * Throws `ScriptError` with the page's message when it throws.
60
+ */
61
+ export async function evaluateExpression(connector, context, expression) {
62
+ const result = await connector.send("script.evaluate", {
63
+ expression,
64
+ awaitPromise: true,
65
+ target: { context },
66
+ resultOwnership: "none",
67
+ });
68
+ if (result.type === "exception") {
69
+ throw new ScriptError(result.exceptionDetails.text, expression, result.exceptionDetails.stackTrace);
70
+ }
71
+ return fromRemoteValue(result.result);
72
+ }
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * A reference to a DOM node in the browser. The `sharedId` stays valid across scripts in the
3
3
  * same document; after navigation or removal the browser answers `no such node`.
4
- * Internal: user code keeps using `Locator`.
4
+ * What `evaluate` returns for a DOM node; user code keeps using `Locator` to act on elements.
5
5
  */
6
6
  export declare class ElementHandle {
7
7
  readonly sharedId: string;
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * A reference to a DOM node in the browser. The `sharedId` stays valid across scripts in the
3
3
  * same document; after navigation or removal the browser answers `no such node`.
4
- * Internal: user code keeps using `Locator`.
4
+ * What `evaluate` returns for a DOM node; user code keeps using `Locator` to act on elements.
5
5
  */
6
6
  export class ElementHandle {
7
7
  constructor(sharedId) {
@@ -1,5 +1,6 @@
1
1
  import type { StepDefinition } from "../flows/steps/define.js";
2
2
  import type { SupportedBrowser } from "./browser.js";
3
+ import type { UseOptions } from "./use.js";
3
4
 
4
5
  interface SamuraiTest {
5
6
  /** Path relative to srcDir, e.g. 'checkout/coupons.spec.ts' */
@@ -129,8 +130,8 @@ export interface SamuraiTestConfig {
129
130
  * @default "files"
130
131
  */
131
132
  filesDir?: string;
132
- /** How pages are opened */
133
- use?: {
133
+ /** How pages are opened, and what the browser pretends: the options of `test.use()` apply to every test */
134
+ use?: UseOptions & {
134
135
  /**
135
136
  * Size of the page's viewport, so screenshots and layout are the same on every machine.
136
137
  * `null` keeps the browser's own size
@@ -0,0 +1,30 @@
1
+ import type { TestFixtures } from "./test.js";
2
+
3
+ /** What a test sees when it is built with `test.extend()`: the built-in fixtures, a custom one of the same name replacing it */
4
+ export type FixturesOf<X> = Omit<TestFixtures, keyof X> & X;
5
+
6
+ /** `B` on top of `A`: a key in both takes the type from `B` */
7
+ export type MergeFixtures<A, B> = Omit<A, keyof B> & B;
8
+
9
+ /** The options of a fixture written as `[fixture, options]` */
10
+ export interface FixtureOptions {
11
+ /** Set the fixture up for every test, whether or not the test asks for it */
12
+ auto?: boolean;
13
+ /** Fixtures live as long as one test; worker-scoped fixtures are not supported */
14
+ scope?: "test";
15
+ }
16
+
17
+ /** A value, or a function that sets the value up, calls `use(value)` and cleans up once `use` returns */
18
+ export type FixtureValue<R, Args> =
19
+ | Exclude<R, (...args: never[]) => unknown>
20
+ | ((args: Args, use: (value: R) => Promise<void>) => Promise<void>);
21
+
22
+ /** What `test.extend()` takes: each fixture alone, or as `[fixture, options]` */
23
+ export type FixtureDefinitions<T, Existing> = {
24
+ [K in keyof T]:
25
+ | FixtureValue<T[K], FixturesOf<MergeFixtures<Existing, T>>>
26
+ | [
27
+ FixtureValue<T[K], FixturesOf<MergeFixtures<Existing, T>>>,
28
+ FixtureOptions,
29
+ ];
30
+ };
@@ -1,7 +1,9 @@
1
1
  import type { Browser } from "../browser/browser.js";
2
2
  import type Page from "../browser/page.js";
3
+ import type { BrowserContext } from "../browser/browser-context.js";
3
4
  import type Locator from "../locator/locator.js";
4
5
  import type { SupportedBrowser } from "./browser.js";
6
+ import type { UseOptions } from "./use.js";
5
7
 
6
8
  /** What a test function receives */
7
9
  export interface TestFixtures {
@@ -19,6 +21,101 @@ export interface TestFixtures {
19
21
  inputs: Readonly<Record<string, string>>;
20
22
  /** Saves the text values the test declared as outputs */
21
23
  outputs: TestOutputs;
24
+ /** The browser context (cookies, storage) of `page`; same as `page.context()` */
25
+ context: BrowserContext;
26
+ /** The run's `baseURL` (the environment's, else the project's); `undefined` when none is set */
27
+ baseURL: string | undefined;
28
+ /** About the running test: its title, retry, attachments; same as `test.info()` */
29
+ testInfo: TestInfo;
30
+ /** Whether the page runs as a phone (`use.isMobile`); always false on Firefox, which cannot do it */
31
+ isMobile: boolean;
32
+ /** Whether the page has touch input (`use.hasTouch`); always false on Firefox, which cannot do it */
33
+ hasTouch: boolean;
34
+ }
35
+
36
+ /** A file or text a test attached to its result */
37
+ export interface TestAttachment {
38
+ name: string;
39
+ contentType: string;
40
+ /** Absolute path of the saved file */
41
+ path: string;
42
+ }
43
+
44
+ /** An attachment as the report lists it */
45
+ export interface TestAttachmentEntry {
46
+ name: string;
47
+ contentType: string;
48
+ /** Relative to the result folder */
49
+ path: string;
50
+ }
51
+
52
+ /** A note on a test (`{ type: "issue", description: "PROJ-1" }`) that the report lists */
53
+ export interface TestAnnotation {
54
+ type: string;
55
+ description?: string;
56
+ }
57
+
58
+ /** A step the test ran with `test.step`, as the report lists it */
59
+ export interface TestStepEntry {
60
+ title: string;
61
+ /** `"skipped"`: the step ended the test with `test.skip(...)` */
62
+ status: "passed" | "failed" | "skipped";
63
+ /** Milliseconds */
64
+ duration: number;
65
+ /** The message of what the step threw, or that the test ended before it finished */
66
+ error?: string;
67
+ /** The steps started inside this one; absent when there are none */
68
+ steps?: TestStepEntry[];
69
+ }
70
+
71
+ /** What `testInfo` and `test.info()` tell about the running test (hooks included) */
72
+ export interface TestInfo {
73
+ /** The test's own title */
74
+ readonly title: string;
75
+ /** The `describe` titles, then the title */
76
+ readonly titlePath: string[];
77
+ /** Absolute path of the spec file */
78
+ readonly file: string;
79
+ /** Where the test is written; absent when unknown */
80
+ readonly line?: number;
81
+ readonly column?: number;
82
+ /** The attempt, counting from 0: 1 is the first run again */
83
+ readonly retry: number;
84
+ readonly workerIndex: number;
85
+ /** The browser this run is in. There is no `projects` matrix yet, so `name` is the browser name */
86
+ readonly project: { name: string };
87
+ /** The run settings that apply to the test */
88
+ readonly config: { baseURL: string | undefined; timeout: number };
89
+ /** Push notes here; they are listed in the report entry */
90
+ annotations: TestAnnotation[];
91
+ /** Undefined while the test body runs; set before the `afterEach` hooks */
92
+ readonly status: "passed" | "failed" | "skipped" | undefined;
93
+ /** What the test is expected to end as: `"failed"` after `test.fail()`, else `"passed"` */
94
+ readonly expectedStatus: "passed" | "failed";
95
+ /** The time limit of the test in milliseconds (0: none); see `setTimeout` */
96
+ readonly timeout: number;
97
+ /** Sets the limit of this test, counted from its start; `0` removes it */
98
+ setTimeout(ms: number): void;
99
+ /** Triples the timeout, once, and adds a `slow` annotation; nothing happens when `condition` is false */
100
+ slow(condition?: boolean, description?: string): void;
101
+ /** Expects the test to fail (`expectedStatus` is `"failed"`) and adds a `fail` annotation */
102
+ fail(condition?: boolean, description?: string): void;
103
+ /** Ends the test as skipped when `condition` is not false */
104
+ skip(condition?: boolean, description?: string): void;
105
+ /** Ends the test as skipped (known to be broken) when `condition` is not false */
106
+ fixme(condition?: boolean, description?: string): void;
107
+ /** Milliseconds since the attempt started; fixed once the body is done */
108
+ readonly duration: number;
109
+ /** The errors of the test body or of its `beforeEach` hooks; filled before the `afterEach` hooks */
110
+ readonly errors: TestError[];
111
+ /** Folder for the files this attempt attaches (created when the first one is saved) */
112
+ readonly outputDir: string;
113
+ readonly attachments: readonly TestAttachment[];
114
+ /** Saves a file in `outputDir` and lists it in the report entry; give `body` or `path`, not both */
115
+ attach(
116
+ name: string,
117
+ options: { body?: string | Buffer; path?: string; contentType?: string },
118
+ ): Promise<void>;
22
119
  }
23
120
 
24
121
  /** The `outputs` fixture */
@@ -73,6 +170,10 @@ export interface Suite {
73
170
  afterEach?: EachHook[];
74
171
  beforeAll?: AllHook[];
75
172
  afterAll?: AllHook[];
173
+ /** `test.fail(true)` and `test.slow(true)` written in this scope: they apply to every test below */
174
+ marks?: { type: "fail" | "slow"; description?: string }[];
175
+ /** The `test.use()` calls written in this scope, in order; they apply to every test below */
176
+ use?: UseOptions[];
76
177
  }
77
178
 
78
179
  export interface RegisteredTestCase extends TestCaseBase {
@@ -80,6 +181,9 @@ export interface RegisteredTestCase extends TestCaseBase {
80
181
  file: string;
81
182
  /** describe titles, then the test title */
82
183
  titlePath: string[];
184
+ /** Where the test is written, when known */
185
+ line?: number;
186
+ column?: number;
83
187
  /** The `describe` block (or the file) the test is written in */
84
188
  suite: Suite;
85
189
  /** What applies to the test: its own modifier, or the one of a suite around it */
@@ -90,6 +194,10 @@ export interface RegisteredTestCase extends TestCaseBase {
90
194
  inputs?: Record<string, string>;
91
195
  /** The names of the outputs the test declared */
92
196
  outputs?: string[];
197
+ /** Registered with `test.fail("title", fn)`: expected to fail; `description` is the `{ reason }` given */
198
+ expectedFail?: { description?: string };
199
+ /** The custom fixtures of the `test` it was written with (`test.extend()`); absent for the plain `test` */
200
+ extension?: ReadonlyMap<string, readonly unknown[]>;
93
201
  }
94
202
 
95
203
  export interface TestCaseBase {
@@ -118,6 +226,12 @@ interface TestResultSuccess extends TestResultIdentity, TestRetries {
118
226
  inputs?: Record<string, string>;
119
227
  /** The outputs the test saved; absent when it declares none */
120
228
  outputs?: Record<string, string>;
229
+ /** Files the test attached, relative to the result folder; absent when it attached none */
230
+ attachments?: TestAttachmentEntry[];
231
+ /** The notes the test pushed to `testInfo.annotations`; absent when there are none */
232
+ annotations?: TestAnnotation[];
233
+ /** The steps the test ran with `test.step`; absent when it ran none */
234
+ steps?: TestStepEntry[];
121
235
  }
122
236
 
123
237
  /** The error sits flat on the entry, as in the report: `message`, `type`, `stack`, and for assertions `expected` and `actual` */
@@ -132,6 +246,12 @@ interface TestResultFailed extends TestResultIdentity, TestError, TestRetries {
132
246
  inputs?: Record<string, string>;
133
247
  /** The outputs the test saved; absent when it declares none */
134
248
  outputs?: Record<string, string>;
249
+ /** Files the test attached, relative to the result folder; absent when it attached none */
250
+ attachments?: TestAttachmentEntry[];
251
+ /** The notes the test pushed to `testInfo.annotations`; absent when there are none */
252
+ annotations?: TestAnnotation[];
253
+ /** The steps the test ran with `test.step`; absent when it ran none */
254
+ steps?: TestStepEntry[];
135
255
  }
136
256
 
137
257
  /** What a result says about the retries of its test; absent when it passed or failed on the first attempt */
@@ -0,0 +1,47 @@
1
+ /** How the page looks to the site, and what the browser starts with. Set in config `use` or with `test.use()`. */
2
+ export interface UseOptions {
3
+ /** A BCP 47 tag, e.g. "nl-NL": `navigator.language`, `Intl` formatting and the `Accept-Language` header */
4
+ locale?: string;
5
+ /** An IANA timezone, e.g. "Europe/Amsterdam" */
6
+ timezoneId?: string;
7
+ /** The `User-Agent` header and `navigator.userAgent` */
8
+ userAgent?: string;
9
+ /** `false` runs pages without JavaScript */
10
+ javaScriptEnabled?: boolean;
11
+ /** Accept certificates that are self-signed, expired or for another host. Fixed when the browser starts. */
12
+ acceptInsecureCerts?: boolean;
13
+ /** Touch input: `navigator.maxTouchPoints` is 1. Chrome only. */
14
+ hasTouch?: boolean;
15
+ /** A phone-like browser (the layout viewport follows the page's `<meta name="viewport">`). Chrome only. */
16
+ isMobile?: boolean;
17
+ /** Cookies and local storage to start with: a JSON file (relative to the project folder) or the object itself */
18
+ storageState?: string | StorageState;
19
+ /** Headers added to every request of the page */
20
+ extraHTTPHeaders?: Record<string, string>;
21
+ /** What `prefers-color-scheme` reports */
22
+ colorScheme?: ColorScheme;
23
+ }
24
+
25
+ export type ColorScheme = "light" | "dark" | "no-preference";
26
+
27
+ /** Cookies and local storage, in the format Playwright's `storageState` files use */
28
+ export interface StorageState {
29
+ cookies?: StorageStateCookie[];
30
+ origins?: {
31
+ origin: string;
32
+ localStorage: { name: string; value: string }[];
33
+ }[];
34
+ }
35
+
36
+ export interface StorageStateCookie {
37
+ name: string;
38
+ value: string;
39
+ domain: string;
40
+ /** @default "/" */
41
+ path?: string;
42
+ /** Seconds since the epoch; -1 or left out is a session cookie */
43
+ expires?: number;
44
+ httpOnly?: boolean;
45
+ secure?: boolean;
46
+ sameSite?: "Strict" | "Lax" | "None";
47
+ }
@@ -1,6 +1,8 @@
1
1
  export interface WaitOptions {
2
2
  /** Total time (ms) to keep probing; 0 probes exactly once */
3
3
  timeout: number;
4
+ /** Pauses (ms) after the first, second, ... probe; the last one repeats. Default: 100 every time */
5
+ intervals?: readonly number[];
4
6
  }
5
7
  export declare class WaitTimeoutError<T> extends Error {
6
8
  readonly timeout: number;
@@ -13,4 +15,4 @@ export declare class WaitTimeoutError<T> extends Error {
13
15
  * Always probes at least once; with `timeout > 0` every probe races the time left.
14
16
  * Probe errors are rethrown immediately. Throws WaitTimeoutError at the deadline.
15
17
  */
16
- export declare function waitUntil<T>(probe: () => Promise<T>, isDone: (current: T, previous: T | undefined) => boolean, { timeout }: WaitOptions): Promise<T>;
18
+ export declare function waitUntil<T>(probe: () => Promise<T>, isDone: (current: T, previous: T | undefined) => boolean, { timeout, intervals }: WaitOptions): Promise<T>;
@@ -29,9 +29,10 @@ function probeBefore(probe, remaining) {
29
29
  * Always probes at least once; with `timeout > 0` every probe races the time left.
30
30
  * Probe errors are rethrown immediately. Throws WaitTimeoutError at the deadline.
31
31
  */
32
- export async function waitUntil(probe, isDone, { timeout }) {
32
+ export async function waitUntil(probe, isDone, { timeout, intervals }) {
33
33
  const deadline = Date.now() + timeout;
34
34
  let previous;
35
+ let probes = 0;
35
36
  for (let first = true;; first = false) {
36
37
  const remaining = deadline - Date.now();
37
38
  // Only start a probe while time remains; a later one could only lose the race
@@ -49,6 +50,10 @@ export async function waitUntil(probe, isDone, { timeout }) {
49
50
  const left = deadline - Date.now();
50
51
  if (left <= 0)
51
52
  throw new WaitTimeoutError(timeout, previous);
52
- await sleep(Math.min(POLL_INTERVAL, left));
53
+ const pause = intervals?.length
54
+ ? intervals[Math.min(probes, intervals.length - 1)]
55
+ : POLL_INTERVAL;
56
+ probes++;
57
+ await sleep(Math.min(pause, left));
53
58
  }
54
59
  }