@vitest/browser 5.0.0-beta.1 → 5.0.0-beta.3

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 (35) hide show
  1. package/context.d.ts +40 -1
  2. package/dist/client/.vite/manifest.json +8 -8
  3. package/dist/client/__vitest__/assets/index-BlLo6Q_D.css +1 -0
  4. package/dist/client/__vitest__/assets/index-O8gheoYf.js +89 -0
  5. package/dist/client/__vitest__/index.html +2 -2
  6. package/dist/client/__vitest_browser__/defineProperty-C3k2g8Sk.js +267 -0
  7. package/dist/client/__vitest_browser__/orchestrator-B44yH1M4.js +343 -0
  8. package/dist/client/__vitest_browser__/rrweb-snapshot-iZCFA2to.js +4388 -0
  9. package/dist/client/__vitest_browser__/tester-Byk-s_d6.js +5088 -0
  10. package/dist/client/orchestrator.html +2 -2
  11. package/dist/client/tester/locators.d.ts +69 -0
  12. package/dist/client/tester/tester.html +2 -2
  13. package/dist/client/tester/trace.d.ts +11 -7
  14. package/dist/client.js +10 -1
  15. package/dist/context.js +73 -44
  16. package/dist/expect-element.js +30 -30
  17. package/dist/index.d.ts +6 -22
  18. package/dist/index.js +99 -4573
  19. package/dist/locators-DUkyvRhY.js +5 -0
  20. package/dist/locators.d.ts +9 -1
  21. package/dist/locators.js +1 -1
  22. package/dist/shared/screenshotMatcher/types.d.ts +4 -3
  23. package/dist/state.js +64 -14
  24. package/dist/types.d.ts +2 -0
  25. package/jest-dom.d.ts +1 -0
  26. package/matchers.d.ts +2 -1
  27. package/package.json +10 -7
  28. package/dist/client/__vitest__/assets/index-BmuVn2L3.js +0 -136
  29. package/dist/client/__vitest__/assets/index-CxYquQyv.css +0 -1
  30. package/dist/client/__vitest__/bg.png +0 -0
  31. package/dist/client/__vitest_browser__/orchestrator-pTEf6o0n.js +0 -383
  32. package/dist/client/__vitest_browser__/rrweb-snapshot-xhvrgOHx.js +0 -5476
  33. package/dist/client/__vitest_browser__/tester-CIKiUsoz.js +0 -2431
  34. package/dist/client/__vitest_browser__/utils-BYUpz6v6.js +0 -3379
  35. package/dist/index-BlWsE3ij.js +0 -5
@@ -26,8 +26,8 @@
26
26
  {__VITEST_INJECTOR__}
27
27
  {__VITEST_ERROR_CATCHER__}
28
28
  {__VITEST_SCRIPTS__}
29
- <script type="module" crossorigin src="/__vitest_browser__/orchestrator-pTEf6o0n.js"></script>
30
- <link rel="modulepreload" crossorigin href="/__vitest_browser__/utils-BYUpz6v6.js">
29
+ <script type="module" crossorigin src="/__vitest_browser__/orchestrator-B44yH1M4.js"></script>
30
+ <link rel="modulepreload" crossorigin href="/__vitest_browser__/defineProperty-C3k2g8Sk.js">
31
31
  </head>
32
32
  <body>
33
33
  <div id="vitest-tester"></div>
