@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
@@ -3,6 +3,23 @@ import { type ActionabilityContext } from "./actionability.js";
3
3
  import { type ElementAccess } from "./element-access.js";
4
4
  /** Trimmed text content, or null when the element is missing */
5
5
  export declare function readTextContent(access: ElementAccess): Promise<string | null>;
6
+ /** Rendered text (what a person would copy: line breaks, no hidden text), untrimmed, or null when the element is missing */
7
+ export declare function readInnerText(access: ElementAccess): Promise<string | null>;
8
+ /** The markup inside the element, or null when the element is missing */
9
+ export declare function readInnerHTML(access: ElementAccess): Promise<string | null>;
10
+ /** `textContent` of every match, as the page has it (untrimmed); an empty array when none match */
11
+ export declare function readAllTextContents(access: ElementAccess): Promise<string[]>;
12
+ /** `innerText` of every match; an empty array when none match */
13
+ export declare function readAllInnerTexts(access: ElementAccess): Promise<string[]>;
14
+ /** The element's box in CSS pixels from the viewport's top left */
15
+ export interface BoundingBox {
16
+ x: number;
17
+ y: number;
18
+ width: number;
19
+ height: number;
20
+ }
21
+ /** The element's box, or null when it is missing or not rendered (`display: none`, detached): it has no boxes then */
22
+ export declare function readBoundingBox(access: ElementAccess): Promise<BoundingBox | null>;
6
23
  /** The accessible name (see `ACCESSIBLE_NAME_JS`), "" when it has none, or null when the element is missing */
7
24
  export declare function readAccessibleName(access: ElementAccess): Promise<string | null>;
8
25
  /** The accessible description, "" when it has none, or null when the element is missing */
@@ -2,6 +2,7 @@ import { probeState } from "./actionability.js";
2
2
  import { callOnElement, callOnExistingElement, } from "./element-access.js";
3
3
  import { describeType } from "./element-state.js";
4
4
  import { ACCESSIBLE_NAME_JS } from "./accessible-name.js";
5
+ import { evaluateOnAll } from "./evaluate.js";
5
6
  /** Body run in the page as `(el) => { ... }`: a checked checkbox or radio input, or `aria-checked="true"` */