@@ -0,0 +1,69 @@
1
+ import type { LocatorByRoleOptions, LocatorOptions, LocatorScreenshotOptions, MarkOptions, SelectorOptions, UserEventClearOptions, UserEventClickOptions, UserEventDragAndDropOptions, UserEventFillOptions, UserEventHoverOptions, UserEventSelectOptions, UserEventUploadOptions, UserEventWheelOptions } from "vitest/browser";
2
+ import { Ivya } from "ivya";
3
+ export { ensureAwaited } from "../utils.js";
4
+ export { convertElementToCssSelector, getIframeScale, processTimeoutOptions } from "./tester-utils.js";
5
+ export { getByAltTextSelector, getByLabelSelector, getByPlaceholderSelector, getByRoleSelector, getByTestIdSelector, getByTextSelector, getByTitleSelector } from "ivya";
6
+ export declare const selectorEngine: Ivya;
7
+ export declare abstract class Locator {
8
+ abstract selector: string;
9
+ private _parsedSelector;
10
+ protected _container?: Element | undefined;
11
+ protected _pwSelector?: string | undefined;
12
+ protected _pwLocator?: string | undefined;
13
+ protected _errorSource?: Error;
14
+ constructor();
15
+ click(options?: UserEventClickOptions): Promise<void>;
16
+ dblClick(options?: UserEventClickOptions): Promise<void>;
17
+ tripleClick(options?: UserEventClickOptions): Promise<void>;
18
+ wheel(options: UserEventWheelOptions): Promise<void>;
19
+ clear(options?: UserEventClearOptions): Promise<void>;
20
+ hover(options?: UserEventHoverOptions): Promise<void>;
21
+ unhover(options?: UserEventHoverOptions): Promise<void>;
22
+ fill(text: string, options?: UserEventFillOptions): Promise<void>;
23
+ upload(files: string | string[] | File | File[], options?: UserEventUploadOptions): Promise<void>;
24
+ dropTo(target: Locator, options?: UserEventDragAndDropOptions): Promise<void>;
25
+ selectOptions(value: HTMLElement | HTMLElement[] | Locator | Locator[] | string | string[], options?: UserEventSelectOptions): Promise<void>;
26
+ screenshot(options: Omit<LocatorScreenshotOptions, "base64"> & {
27
+ base64: true;
28
+ }): Promise<{
29
+ path: string;
30
+ base64: string;
31
+ }>;
32
+ screenshot(options?: LocatorScreenshotOptions): Promise<string>;
33
+ mark(name: string, options?: MarkOptions): Promise<void>;
34
+ protected abstract locator(selector: string): Locator;
35
+ protected abstract elementLocator(element: Element): Locator;
36
+ getByRole(role: string, options?: LocatorByRoleOptions): Locator;
37
+ getByAltText(text: string | RegExp, options?: LocatorOptions): Locator;
38
+ getByLabelText(text: string | RegExp, options?: LocatorOptions): Locator;
39
+ getByPlaceholder(text: string | RegExp, options?: LocatorOptions): Locator;
40
+ getByTestId(testId: string | RegExp): Locator;
41
+ getByText(text: string | RegExp, options?: LocatorOptions): Locator;
42
+ getByTitle(title: string | RegExp, options?: LocatorOptions): Locator;
43
+ filter(filter: LocatorOptions): Locator;
44
+ and(locator: Locator): Locator;
45
+ or(locator: Locator): Locator;
46
+ query(): HTMLElement | SVGElement | null;
47
+ element(): HTMLElement | SVGElement;
48
+ elements(): (HTMLElement | SVGElement)[];
49
+ get length(): number;
50
+ all(): Locator[];
51
+ nth(index: number): Locator;
52
+ first(): Locator;
53
+ last(): Locator;
54
+ toString(): string;
55
+ serialize(): SerializedLocator;
56
+ asLocator(): string;
57
+ toJSON(): SerializedLocator;
58
+ findElement(options_?: SelectorOptions): Promise<HTMLElement | SVGElement>;
59
+ protected triggerCommand<T>(command: string, ...args: any[]): Promise<T>;
60
+ }
61
+ export declare function triggerCommandWithTrace<T>(options: {
62
+ name: string;
63
+ arguments: unknown[];
64
+ errorSource?: Error | undefined;
65
+ }): Promise<T>;
66
+ export interface SerializedLocator {
67
+ selector: string;
68
+ locator: string;
69
+ }
@@ -5,8 +5,8 @@
5
5
  <link rel="icon" href="{__VITEST_FAVICON__}" type="image/svg+xml">
6
6
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
7
7
  <title>Vitest Browser Tester</title>
8
- <script type="module" crossorigin src="/__vitest_browser__/tester-CIKiUsoz.js"></script>
9
- <link rel="modulepreload" crossorigin href="/__vitest_browser__/utils-BYUpz6v6.js">
8
+ <script type="module" crossorigin src="/__vitest_browser__/tester-Byk-s_d6.js"></script>
9
+ <link rel="modulepreload" crossorigin href="/__vitest_browser__/defineProperty-C3k2g8Sk.js">
10
10
  </head>
11
11
  <body>
12
12
  </body>
@@ -1,16 +1,23 @@
1
1
  import type { Task } from "@vitest/runner";
2
+ import type { BrowserTraceEntryKind } from "vitest/browser";
3
+ import type { SerializedLocator } from "./locators.js";
2
4
  export interface BrowserTraceData {
3
5
  retry: number;
4
6
  repeats: number;
5
7
  recordCanvas: boolean;
6
8
  entries: BrowserTraceEntry[];
7
9
  }
8
- export type BrowserTraceEntryKind = "action" | "expect" | "mark" | "lifecycle";
9
10
  export type BrowserTraceEntryStatus = "pass" | "fail";
11
+ export type BrowserTraceEntryRangePhase = "start" | "end";
10
12
  export type BrowserTraceSelectorResolution = "matched" | "missing" | "error";
13
+ export interface BrowserTraceEntryRange {
14
+ id: string;
15
+ phase: BrowserTraceEntryRangePhase;
16
+ }
11
17
  export interface BrowserTraceEntry {
12
18
  name: string;
13
19
  kind: BrowserTraceEntryKind;
20
+ range?: BrowserTraceEntryRange;
14
21
  status?: BrowserTraceEntryStatus;
15
22
  startTime: number;
16
23
  duration?: number;
@@ -20,7 +27,7 @@ export interface BrowserTraceEntry {
20
27
  line: number;
21
28
  column: number;
22
29
  };
23
- selector?: string;
30
+ element?: SerializedLocator;
24
31
  snapshot: TraceSnapshot;
25
32
  }