6
7
  const IS_CHECKED_BODY = `
7
8
  const type = (el.getAttribute("type") || "").toLowerCase();
@@ -31,6 +32,36 @@ function stringOrNull(value) {
31
32
  export async function readTextContent(access) {
32
33
  return stringOrNull(await callOnElement(access, null, `return el.textContent.trim();`, [], [], "textContent"));
33
34
  }
35
+ /** Rendered text (what a person would copy: line breaks, no hidden text), untrimmed, or null when the element is missing */
36
+ export async function readInnerText(access) {
37
+ return stringOrNull(await callOnElement(access, null, `return el.innerText;`, [], [], "innerText"));
38
+ }
39
+ /** The markup inside the element, or null when the element is missing */
40
+ export async function readInnerHTML(access) {
41
+ return stringOrNull(await callOnElement(access, null, `return el.innerHTML;`, [], [], "innerHTML"));
42
+ }
43
+ /** `textContent` of every match, as the page has it (untrimmed); an empty array when none match */
44
+ export function readAllTextContents(access) {
45
+ return evaluateOnAll(access, `(els) => els.map((el) => el.textContent ?? "")`, undefined);
46
+ }
47
+ /** `innerText` of every match; an empty array when none match */
48
+ export function readAllInnerTexts(access) {
49
+ return evaluateOnAll(access, `(els) => els.map((el) => el.innerText ?? "")`, undefined);
50
+ }
51
+ /** The element's box, or null when it is missing or not rendered (`display: none`, detached): it has no boxes then */
52
+ export async function readBoundingBox(access) {
53
+ const box = await callOnElement(access, null, `
54
+ if (el.getClientRects().length === 0) return null;
55
+ const { x, y, width, height } = el.getBoundingClientRect();
56
+ return { x, y, width, height };
57
+ `, [], [], "boundingBox");
58
+ if (box === null)
59
+ return null;
60
+ if (typeof box !== "object") {
61
+ throw new Error(`Expected to receive an object but instead received "${describeType(box)}"`);
62
+ }
63
+ return box;
64
+ }
34
65
  /** The accessible name (see `ACCESSIBLE_NAME_JS`), "" when it has none, or null when the element is missing */
35
66
  export async function readAccessibleName(access) {
36
67
  return stringOrNull(await callOnElement(access, null, `${ACCESSIBLE_NAME_JS} return accessibleName(el);`, [], [], "accessibleName"));
@@ -0,0 +1,12 @@
1
+ import { type PageFunction } from "../script/call-function.js";
2
+ import type { ElementAccess } from "./element-access.js";
3
+ /**
4
+ * Waits until the element is attached (hidden is fine), then calls `fn(element, arg)` in the page.
5
+ * A missing element ends in the usual timeout error; a strict locator throws when several match.
6
+ * What the page function throws reaches the caller unchanged (a `ScriptError`).
7
+ */
8
+ export declare function evaluateOnElement<R, A>(access: ElementAccess, fn: PageFunction<[any, A], R>, arg: A | undefined, options?: {
9
+ timeout?: number;
10
+ }): Promise<R>;
11
+ /** Calls `fn(elements, arg)` in the page with every element the locator matches now (an empty array when none); does not wait */
12
+ export declare function evaluateOnAll<R, A>(access: ElementAccess, fn: PageFunction<[any[], A], R>, arg: A | undefined): Promise<R>;
@@ -0,0 +1,36 @@
1
+ import { BiDiError } from "../transport/bidi-error.js";
2
+ import { callFunction } from "../script/call-function.js";
3
+ import { ATTACHED_ONLY } from "./page-scripts.js";
4
+ /**
5
+ * Waits until the element is attached (hidden is fine), then calls `fn(element, arg)` in the page.
6
+ * A missing element ends in the usual timeout error; a strict locator throws when several match.
7
+ * What the page function throws reaches the caller unchanged (a `ScriptError`).
8
+ */
9
+ export async function evaluateOnElement(access, fn, arg, options) {
10
+ await access.waitForActionable("evaluate", ATTACHED_ONLY, { scroll: false, hitTest: false }, options?.timeout === undefined ? undefined : { timeout: options.timeout });
11
+ const el = await access.requireElement();
12
+ try {
13
+ return await callFunction(access.connector, access.contextId, fn, arg === undefined ? [el] : [el, arg]);
14
+ }
15
+ catch (err) {
16
+ // The element went away between the search and the call
17
+ if (err instanceof BiDiError && err.code === "no such node")
18
+ throw access.noElementError();
19
+ throw err;
20
+ }
21
+ }
22
+ /** Calls `fn(elements, arg)` in the page with every element the locator matches now (an empty array when none); does not wait */
23
+ export async function evaluateOnAll(access, fn, arg) {
24
+ // One more search when an element was removed between the search and the call
25
+ for (let attempt = 1;; attempt++) {
26
+ const elements = await access.resolveAll();
27
+ try {
28
+ return await callFunction(access.connector, access.contextId, fn, arg === undefined ? [elements] : [elements, arg]);
29
+ }
30
+ catch (err) {
31
+ if (attempt === 2 ||
32
+ !(err instanceof BiDiError && err.code === "no such node"))
33
+ throw err;
34
+ }
35
+ }
36
+ }
@@ -1,10 +1,12 @@
1
1
  import type { BiDiConnector } from "../transport/bidi-connection.js";
2
2
  import type { BrowsingContext, ElementRectangle } from "../types/bidi-modules/browsing-context.js";
3
3
  import { ElementHandle } from "../script/element-handle.js";
4
+ import type { PageFunction } from "../script/call-function.js";
4
5
  import type { ElementScreenshotOptions } from "../browser/screenshot.js";
5
6
  import type { FileInput } from "../browser/generated-files.js";
6
7
  import { type ActionOptions } from "./actionability.js";
7
8
  import { type CheckOptions } from "./check-actions.js";
9
+ import { type BoundingBox } from "./element-reads.js";
8
10
  import type { WaitForState } from "./element-state.js";
9
11
  import type { HelperInstaller } from "./helper-realm.js";
10
12
  import { type StrictSource } from "./strict-mode.js";
@@ -197,6 +199,27 @@ export default class Locator {
197
199
  isHidden(): Promise<boolean>;
198
200
  /** Trimmed text content, or null when the element is missing */
199
201
  textContent(): Promise<string | null>;
202
+ /** Rendered text of the element (as a person would copy it, hidden text left out), untrimmed, or null when the element is missing */
203
+ innerText(): Promise<string | null>;
204
+ /** The markup inside the element, or null when the element is missing */
205
+ innerHTML(): Promise<string | null>;
206
+ /** `textContent` of every match, as the page has it (untrimmed, unlike `textContent()`); `[]` when none match. Does not wait */
207
+ allTextContents(): Promise<string[]>;
208
+ /** `innerText` of every match; `[]` when none match. Does not wait */
209
+ allInnerTexts(): Promise<string[]>;
210
+ /** The element's box in CSS pixels from the viewport's top left, or null when it is missing or not rendered (`display: none`). Does not wait or scroll */
211
+ boundingBox(): Promise<BoundingBox | null>;
212
+ /**
213
+ * Waits until the element is attached (hidden is fine), then calls `fn(element, arg)` in the page and returns the result
214
+ * (see `page.evaluate` for what comes back). `fn` is a function or the source of one; a strict locator throws when several match.
215
+ * @example
216
+ * const id = await page.getByTestId("row").evaluate((el) => el.dataset.id);
217
+ */
218
+ evaluate<R = unknown, A = unknown>(fn: PageFunction<[any, A], R>, arg?: A, options?: {
219
+ timeout?: number;
220
+ }): Promise<R>;
221
+ /** Calls `fn(elements, arg)` in the page with an array of every match (empty when none; does not wait) and returns the result */
222
+ evaluateAll<R = unknown, A = unknown>(fn: PageFunction<[any[], A], R>, arg?: A): Promise<R>;
200
223
  /** Value of an input/textarea/select, or null when missing */
201
224
  inputValue(): Promise<string | null>;
202
225
  /** Attribute value, or null when the element or attribute is missing */
@@ -4,7 +4,8 @@ import { setChecked } from "./check-actions.js";
4
4
  import { ChainResolver } from "./chain-resolver.js";
5
5
  import { callInPage, noElementError, requireElement, } from "./element-access.js";
6
6
  import { clickElement, fillElement, focusElement, pressOnElement, screenshotElement, } from "./element-actions.js";
7
- import { readAccessibleDescription, readAccessibleName, readAttribute, readBoundingClientRect, readClassAttribute, readCssValue, readEnabledState, readInputValue, readIsChecked, readIsEditable, readIsEnabled, readIsVisible, readTextContent, } from "./element-reads.js";
7
+ import { readAccessibleDescription, readAccessibleName, readAllInnerTexts, readAllTextContents, readAttribute, readBoundingBox, readBoundingClientRect, readClassAttribute, readCssValue, readEnabledState, readInnerHTML, readInnerText, readInputValue, readIsChecked, readIsEditable, readIsEnabled, readIsVisible, readTextContent, } from "./element-reads.js";
8
+ import { evaluateOnAll, evaluateOnElement } from "./evaluate.js";
8
9
  import { dropFiles, setInputFiles } from "./file-actions.js";
9
10
  import { DEFAULT_PAUSE_MS, pause } from "./input-actions.js";
10
11
  import { NOT_STRICT, strictViolation, } from "./strict-mode.js";
@@ -44,6 +45,7 @@ export default class Locator {
44
45
  return locator.selector;
45
46
  },
46
47
  resolveOne: (method) => this.resolveOne(method),
48
+ resolveAll: () => this.resolve(),
47
49
  call: (fn, args, sandbox) => callInPage(this.access, fn, args, sandbox),
48
50
  waitForActionable: (action, checks, probe, options) => this.waitForActionable(action, checks, probe, options),
49
51
  requireElement: () => requireElement(this.access),
@@ -338,6 +340,39 @@ export default class Locator {
338
340
  async textContent() {
339
341
  return readTextContent(this.access);
340
342
  }
343
+ /** Rendered text of the element (as a person would copy it, hidden text left out), untrimmed, or null when the element is missing */
344
+ async innerText() {
345
+ return readInnerText(this.access);
346
+ }
347
+ /** The markup inside the element, or null when the element is missing */
348
+ async innerHTML() {
349
+ return readInnerHTML(this.access);
350
+ }
351
+ /** `textContent` of every match, as the page has it (untrimmed, unlike `textContent()`); `[]` when none match. Does not wait */
352
+ async allTextContents() {
353
+ return readAllTextContents(this.access);
354
+ }
355
+ /** `innerText` of every match; `[]` when none match. Does not wait */
356
+ async allInnerTexts() {
357
+ return readAllInnerTexts(this.access);
358
+ }
359
+ /** The element's box in CSS pixels from the viewport's top left, or null when it is missing or not rendered (`display: none`). Does not wait or scroll */
360
+ async boundingBox() {
361
+ return readBoundingBox(this.access);
362
+ }
363
+ /**
364
+ * Waits until the element is attached (hidden is fine), then calls `fn(element, arg)` in the page and returns the result
365
+ * (see `page.evaluate` for what comes back). `fn` is a function or the source of one; a strict locator throws when several match.
366
+ * @example
367
+ * const id = await page.getByTestId("row").evaluate((el) => el.dataset.id);
368
+ */
369
+ async evaluate(fn, arg, options) {
370
+ return evaluateOnElement(this.access, fn, arg, options);
371
+ }
372
+ /** Calls `fn(elements, arg)` in the page with an array of every match (empty when none; does not wait) and returns the result */
373
+ async evaluateAll(fn, arg) {
374
+ return evaluateOnAll(this.access, fn, arg);
375
+ }
341
376
  /** Value of an input/textarea/select, or null when missing */
342
377
  async inputValue() {
343
378
  return readInputValue(this.access);
@@ -0,0 +1,4 @@
1
+ /** Logs a warning the first time `key` is seen in this process; later calls with the same key do nothing */
2
+ export declare function warnOnce(key: string, message: string): void;
3
+ /** Forgets what was warned about (tests) */
4
+ export declare function resetWarnOnce(): void;
@@ -0,0 +1,13 @@
1
+ import logger from "./index.js";
2
+ const warned = new Set();
3
+ /** Logs a warning the first time `key` is seen in this process; later calls with the same key do nothing */
4
+ export function warnOnce(key, message) {
5
+ if (warned.has(key))
6
+ return;
7
+ warned.add(key);
8
+ logger.warn(message);
9
+ }
10
+ /** Forgets what was warned about (tests) */
11
+ export function resetWarnOnce() {
12
+ warned.clear();
13
+ }
@@ -0,0 +1,16 @@
1
+ /** What `testInfo.attach` takes besides the name */
2
+ export interface AttachInput {
3
+ body?: string | Buffer;
4
+ path?: string;
5
+ contentType?: string;
6
+ }
7
+ /**
8
+ * Writes one attachment into `dir` and returns what to list: a body is saved under the cleaned name, a path is
9
+ * copied under it keeping its extension. `used` holds the file names taken so far; a repeat gets -2, -3, ...
10
+ * String bodies are masked like the rest of a report.
11
+ */
12
+ export declare function saveAttachment(dir: string, used: Set<string>, name: string, input: AttachInput): Promise<{
13
+ name: string;
14
+ contentType: string;
15
+ path: string;
16
+ }>;
@@ -0,0 +1,60 @@
1
+ import { copyFile, mkdir, writeFile } from "node:fs/promises";
2
+ import path from "node:path";
3
+ import { cleanName } from "../browser/evidence.js";
4
+ import { maskText } from "../config/mask.js";
5
+ /** The file types an attachment is known by; the rest is `application/octet-stream` */
6
+ const TYPES = {
7
+ ".txt": "text/plain",
8
+ ".log": "text/plain",
9
+ ".html": "text/html",
10
+ ".json": "application/json",
11
+ ".csv": "text/csv",
12
+ ".png": "image/png",
13
+ ".jpg": "image/jpeg",
14
+ ".jpeg": "image/jpeg",
15
+ ".webm": "video/webm",
16
+ ".pdf": "application/pdf",
17
+ ".zip": "application/zip",
18
+ };
19
+ const OCTET = "application/octet-stream";
20
+ /** The extension (with its dot) that stands for `contentType`, else `fallback` */
21
+ function extensionOf(contentType, fallback) {
22
+ const found = Object.entries(TYPES).find(([, type]) => type === contentType);
23
+ return found ? found[0] : fallback;
24
+ }
25
+ /**
26
+ * Writes one attachment into `dir` and returns what to list: a body is saved under the cleaned name, a path is
27
+ * copied under it keeping its extension. `used` holds the file names taken so far; a repeat gets -2, -3, ...
28
+ * String bodies are masked like the rest of a report.
29
+ */
30
+ export async function saveAttachment(dir, used, name, input) {
31
+ if (name === "")
32
+ throw new Error("attach(): name must not be empty");
33
+ const { body, path: from } = input;
34
+ if (body === undefined && from === undefined)
35
+ throw new Error("attach(): give either body or path");
36
+ if (body !== undefined && from !== undefined)
37
+ throw new Error("attach(): give either body or path, not both");
38
+ const fileExt = from !== undefined ? path.extname(from).toLowerCase() : "";
39
+ const contentType = input.contentType ??
40
+ (from !== undefined
41
+ ? (TYPES[fileExt] ?? OCTET)
42
+ : typeof body === "string"
43
+ ? "text/plain"
44
+ : OCTET);
45
+ const ext = fileExt !== ""
46
+ ? fileExt
47
+ : extensionOf(contentType, typeof body === "string" ? ".txt" : ".bin");
48
+ const base = cleanName(name);
49
+ let unique = base;
50
+ for (let n = 2; used.has(unique); n++)
51
+ unique = `${base}-${n}`;
52
+ used.add(unique);
53
+ const file = path.join(dir, `${unique}${ext}`);
54
+ await mkdir(dir, { recursive: true });
55
+ if (from !== undefined)
56
+ await copyFile(from, file);
57
+ else
58
+ await writeFile(file, typeof body === "string" ? maskText(body) : body);
59
+ return { name, contentType, path: file };
60
+ }
@@ -0,0 +1,15 @@
1
+ /** What a function asks for in the destructuring of its first parameter */
2
+ export type Requested = {
3
+ kind: "names";
4
+ names: string[];
5
+ rest: boolean;
6
+ }
7
+ /** No destructuring pattern: the function takes the fixtures object whole, or none */
8
+ | {
9
+ kind: "opaque";
10
+ };
11
+ /**
12
+ * The fixture names a function destructures from its first parameter (`async ({ page, user }) => ...`), read
13
+ * from its source. A `...rest` entry asks for everything.
14
+ */
15
+ export declare function requestedBy(fn: (...args: never[]) => unknown): Requested;
@@ -0,0 +1,95 @@
1
+ const OPAQUE = { kind: "opaque" };
2
+ /** Where the `}` or `)` that closes the bracket at `from` is, skipping strings; -1 when there is none */
3
+ function closing(text, from) {
4
+ let depth = 0;
5
+ for (let i = from; i < text.length; i++) {
6
+ const c = text[i];
7
+ if (c === '"' || c === "'" || c === "`") {
8
+ i = endOfString(text, i);
9
+ if (i < 0)
10
+ return -1;
11
+ }
12
+ else if (c === "{" || c === "(" || c === "[")
13
+ depth++;
14
+ else if (c === "}" || c === ")" || c === "]") {
15
+ depth--;
16
+ if (depth === 0)
17
+ return i;
18
+ }
19
+ }
20
+ return -1;
21
+ }
22
+ function endOfString(text, from) {
23
+ const quote = text[from];
24
+ for (let i = from + 1; i < text.length; i++) {
25
+ if (text[i] === "\\")
26
+ i++;
27
+ else if (text[i] === quote)
28
+ return i;
29
+ }
30
+ return -1;
31
+ }
32
+ /** Splits at the commas that sit outside brackets and strings */
33
+ function topLevelParts(text) {
34
+ const parts = [];
35
+ let depth = 0;
36
+ let start = 0;
37
+ for (let i = 0; i < text.length; i++) {
38
+ const c = text[i];
39
+ if (c === '"' || c === "'" || c === "`") {
40
+ const end = endOfString(text, i);
41
+ if (end < 0)
42
+ break;
43
+ i = end;
44
+ }
45
+ else if (c === "{" || c === "(" || c === "[")
46
+ depth++;
47
+ else if (c === "}" || c === ")" || c === "]")
48
+ depth--;
49
+ else if (c === "," && depth === 0) {
50
+ parts.push(text.slice(start, i));
51
+ start = i + 1;
52
+ }
53
+ }
54
+ parts.push(text.slice(start));
55
+ return parts.map((part) => part.trim()).filter((part) => part !== "");
56
+ }
57
+ /** The property a destructuring entry reads: `a`, `a: b`, `a = 1`, `"a-b": c`; undefined for a computed key */
58
+ function keyOf(entry) {
59
+ const quoted = /^(["'])(.*?)\1/.exec(entry);
60
+ if (quoted)
61
+ return quoted[2];
62
+ return /^[\w$]+/.exec(entry)?.[0];
63
+ }
64
+ /**
65
+ * The fixture names a function destructures from its first parameter (`async ({ page, user }) => ...`), read
66
+ * from its source. A `...rest` entry asks for everything.
67
+ */
68
+ export function requestedBy(fn) {
69
+ const text = Function.prototype.toString.call(fn);
70
+ if (/^(async\s+)?[\w$]+\s*=>/.test(text))
71
+ return OPAQUE;
72
+ const open = text.indexOf("(");
73
+ if (open < 0)
74
+ return OPAQUE;
75
+ let at = open + 1;
76
+ while (/\s/.test(text[at] ?? ""))
77
+ at++;
78
+ if (text[at] === ")")
79
+ return { kind: "names", names: [], rest: false };
80
+ if (text[at] !== "{")
81
+ return OPAQUE;
82
+ const end = closing(text, at);
83
+ if (end < 0)
84
+ return OPAQUE;
85
+ const names = [];
86
+ let rest = false;
87
+ for (const entry of topLevelParts(text.slice(at + 1, end))) {
88
+ const key = entry.startsWith("...") ? undefined : keyOf(entry);
89
+ if (key === undefined)
90
+ rest = true;
91
+ else
92
+ names.push(key);
93
+ }
94
+ return { kind: "names", names, rest };
95
+ }
@@ -0,0 +1,22 @@
1
+ /** The function of a fixture: sets the value up, calls `use(value)`, cleans up after `use` returns */
2
+ type FixtureSetup = (args: Record<string, unknown>, use: (value: unknown) => Promise<void>) => Promise<void>;
3
+ /** One definition of a fixture: the value or the setup function, what it depends on, whether every test gets it */
4
+ export interface FixtureDef {
5
+ name: string;
6
+ auto: boolean;
7
+ deps: string[];
8
+ setup?: FixtureSetup;
9
+ value?: unknown;
10
+ }
11
+ /** Every custom fixture by name; the last definition of a name wins, the ones before it are what it may depend on by its own name */
12
+ export type FixtureLayers = ReadonlyMap<string, readonly FixtureDef[]>;
13
+ /** The fixtures the runner builds for every test: a custom fixture of the same name overrides it */
14
+ export declare const BUILT_IN_FIXTURES: ReadonlySet<string>;
15
+ /** Where a dependency of `name` (at definition `level`) comes from: another definition, or the built-in fixture */
16
+ export declare function dependencyOf(layers: FixtureLayers, name: string, level: number, dep: string): {
17
+ name: string;
18
+ level: number;
19
+ } | "built-in" | undefined;
20
+ /** The fixtures after `test.extend(definitions)` on a test that has `base`; throws `extend(): ...` for a definition that cannot work */
21
+ export declare function extendLayers(base: FixtureLayers | undefined, definitions: unknown): FixtureLayers;
22
+ export {};
@@ -0,0 +1,103 @@
1
+ import { requestedBy } from "./fixture-args.js";
2
+ /** The fixtures the runner builds for every test: a custom fixture of the same name overrides it */
3
+ export const BUILT_IN_FIXTURES = new Set([
4
+ "page",
5
+ "browser",
6
+ "browserName",
7
+ "workerIndex",
8
+ "env",
9
+ "secrets",
10
+ "inputs",
11
+ "outputs",
12
+ "context",
13
+ "baseURL",
14
+ "testInfo",
15
+ "isMobile",
16
+ "hasTouch",
17
+ ]);
18
+ const OPTION_KEYS = new Set(["auto", "scope"]);
19
+ function isPlainObject(value) {
20
+ if (typeof value !== "object" || value === null)
21
+ return false;
22
+ const proto = Object.getPrototypeOf(value);
23
+ return proto === Object.prototype || proto === null;
24
+ }
25
+ /** `[fixture, options]` is always a pair with options; an array value goes in a function */
26
+ function checkedOptions(name, options) {
27
+ for (const key of Object.keys(options))
28
+ if (!OPTION_KEYS.has(key))
29
+ throw new Error(`extend(): fixture "${name}" has an unknown option "${key}"`);
30
+ if (options.auto !== undefined && typeof options.auto !== "boolean")
31
+ throw new Error(`extend(): fixture "${name}": auto must be true or false`);
32
+ if (options.scope === "worker")
33
+ throw new Error(`extend(): fixture "${name}" has scope "worker", which is not supported; fixtures live as long as one test ({ scope: "test" })`);
34
+ if (options.scope !== undefined && options.scope !== "test")
35
+ throw new Error(`extend(): fixture "${name}": scope must be "test"`);
36
+ return { auto: options.auto === true };
37
+ }
38
+ function definitionOf(name, raw) {
39
+ const [fixture, options] = Array.isArray(raw) && raw.length === 2 && isPlainObject(raw[1])
40
+ ? [raw[0], checkedOptions(name, raw[1])]
41
+ : [raw, { auto: false }];
42
+ if (typeof fixture !== "function")
43
+ return { name, auto: options.auto, deps: [], value: fixture };
44
+ const requested = requestedBy(fixture);
45
+ if (requested.kind === "opaque" || requested.rest)
46
+ throw new Error(`extend(): fixture "${name}" must destructure the fixtures it uses in its first argument, as ({ page }, use) or ({}, use)`);
47
+ return {
48
+ name,
49
+ auto: options.auto,
50
+ deps: requested.names,
51
+ setup: fixture,
52
+ };
53
+ }
54
+ /** Where a dependency of `name` (at definition `level`) comes from: another definition, or the built-in fixture */
55
+ export function dependencyOf(layers, name, level, dep) {
56
+ if (dep === name)
57
+ return level > 0
58
+ ? { name, level: level - 1 }
59
+ : BUILT_IN_FIXTURES.has(dep)
60
+ ? "built-in"
61
+ : undefined;
62
+ const defs = layers.get(dep);
63
+ if (defs)
64
+ return { name: dep, level: defs.length - 1 };
65
+ return BUILT_IN_FIXTURES.has(dep) ? "built-in" : undefined;
66
+ }
67
+ /** Throws for a dependency nobody defines and for a cycle (`a` needs `b` needs `a`) */
68
+ function validate(layers) {
69
+ const done = new Set();
70
+ const walk = (name, level, path) => {
71
+ const key = `${name}#${level}`;
72
+ if (done.has(key))
73
+ return;
74
+ if (path.includes(key)) {
75
+ const loop = [...path.slice(path.indexOf(key)), key];
76
+ throw new Error(`extend(): fixtures depend on each other in a circle: ${loop.map((k) => k.split("#")[0]).join(" -> ")}`);
77
+ }
78
+ const def = layers.get(name)?.[level];
79
+ if (!def)
80
+ return;
81
+ for (const dep of def.deps) {
82
+ const target = dependencyOf(layers, name, level, dep);
83
+ if (target === undefined)
84
+ throw new Error(`extend(): fixture "${name}" depends on unknown fixture "${dep}"`);
85
+ if (target !== "built-in")
86
+ walk(target.name, target.level, [...path, key]);
87
+ }
88
+ done.add(key);
89
+ };
90
+ for (const [name, defs] of layers)
91
+ for (let level = 0; level < defs.length; level++)
92
+ walk(name, level, []);
93
+ }
94
+ /** The fixtures after `test.extend(definitions)` on a test that has `base`; throws `extend(): ...` for a definition that cannot work */
95
+ export function extendLayers(base, definitions) {
96
+ if (!isPlainObject(definitions))
97
+ throw new Error("extend(): needs an object of fixtures");
98
+ const layers = new Map(base);
99
+ for (const [name, raw] of Object.entries(definitions))
100
+ layers.set(name, [...(layers.get(name) ?? []), definitionOf(name, raw)]);
101
+ validate(layers);
102
+ return layers;
103
+ }
@@ -0,0 +1,33 @@
1
+ import type { TestFixtures } from "../types/test.js";
2
+ import { type FixtureLayers } from "./fixture-defs.js";
3
+ /** A custom fixture whose setup threw or never called `use` */
4
+ export declare class FixtureSetupError extends Error {
5
+ constructor(name: string, cause: unknown);
6
+ }
7
+ /**
8
+ * The custom fixtures of one test run. Fixtures are set up when a test or hook asks for them (by destructuring
9
+ * them), their dependencies first, and torn down in the opposite order. The hooks and the body of a test share
10
+ * one scope, so they see the same instances.
11
+ */
12
+ export declare class FixtureScope {
13
+ private readonly layers;
14
+ private readonly base;
15
+ /** The built-in fixtures, with each custom fixture added (replacing a built-in of that name) once it is set up */
16
+ readonly fixtures: TestFixtures;
17
+ private readonly values;
18
+ private readonly pending;
19
+ private readonly started;
20
+ private closed;
21
+ constructor(layers: FixtureLayers, base: TestFixtures);
22
+ /** Sets up the fixtures marked `auto` */
23
+ prepareAuto(): Promise<void>;
24
+ /** Sets up the custom fixtures `fn` destructures from its first parameter */
25
+ prepare(fn: (...args: never[]) => unknown): Promise<void>;
26
+ /**
27
+ * Finishes the fixtures that were set up, last first, each within `limitMs`. Returns the first error (a
28
+ * fixture's cleanup threw or ran out of time); the others still run.
29
+ */
30
+ teardown(limitMs: number): Promise<Error | undefined>;
31
+ private ensure;
32
+ private setUp;
33
+ }