26
33
  interface TraceSnapshot {
@@ -40,14 +47,11 @@ interface TraceSnapshot {
40
47
  }
41
48
  declare const PSEUDO_CLASS_NAMES: readonly [":hover", ":active", ":focus", ":focus-visible", ":focus-within"];
42
49
  type PseudoClassName = (typeof PSEUDO_CLASS_NAMES)[number];
43
- export type BrowserTraceState = Record<string, BrowserTraceData>;
44
50
  export interface BrowserTraceAttempt {
45
51
  retry: number;
46
52
  repeats: number;
47
53
  startTime: number;
48
54
  }
49
- export declare function recordBrowserTraceEntry(task: Task, options: Omit<BrowserTraceEntry, "snapshot" | "startTime"> & {
50
- startTime?: number;
51
- }): void;
52
- export declare function getBrowserTrace(testId: string, repeats: number, retry: number): BrowserTraceData | undefined;
55
+ export declare function createBrowserTraceRangeId(): string;
56
+ export declare function recordBrowserTraceEntry(task: Task, options: Omit<BrowserTraceEntry, "snapshot" | "startTime">): Promise<void>;
53
57
  export {};
package/dist/client.js CHANGED
@@ -337,6 +337,10 @@ const onCancelCallbacks = [];
337
337
  function onCancel(callback) {
338
338
  onCancelCallbacks.push(callback);
339
339
  }
340
+ let pageMarkHandler = null;
341
+ function registerPageMarkHandler(handler) {
342
+ pageMarkHandler = handler;
343
+ }
340
344
  // ws connection can be established before the orchestrator is fully loaded
341
345
  // in very rare cases in the preview provider
342
346
  function waitForOrchestrator() {
@@ -385,6 +389,11 @@ function createClient() {
385
389
  }
386
390
  cdp.emit(event, payload);
387
391
  },
392
+ async pageMark(name, options) {
393
+ if (pageMarkHandler) {
394
+ await pageMarkHandler(name, options);
395
+ }
396
+ },
388
397
  async resolveManualMock(url) {
389
398
  // @ts-expect-error not typed global API
390
399
  const mocker = globalThis.__vitest_mocker__;
@@ -461,4 +470,4 @@ function createClient() {
461
470
  }
462
471
  const client = createClient();
463
472
 
464
- export { ENTRY_URL, HOST, PORT, RPC_ID, channel, client, globalChannel, onCancel };
473
+ export { ENTRY_URL, HOST, PORT, RPC_ID, channel, client, globalChannel, onCancel, registerPageMarkHandler };
package/dist/context.js CHANGED
@@ -66,27 +66,13 @@ const PSEUDO_CLASS_NAMES = [
66
66
  ":focus-visible",
67
67
  ":focus-within"
68
68
  ];
69
- function getBrowserTraceState() {
70
- return getBrowserState().browserTraceState ??= {};
69
+ function createBrowserTraceRangeId() {
70
+ return Math.random().toString(36).slice(2);
71
71
  }
72
- function getTraceStateKey(testId, repeats, retry) {
73
- return `${testId}:${repeats}:${retry}`;
74
- }
75
- // TODO: should we avoid accumulating? send and immediately clear each entry to save memory?
76
- function recordBrowserTraceEntry(task, options) {
77
- // TODO: trace-view currently receives selectors after locator/action resolution,
78
- // so provider-specific lowered selectors can leak into snapshot lookup. Preserve
79
- // the original locator selector, or record the target node id before lowering.
80
- // for example, this causes shadow dom selectors to fail with `>>>` marker.
81
- // For now, remove trivial `html >` prefix generated by convertElementToCssSelector.
82
- // this is also necessary to `engine.querySelector + document.documentElement`
83
- // to find an element on webdriverio
84
- if (options.selector?.startsWith("html >")) {
85
- options.selector = options.selector.slice(6);
86
- }
72
+ async function recordBrowserTraceEntry(task, options) {
87
73
  const attemptInfo = getBrowserState().browserTraceAttempts.get(task.id);
88
- const relativeStartTime = (options.startTime ?? now()) - attemptInfo.startTime;
89
- const snapshot = takeSnapshot(options.selector);
74
+ const relativeStartTime = now() - attemptInfo.startTime;
75
+ const snapshot = takeSnapshot(options.element);
90
76
  const entry = {
91
77
  ...options,
92
78
  startTime: relativeStartTime,
@@ -94,15 +80,21 @@ function recordBrowserTraceEntry(task, options) {
94
80
  };
95
81
  const { retry, repeats } = attemptInfo;
96
82
  const { recordCanvas } = getBrowserState().config.browser.traceView;
97
- const state = getBrowserTraceState();
98
- const traceKey = getTraceStateKey(task.id, repeats, retry);
99
- state[traceKey] ??= {
83
+ // An async lane could defer artifact recording and flush it at test-attempt end,
84
+ // but the synchronous snapshot work is already a comparable cost, and this path
85
+ // is mostly data passing after that.
86
+ // Keep it simple unless measurements show artifact recording is a bottleneck.
87
+ const data = {
100
88
  retry,
101
89
  repeats,
102
90
  recordCanvas,
103
- entries: []
91
+ entries: [entry]
104
92
  };
105
- state[traceKey].entries.push(entry);
93
+ const rpc = getWorkerState().rpc;
94
+ await rpc.triggerCommand(getBrowserState().sessionId, "__vitest_recordBrowserTrace", undefined, [{
95
+ testId: task.id,
96
+ data
97
+ }]);
106
98
  }
107
99
  // Resolve ivya selector to a DOM element and take a snapshot with rrweb Mirror
108
100
  // so we can store the nodeId for provider-agnostic element highlighting in the viewer.
@@ -110,7 +102,7 @@ function recordBrowserTraceEntry(task, options) {
110
102
  // selector engine inside the snapshot iframe at view time via injected script.
111
103
  // Our approach resolves at collection time (same moment as snapshot) — simpler but
112
104
  // requires Mirror plumbing. nodeId-based lookup also works across shadow DOM, unlike querySelector.
113
- function takeSnapshot(selector) {
105
+ function takeSnapshot(serializedLocator) {
114
106
  const { snapshot, createMirror } = getBrowserState().browserTraceDomSnapshot;
115
107
  const traceView = getBrowserState().config.browser.traceView;
116
108
  const engine = getBrowserState().selectorEngine;
@@ -137,9 +129,9 @@ function takeSnapshot(selector) {
137
129
  const ids = Array.from(elements, (el) => mirror.getId(el)).filter((id) => id !== -1);
138
130
  result.pseudoClassIds[className] = ids;
139
131
  }
140
- if (selector) {
132
+ if (serializedLocator) {
141
133
  try {
142
- const el = engine.querySelector(engine.parseSelector(selector), document.documentElement, false);
134
+ const el = engine.querySelector(engine.parseSelector(serializedLocator._pwSelector ?? serializedLocator.selector), document.documentElement, false);
143
135
  if (!el) {
144
136
  result.selectorResolution = "missing";
145
137
  } else {
@@ -262,19 +254,28 @@ function processTimeoutOptions(options_) {
262
254
  }
263
255
  const provider$1 = getBrowserState().provider;
264
256
  const kElementLocator = Symbol.for("$$vitest:locator-resolved");
265
- async function convertToSelector(elementOrLocator, options) {
257
+ async function serializeElement(elementOrLocator, options) {
266
258
  if (!elementOrLocator) {
267
259
  throw new Error("Expected element or locator to be defined.");
268
260
  }
269
261
  if (elementOrLocator instanceof Element) {
270
- return convertElementToCssSelector(elementOrLocator);
262
+ const selector = convertElementToCssSelector(elementOrLocator);
263
+ return {
264
+ selector,
265
+ locator: __INTERNAL._asLocator("javascript", selector)
266
+ };
271
267
  }
272
268
  if (isLocator(elementOrLocator)) {
273
269
  if (provider$1 === "playwright" || kElementLocator in elementOrLocator) {
274
- return elementOrLocator.selector;
270
+ return elementOrLocator.serialize();
275
271
  }
276
272
  const element = await elementOrLocator.findElement(options);
277
- return convertElementToCssSelector(element);
273
+ const selector = convertElementToCssSelector(element);
274
+ const locator = __INTERNAL._asLocator("javascript", selector);
275
+ return {
276
+ selector,
277
+ locator
278
+ };
278
279
  }
279
280
  throw new Error("Expected element or locator to be an instance of Element or Locator.");
280
281
  }
@@ -380,9 +381,9 @@ function createUserEvent(__tl_user_event_base__, options) {
380
381
  },
381
382
  type(element, text, options) {
382
383
  return ensureAwaited(async (error) => {
383
- const selector = await convertToSelector(element, options);
384
+ const serializedElement = await serializeElement(element, options);
384
385
  const { unreleased } = await triggerCommand("__vitest_type", [
385
- selector,
386
+ serializedElement,
386
387
  text,
387
388
  {
388
389
  ...options,
@@ -566,7 +567,7 @@ const page = {
566
567
  screenshotIds[repeatCount] ??= {};
567
568
  screenshotIds[repeatCount][taskName] = number + 1;
568
569
  const name = options.path || `${taskName.replace(/[^a-z0-9]/gi, "-")}-${number}.png`;
569
- const [element, ...mask] = await Promise.all([options.element ? convertToSelector(options.element, options) : undefined, ..."mask" in options ? options.mask.map((el) => convertToSelector(el, options)) : []]);
570
+ const [element, ...mask] = await Promise.all([options.element ? serializeElement(options.element, options) : undefined, ..."mask" in options ? options.mask.map((el) => serializeElement(el, options)) : []]);
570
571
  const normalizedOptions = "mask" in options ? {
571
572
  ...options,
572
573
  mask
@@ -586,13 +587,24 @@ const page = {
586
587
  if (typeof bodyOrOptions === "function") {
587
588
  return ensureAwaited(async (error) => {
588
589
  let status = "pass";
589
- const startTime = now();
590
+ const traceRangeId = hasActiveTraceView ? createBrowserTraceRangeId() : undefined;
590
591
  if (hasActiveTrace) {
591
592
  await triggerCommand("__vitest_groupTraceStart", [{
592
593
  name,
593
594
  stack: options?.stack ?? error?.stack
594
595
  }], error);
595
596
  }
597
+ if (hasActiveTraceView) {
598
+ await recordBrowserTraceEntry(currentTest, {
599
+ name,
600
+ kind: "mark",
601
+ range: {
602
+ id: traceRangeId,
603
+ phase: "start"
604
+ },
605
+ stack: options?.stack ?? error?.stack
606
+ });
607
+ }
596
608
  try {
597
609
  return await bodyOrOptions();
598
610
  } catch (err) {
@@ -600,13 +612,14 @@ const page = {
600
612
  throw err;
601
613
  } finally {
602
614
  if (hasActiveTraceView) {
603
- // TODO: support nested trace
604
- recordBrowserTraceEntry(currentTest, {
615
+ await recordBrowserTraceEntry(currentTest, {
605
616
  name,
606
- kind: "mark",
617
+ kind: options?.kind ?? "mark",
618
+ range: {
619
+ id: traceRangeId,
620
+ phase: "end"
621
+ },
607
622
  status,
608
- startTime,
609
- duration: now() - startTime,
610
623
  stack: options?.stack ?? error?.stack
611
624
  });
612
625
  }
@@ -619,11 +632,11 @@ const page = {
619
632
  if (!hasActiveTrace && !hasActiveTraceView) {
620
633
  return Promise.resolve();
621
634
  }
622
- return ensureAwaited((error) => {
635
+ return ensureAwaited(async (error) => {
623
636
  if (hasActiveTraceView) {
624
- recordBrowserTraceEntry(currentTest, {
637
+ await recordBrowserTraceEntry(currentTest, {
625
638
  name,
626
- kind: "mark",
639
+ kind: bodyOrOptions?.kind ?? "mark",
627
640
  stack: bodyOrOptions?.stack ?? error?.stack
628
641
  });
629
642
  }
@@ -754,10 +767,26 @@ function prettyDOM(dom, maxLength = Number(defaultOptions?.maxLength ?? import.m
754
767
  return dom.outerHTML.length > maxLength ? `${pretty.slice(0, maxLength)}...` : pretty;
755
768
  }
756
769
  function getElementError(selector, container) {
757
- const error = new Error(`Cannot find element with locator: ${__INTERNAL._asLocator("javascript", selector)}\n\n${prettyDOM(container)}`);
770
+ const locator = typeof selector === "string" ? __INTERNAL._asLocator("javascript", selector) : selector.asLocator();
771
+ const formatted = formatDOM(container);
772
+ const error = new Error(`Cannot find element with locator: ${locator}\n\n${formatted}`);
758
773
  error.name = "VitestBrowserElementError";
759
774
  return error;
760
775
  }
776
+ function formatDOM(container) {
777
+ const format = getBrowserState().config.browser.locators.errorFormat;
778
+ if (format === "aria") {
779
+ return `ARIA tree:\n${formatAriaTree(container)}`;
780
+ }
781
+ if (format === "all") {
782
+ return `ARIA tree:\n${formatAriaTree(container)}\n\nHTML:\n${prettyDOM(container)}`;
783
+ }
784
+ return prettyDOM(container);
785
+ }
786
+ function formatAriaTree(container) {
787
+ const { generateAriaTree, renderAriaTree } = getBrowserState().aria;
788
+ return renderAriaTree(generateAriaTree(container));
789
+ }
761
790
  function configurePrettyDOM(options) {
762
791
  defaultOptions = options;
763
792
  }