@vitest/browser-playwright 4.0.0-beta.14

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/index.js ADDED
@@ -0,0 +1,1099 @@
1
+ import { parseKeyDef, resolveScreenshotPath, defineBrowserProvider } from '@vitest/browser';
2
+ export { defineBrowserCommand } from '@vitest/browser';
3
+ import { createManualModuleSource } from '@vitest/mocker/node';
4
+ import c from 'tinyrainbow';
5
+ import { createDebugger, isCSSRequest } from 'vitest/node';
6
+ import { mkdir, unlink } from 'node:fs/promises';
7
+ import { fileURLToPath } from 'node:url';
8
+
9
+ const _DRIVE_LETTER_START_RE = /^[A-Za-z]:\//;
10
+ function normalizeWindowsPath(input = "") {
11
+ if (!input) {
12
+ return input;
13
+ }
14
+ return input.replace(/\\/g, "/").replace(_DRIVE_LETTER_START_RE, (r) => r.toUpperCase());
15
+ }
16
+
17
+ const _UNC_REGEX = /^[/\\]{2}/;
18
+ const _IS_ABSOLUTE_RE = /^[/\\](?![/\\])|^[/\\]{2}(?!\.)|^[A-Za-z]:[/\\]/;
19
+ const _DRIVE_LETTER_RE = /^[A-Za-z]:$/;
20
+ const _ROOT_FOLDER_RE = /^\/([A-Za-z]:)?$/;
21
+ const normalize = function(path) {
22
+ if (path.length === 0) {
23
+ return ".";
24
+ }
25
+ path = normalizeWindowsPath(path);
26
+ const isUNCPath = path.match(_UNC_REGEX);
27
+ const isPathAbsolute = isAbsolute(path);
28
+ const trailingSeparator = path[path.length - 1] === "/";
29
+ path = normalizeString(path, !isPathAbsolute);
30
+ if (path.length === 0) {
31
+ if (isPathAbsolute) {
32
+ return "/";
33
+ }
34
+ return trailingSeparator ? "./" : ".";
35
+ }
36
+ if (trailingSeparator) {
37
+ path += "/";
38
+ }
39
+ if (_DRIVE_LETTER_RE.test(path)) {
40
+ path += "/";
41
+ }
42
+ if (isUNCPath) {
43
+ if (!isPathAbsolute) {
44
+ return `//./${path}`;
45
+ }
46
+ return `//${path}`;
47
+ }
48
+ return isPathAbsolute && !isAbsolute(path) ? `/${path}` : path;
49
+ };
50
+ function cwd() {
51
+ if (typeof process !== "undefined" && typeof process.cwd === "function") {
52
+ return process.cwd().replace(/\\/g, "/");
53
+ }
54
+ return "/";
55
+ }
56
+ const resolve = function(...arguments_) {
57
+ arguments_ = arguments_.map((argument) => normalizeWindowsPath(argument));
58
+ let resolvedPath = "";
59
+ let resolvedAbsolute = false;
60
+ for (let index = arguments_.length - 1; index >= -1 && !resolvedAbsolute; index--) {
61
+ const path = index >= 0 ? arguments_[index] : cwd();
62
+ if (!path || path.length === 0) {
63
+ continue;
64
+ }
65
+ resolvedPath = `${path}/${resolvedPath}`;
66
+ resolvedAbsolute = isAbsolute(path);
67
+ }
68
+ resolvedPath = normalizeString(resolvedPath, !resolvedAbsolute);
69
+ if (resolvedAbsolute && !isAbsolute(resolvedPath)) {
70
+ return `/${resolvedPath}`;
71
+ }
72
+ return resolvedPath.length > 0 ? resolvedPath : ".";
73
+ };
74
+ function normalizeString(path, allowAboveRoot) {
75
+ let res = "";
76
+ let lastSegmentLength = 0;
77
+ let lastSlash = -1;
78
+ let dots = 0;
79
+ let char = null;
80
+ for (let index = 0; index <= path.length; ++index) {
81
+ if (index < path.length) {
82
+ char = path[index];
83
+ } else if (char === "/") {
84
+ break;
85
+ } else {
86
+ char = "/";
87
+ }
88
+ if (char === "/") {
89
+ if (lastSlash === index - 1 || dots === 1) ; else if (dots === 2) {
90
+ if (res.length < 2 || lastSegmentLength !== 2 || res[res.length - 1] !== "." || res[res.length - 2] !== ".") {
91
+ if (res.length > 2) {
92
+ const lastSlashIndex = res.lastIndexOf("/");
93
+ if (lastSlashIndex === -1) {
94
+ res = "";
95
+ lastSegmentLength = 0;
96
+ } else {
97
+ res = res.slice(0, lastSlashIndex);
98
+ lastSegmentLength = res.length - 1 - res.lastIndexOf("/");
99
+ }
100
+ lastSlash = index;
101
+ dots = 0;
102
+ continue;
103
+ } else if (res.length > 0) {
104
+ res = "";
105
+ lastSegmentLength = 0;
106
+ lastSlash = index;
107
+ dots = 0;
108
+ continue;
109
+ }
110
+ }
111
+ if (allowAboveRoot) {
112
+ res += res.length > 0 ? "/.." : "..";
113
+ lastSegmentLength = 2;
114
+ }
115
+ } else {
116
+ if (res.length > 0) {
117
+ res += `/${path.slice(lastSlash + 1, index)}`;
118
+ } else {
119
+ res = path.slice(lastSlash + 1, index);
120
+ }
121
+ lastSegmentLength = index - lastSlash - 1;
122
+ }
123
+ lastSlash = index;
124
+ dots = 0;
125
+ } else if (char === "." && dots !== -1) {
126
+ ++dots;
127
+ } else {
128
+ dots = -1;
129
+ }
130
+ }
131
+ return res;
132
+ }
133
+ const isAbsolute = function(p) {
134
+ return _IS_ABSOLUTE_RE.test(p);
135
+ };
136
+ const relative = function(from, to) {
137
+ const _from = resolve(from).replace(_ROOT_FOLDER_RE, "$1").split("/");
138
+ const _to = resolve(to).replace(_ROOT_FOLDER_RE, "$1").split("/");
139
+ if (_to[0][1] === ":" && _from[0][1] === ":" && _from[0] !== _to[0]) {
140
+ return _to.join("/");
141
+ }
142
+ const _fromCopy = [..._from];
143
+ for (const segment of _fromCopy) {
144
+ if (_to[0] !== segment) {
145
+ break;
146
+ }
147
+ _from.shift();
148
+ _to.shift();
149
+ }
150
+ return [..._from.map(() => ".."), ..._to].join("/");
151
+ };
152
+ const dirname = function(p) {
153
+ const segments = normalizeWindowsPath(p).replace(/\/$/, "").split("/").slice(0, -1);
154
+ if (segments.length === 1 && _DRIVE_LETTER_RE.test(segments[0])) {
155
+ segments[0] += "/";
156
+ }
157
+ return segments.join("/") || (isAbsolute(p) ? "/" : ".");
158
+ };
159
+ const basename = function(p, extension) {
160
+ const segments = normalizeWindowsPath(p).split("/");
161
+ let lastSegment = "";
162
+ for (let i = segments.length - 1; i >= 0; i--) {
163
+ const val = segments[i];
164
+ if (val) {
165
+ lastSegment = val;
166
+ break;
167
+ }
168
+ }
169
+ return extension && lastSegment.endsWith(extension) ? lastSegment.slice(0, -extension.length) : lastSegment;
170
+ };
171
+
172
+ const clear = async (context, selector) => {
173
+ const { iframe } = context;
174
+ const element = iframe.locator(selector);
175
+ await element.clear();
176
+ };
177
+
178
+ const click = async (context, selector, options = {}) => {
179
+ const tester = context.iframe;
180
+ await tester.locator(selector).click(options);
181
+ };
182
+ const dblClick = async (context, selector, options = {}) => {
183
+ const tester = context.iframe;
184
+ await tester.locator(selector).dblclick(options);
185
+ };
186
+ const tripleClick = async (context, selector, options = {}) => {
187
+ const tester = context.iframe;
188
+ await tester.locator(selector).click({
189
+ ...options,
190
+ clickCount: 3
191
+ });
192
+ };
193
+
194
+ const dragAndDrop = async (context, source, target, options_) => {
195
+ const frame = await context.frame();
196
+ await frame.dragAndDrop(source, target, options_);
197
+ };
198
+
199
+ const fill = async (context, selector, text, options = {}) => {
200
+ const { iframe } = context;
201
+ const element = iframe.locator(selector);
202
+ await element.fill(text, options);
203
+ };
204
+
205
+ const hover = async (context, selector, options = {}) => {
206
+ await context.iframe.locator(selector).hover(options);
207
+ };
208
+
209
+ const keyboard = async (context, text, state) => {
210
+ const frame = await context.frame();
211
+ await frame.evaluate(focusIframe);
212
+ const pressed = new Set(state.unreleased);
213
+ await keyboardImplementation(pressed, context.provider, context.sessionId, text, async () => {
214
+ const frame = await context.frame();
215
+ await frame.evaluate(selectAll);
216
+ }, true);
217
+ return { unreleased: Array.from(pressed) };
218
+ };
219
+ const keyboardCleanup = async (context, state) => {
220
+ const { provider, sessionId } = context;
221
+ if (!state.unreleased) {
222
+ return;
223
+ }
224
+ const page = provider.getPage(sessionId);
225
+ for (const key of state.unreleased) {
226
+ await page.keyboard.up(key);
227
+ }
228
+ };
229
+ // fallback to insertText for non US key
230
+ // https://github.com/microsoft/playwright/blob/50775698ae13642742f2a1e8983d1d686d7f192d/packages/playwright-core/src/server/input.ts#L95
231
+ const VALID_KEYS = new Set([
232
+ "Escape",
233
+ "F1",
234
+ "F2",
235
+ "F3",
236
+ "F4",
237
+ "F5",
238
+ "F6",
239
+ "F7",
240
+ "F8",
241
+ "F9",
242
+ "F10",
243
+ "F11",
244
+ "F12",
245
+ "Backquote",
246
+ "`",
247
+ "~",
248
+ "Digit1",
249
+ "1",
250
+ "!",
251
+ "Digit2",
252
+ "2",
253
+ "@",
254
+ "Digit3",
255
+ "3",
256
+ "#",
257
+ "Digit4",
258
+ "4",
259
+ "$",
260
+ "Digit5",
261
+ "5",
262
+ "%",
263
+ "Digit6",
264
+ "6",
265
+ "^",
266
+ "Digit7",
267
+ "7",
268
+ "&",
269
+ "Digit8",
270
+ "8",
271
+ "*",
272
+ "Digit9",
273
+ "9",
274
+ "(",
275
+ "Digit0",
276
+ "0",
277
+ ")",
278
+ "Minus",
279
+ "-",
280
+ "_",
281
+ "Equal",
282
+ "=",
283
+ "+",
284
+ "Backslash",
285
+ "\\",
286
+ "|",
287
+ "Backspace",
288
+ "Tab",
289
+ "KeyQ",
290
+ "q",
291
+ "Q",
292
+ "KeyW",
293
+ "w",
294
+ "W",
295
+ "KeyE",
296
+ "e",
297
+ "E",
298
+ "KeyR",
299
+ "r",
300
+ "R",
301
+ "KeyT",
302
+ "t",
303
+ "T",
304
+ "KeyY",
305
+ "y",
306
+ "Y",
307
+ "KeyU",
308
+ "u",
309
+ "U",
310
+ "KeyI",
311
+ "i",
312
+ "I",
313
+ "KeyO",
314
+ "o",
315
+ "O",
316
+ "KeyP",
317
+ "p",
318
+ "P",
319
+ "BracketLeft",
320
+ "[",
321
+ "{",
322
+ "BracketRight",
323
+ "]",
324
+ "}",
325
+ "CapsLock",
326
+ "KeyA",
327
+ "a",
328
+ "A",
329
+ "KeyS",
330
+ "s",
331
+ "S",
332
+ "KeyD",
333
+ "d",
334
+ "D",
335
+ "KeyF",
336
+ "f",
337
+ "F",
338
+ "KeyG",
339
+ "g",
340
+ "G",
341
+ "KeyH",
342
+ "h",
343
+ "H",
344
+ "KeyJ",
345
+ "j",
346
+ "J",
347
+ "KeyK",
348
+ "k",
349
+ "K",
350
+ "KeyL",
351
+ "l",
352
+ "L",
353
+ "Semicolon",
354
+ ";",
355
+ ":",
356
+ "Quote",
357
+ "'",
358
+ "\"",
359
+ "Enter",
360
+ "\n",
361
+ "\r",
362
+ "ShiftLeft",
363
+ "Shift",
364
+ "KeyZ",
365
+ "z",
366
+ "Z",
367
+ "KeyX",
368
+ "x",
369
+ "X",
370
+ "KeyC",
371
+ "c",
372
+ "C",
373
+ "KeyV",
374
+ "v",
375
+ "V",
376
+ "KeyB",
377
+ "b",
378
+ "B",
379
+ "KeyN",
380
+ "n",
381
+ "N",
382
+ "KeyM",
383
+ "m",
384
+ "M",
385
+ "Comma",
386
+ ",",
387
+ "<",
388
+ "Period",
389
+ ".",
390
+ ">",
391
+ "Slash",
392
+ "/",
393
+ "?",
394
+ "ShiftRight",
395
+ "ControlLeft",
396
+ "Control",
397
+ "MetaLeft",
398
+ "Meta",
399
+ "AltLeft",
400
+ "Alt",
401
+ "Space",
402
+ " ",
403
+ "AltRight",
404
+ "AltGraph",
405
+ "MetaRight",
406
+ "ContextMenu",
407
+ "ControlRight",
408
+ "PrintScreen",
409
+ "ScrollLock",
410
+ "Pause",
411
+ "PageUp",
412
+ "PageDown",
413
+ "Insert",
414
+ "Delete",
415
+ "Home",
416
+ "End",
417
+ "ArrowLeft",
418
+ "ArrowUp",
419
+ "ArrowRight",
420
+ "ArrowDown",
421
+ "NumLock",
422
+ "NumpadDivide",
423
+ "NumpadMultiply",
424
+ "NumpadSubtract",
425
+ "Numpad7",
426
+ "Numpad8",
427
+ "Numpad9",
428
+ "Numpad4",
429
+ "Numpad5",
430
+ "Numpad6",
431
+ "NumpadAdd",
432
+ "Numpad1",
433
+ "Numpad2",
434
+ "Numpad3",
435
+ "Numpad0",
436
+ "NumpadDecimal",
437
+ "NumpadEnter",
438
+ "ControlOrMeta"
439
+ ]);
440
+ async function keyboardImplementation(pressed, provider, sessionId, text, selectAll, skipRelease) {
441
+ const page = provider.getPage(sessionId);
442
+ const actions = parseKeyDef(text);
443
+ for (const { releasePrevious, releaseSelf, repeat, keyDef } of actions) {
444
+ const key = keyDef.key;
445
+ // TODO: instead of calling down/up for each key, join non special
446
+ // together, and call `type` once for all non special keys,
447
+ // and then `press` for special keys
448
+ if (pressed.has(key)) {
449
+ if (VALID_KEYS.has(key)) {
450
+ await page.keyboard.up(key);
451
+ }
452
+ pressed.delete(key);
453
+ }
454
+ if (!releasePrevious) {
455
+ if (key === "selectall") {
456
+ await selectAll();
457
+ continue;
458
+ }
459
+ for (let i = 1; i <= repeat; i++) {
460
+ if (VALID_KEYS.has(key)) {
461
+ await page.keyboard.down(key);
462
+ } else {
463
+ await page.keyboard.insertText(key);
464
+ }
465
+ }
466
+ if (releaseSelf) {
467
+ if (VALID_KEYS.has(key)) {
468
+ await page.keyboard.up(key);
469
+ }
470
+ } else {
471
+ pressed.add(key);
472
+ }
473
+ }
474
+ }
475
+ if (!skipRelease && pressed.size) {
476
+ for (const key of pressed) {
477
+ if (VALID_KEYS.has(key)) {
478
+ await page.keyboard.up(key);
479
+ }
480
+ }
481
+ }
482
+ return { pressed };
483
+ }
484
+ function focusIframe() {
485
+ if (!document.activeElement || document.activeElement.ownerDocument !== document || document.activeElement === document.body) {
486
+ window.focus();
487
+ }
488
+ }
489
+ function selectAll() {
490
+ const element = document.activeElement;
491
+ if (element && typeof element.select === "function") {
492
+ element.select();
493
+ }
494
+ }
495
+
496
+ /**
497
+ * Takes a screenshot using the provided browser context and returns a buffer and the expected screenshot path.
498
+ *
499
+ * **Note**: the returned `path` indicates where the screenshot *might* be found.
500
+ * It is not guaranteed to exist, especially if `options.save` is `false`.
501
+ *
502
+ * @throws {Error} If the function is not called within a test or if the browser provider does not support screenshots.
503
+ */
504
+ async function takeScreenshot(context, name, options) {
505
+ if (!context.testPath) {
506
+ throw new Error(`Cannot take a screenshot without a test path`);
507
+ }
508
+ const path = resolveScreenshotPath(context.testPath, name, context.project.config, options.path);
509
+ // playwright does not need a screenshot path if we don't intend to save it
510
+ let savePath;
511
+ if (options.save) {
512
+ savePath = normalize(path);
513
+ await mkdir(dirname(savePath), { recursive: true });
514
+ }
515
+ const mask = options.mask?.map((selector) => context.iframe.locator(selector));
516
+ if (options.element) {
517
+ const { element: selector,...config } = options;
518
+ const element = context.iframe.locator(selector);
519
+ const buffer = await element.screenshot({
520
+ ...config,
521
+ mask,
522
+ path: savePath
523
+ });
524
+ return {
525
+ buffer,
526
+ path
527
+ };
528
+ }
529
+ const buffer = await context.iframe.locator("body").screenshot({
530
+ ...options,
531
+ mask,
532
+ path: savePath
533
+ });
534
+ return {
535
+ buffer,
536
+ path
537
+ };
538
+ }
539
+
540
+ const selectOptions = async (context, selector, userValues, options = {}) => {
541
+ const value = userValues;
542
+ const { iframe } = context;
543
+ const selectElement = iframe.locator(selector);
544
+ const values = await Promise.all(value.map(async (v) => {
545
+ if (typeof v === "string") {
546
+ return v;
547
+ }
548
+ const elementHandler = await iframe.locator(v.element).elementHandle();
549
+ if (!elementHandler) {
550
+ throw new Error(`Element not found: ${v.element}`);
551
+ }
552
+ return elementHandler;
553
+ }));
554
+ await selectElement.selectOption(values, options);
555
+ };
556
+
557
+ const tab = async (context, options = {}) => {
558
+ const page = context.page;
559
+ await page.keyboard.press(options.shift === true ? "Shift+Tab" : "Tab");
560
+ };
561
+
562
+ const startTracing = async ({ context, project, provider, sessionId }) => {
563
+ if (isPlaywrightProvider(provider)) {
564
+ if (provider.tracingContexts.has(sessionId)) {
565
+ return;
566
+ }
567
+ provider.tracingContexts.add(sessionId);
568
+ const options = project.config.browser.trace;
569
+ await context.tracing.start({
570
+ screenshots: options.screenshots ?? true,
571
+ snapshots: options.snapshots ?? true,
572
+ sources: false
573
+ }).catch(() => {
574
+ provider.tracingContexts.delete(sessionId);
575
+ });
576
+ return;
577
+ }
578
+ throw new TypeError(`The ${provider.name} provider does not support tracing.`);
579
+ };
580
+ const startChunkTrace = async (command, { name, title }) => {
581
+ const { provider, sessionId, testPath, context } = command;
582
+ if (!testPath) {
583
+ throw new Error(`stopChunkTrace cannot be called outside of the test file.`);
584
+ }
585
+ if (isPlaywrightProvider(provider)) {
586
+ if (!provider.tracingContexts.has(sessionId)) {
587
+ await startTracing(command);
588
+ }
589
+ const path = resolveTracesPath(command, name);
590
+ provider.pendingTraces.set(path, sessionId);
591
+ await context.tracing.startChunk({
592
+ name,
593
+ title
594
+ });
595
+ return;
596
+ }
597
+ throw new TypeError(`The ${provider.name} provider does not support tracing.`);
598
+ };
599
+ const stopChunkTrace = async (context, { name }) => {
600
+ if (isPlaywrightProvider(context.provider)) {
601
+ const path = resolveTracesPath(context, name);
602
+ context.provider.pendingTraces.delete(path);
603
+ await context.context.tracing.stopChunk({ path });
604
+ return { tracePath: path };
605
+ }
606
+ throw new TypeError(`The ${context.provider.name} provider does not support tracing.`);
607
+ };
608
+ function resolveTracesPath({ testPath, project }, name) {
609
+ if (!testPath) {
610
+ throw new Error(`This command can only be called inside a test file.`);
611
+ }
612
+ const options = project.config.browser.trace;
613
+ const sanitizedName = `${project.name.replace(/[^a-z0-9]/gi, "-")}-${name}.trace.zip`;
614
+ if (options.tracesDir) {
615
+ return resolve(options.tracesDir, sanitizedName);
616
+ }
617
+ const dir = dirname(testPath);
618
+ const base = basename(testPath);
619
+ return resolve(dir, "__traces__", base, `${project.name.replace(/[^a-z0-9]/gi, "-")}-${name}.trace.zip`);
620
+ }
621
+ const deleteTracing = async (context, { traces }) => {
622
+ if (!context.testPath) {
623
+ throw new Error(`stopChunkTrace cannot be called outside of the test file.`);
624
+ }
625
+ if (isPlaywrightProvider(context.provider)) {
626
+ return Promise.all(traces.map((trace) => unlink(trace).catch((err) => {
627
+ if (err.code === "ENOENT") {
628
+ // Ignore the error if the file doesn't exist
629
+ return;
630
+ }
631
+ // Re-throw other errors
632
+ throw err;
633
+ })));
634
+ }
635
+ throw new Error(`provider ${context.provider.name} is not supported`);
636
+ };
637
+ const annotateTraces = async ({ project }, { testId, traces }) => {
638
+ const vitest = project.vitest;
639
+ await Promise.all(traces.map((trace) => {
640
+ const entity = vitest.state.getReportedEntityById(testId);
641
+ return vitest._testRun.annotate(testId, {
642
+ message: relative(project.config.root, trace),
643
+ type: "traces",
644
+ attachment: {
645
+ path: trace,
646
+ contentType: "application/octet-stream"
647
+ },
648
+ location: entity?.location ? {
649
+ file: entity.module.moduleId,
650
+ line: entity.location.line,
651
+ column: entity.location.column
652
+ } : undefined
653
+ });
654
+ }));
655
+ };
656
+ function isPlaywrightProvider(provider) {
657
+ return provider.name === "playwright";
658
+ }
659
+
660
+ const type = async (context, selector, text, options = {}) => {
661
+ const { skipClick = false, skipAutoClose = false } = options;
662
+ const unreleased = new Set(Reflect.get(options, "unreleased") ?? []);
663
+ const { iframe } = context;
664
+ const element = iframe.locator(selector);
665
+ if (!skipClick) {
666
+ await element.focus();
667
+ }
668
+ await keyboardImplementation(unreleased, context.provider, context.sessionId, text, () => element.selectText(), skipAutoClose);
669
+ return { unreleased: Array.from(unreleased) };
670
+ };
671
+
672
+ const upload = async (context, selector, files, options) => {
673
+ const testPath = context.testPath;
674
+ if (!testPath) {
675
+ throw new Error(`Cannot upload files outside of a test`);
676
+ }
677
+ const root = context.project.config.root;
678
+ const { iframe } = context;
679
+ const playwrightFiles = files.map((file) => {
680
+ if (typeof file === "string") {
681
+ return resolve(root, file);
682
+ }
683
+ return {
684
+ name: file.name,
685
+ mimeType: file.mimeType,
686
+ buffer: Buffer.from(file.base64, "base64")
687
+ };
688
+ });
689
+ await iframe.locator(selector).setInputFiles(playwrightFiles, options);
690
+ };
691
+
692
+ var commands = {
693
+ __vitest_upload: upload,
694
+ __vitest_click: click,
695
+ __vitest_dblClick: dblClick,
696
+ __vitest_tripleClick: tripleClick,
697
+ __vitest_takeScreenshot: takeScreenshot,
698
+ __vitest_type: type,
699
+ __vitest_clear: clear,
700
+ __vitest_fill: fill,
701
+ __vitest_tab: tab,
702
+ __vitest_keyboard: keyboard,
703
+ __vitest_selectOptions: selectOptions,
704
+ __vitest_dragAndDrop: dragAndDrop,
705
+ __vitest_hover: hover,
706
+ __vitest_cleanup: keyboardCleanup,
707
+ __vitest_deleteTracing: deleteTracing,
708
+ __vitest_startChunkTrace: startChunkTrace,
709
+ __vitest_startTracing: startTracing,
710
+ __vitest_stopChunkTrace: stopChunkTrace,
711
+ __vitest_annotateTraces: annotateTraces
712
+ };
713
+
714
+ const pkgRoot = resolve(fileURLToPath(import.meta.url), "../..");
715
+ const distRoot = resolve(pkgRoot, "dist");
716
+
717
+ const debug = createDebugger("vitest:browser:playwright");
718
+ const playwrightBrowsers = [
719
+ "firefox",
720
+ "webkit",
721
+ "chromium"
722
+ ];
723
+ function playwright(options = {}) {
724
+ return defineBrowserProvider({
725
+ name: "playwright",
726
+ supportedBrowser: playwrightBrowsers,
727
+ options,
728
+ providerFactory(project) {
729
+ return new PlaywrightBrowserProvider(project, options);
730
+ }
731
+ });
732
+ }
733
+ class PlaywrightBrowserProvider {
734
+ name = "playwright";
735
+ supportsParallelism = true;
736
+ browser = null;
737
+ contexts = new Map();
738
+ pages = new Map();
739
+ mocker;
740
+ browserName;
741
+ browserPromise = null;
742
+ closing = false;
743
+ tracingContexts = new Set();
744
+ pendingTraces = new Map();
745
+ initScripts = [resolve(distRoot, "locators.js")];
746
+ constructor(project, options) {
747
+ this.project = project;
748
+ this.options = options;
749
+ this.browserName = project.config.browser.name;
750
+ this.mocker = this.createMocker();
751
+ for (const [name, command] of Object.entries(commands)) {
752
+ project.browser.registerCommand(name, command);
753
+ }
754
+ // make sure the traces are finished if the test hangs
755
+ process.on("SIGTERM", () => {
756
+ if (!this.browser) {
757
+ return;
758
+ }
759
+ const promises = [];
760
+ for (const [trace, contextId] of this.pendingTraces.entries()) {
761
+ promises.push((() => {
762
+ const context = this.contexts.get(contextId);
763
+ return context?.tracing.stopChunk({ path: trace });
764
+ })());
765
+ }
766
+ return Promise.allSettled(promises);
767
+ });
768
+ }
769
+ async openBrowser() {
770
+ await this._throwIfClosing();
771
+ if (this.browserPromise) {
772
+ debug?.("[%s] the browser is resolving, reusing the promise", this.browserName);
773
+ return this.browserPromise;
774
+ }
775
+ if (this.browser) {
776
+ debug?.("[%s] the browser is resolved, reusing it", this.browserName);
777
+ return this.browser;
778
+ }
779
+ this.browserPromise = (async () => {
780
+ const options = this.project.config.browser;
781
+ const playwright = await import('playwright');
782
+ if (this.options.connectOptions) {
783
+ if (this.options.launchOptions) {
784
+ this.project.vitest.logger.warn(c.yellow(`Found both ${c.bold(c.italic(c.yellow("connect")))} and ${c.bold(c.italic(c.yellow("launch")))} options in browser instance configuration.
785
+ Ignoring ${c.bold(c.italic(c.yellow("launch")))} options and using ${c.bold(c.italic(c.yellow("connect")))} mode.
786
+ You probably want to remove one of the two options and keep only the one you want to use.`));
787
+ }
788
+ const browser = await playwright[this.browserName].connect(this.options.connectOptions.wsEndpoint, this.options.connectOptions);
789
+ this.browser = browser;
790
+ this.browserPromise = null;
791
+ return this.browser;
792
+ }
793
+ const launchOptions = {
794
+ ...this.options.launchOptions,
795
+ headless: options.headless
796
+ };
797
+ if (typeof options.trace === "object" && options.trace.tracesDir) {
798
+ launchOptions.tracesDir = options.trace?.tracesDir;
799
+ }
800
+ const inspector = this.project.vitest.config.inspector;
801
+ if (inspector.enabled) {
802
+ // NodeJS equivalent defaults: https://nodejs.org/en/learn/getting-started/debugging#enable-inspector
803
+ const port = inspector.port || 9229;
804
+ const host = inspector.host || "127.0.0.1";
805
+ launchOptions.args ||= [];
806
+ launchOptions.args.push(`--remote-debugging-port=${port}`);
807
+ launchOptions.args.push(`--remote-debugging-address=${host}`);
808
+ this.project.vitest.logger.log(`Debugger listening on ws://${host}:${port}`);
809
+ }
810
+ // start Vitest UI maximized only on supported browsers
811
+ if (this.project.config.browser.ui && this.browserName === "chromium") {
812
+ if (!launchOptions.args) {
813
+ launchOptions.args = [];
814
+ }
815
+ if (!launchOptions.args.includes("--start-maximized") && !launchOptions.args.includes("--start-fullscreen")) {
816
+ launchOptions.args.push("--start-maximized");
817
+ }
818
+ }
819
+ debug?.("[%s] initializing the browser with launch options: %O", this.browserName, launchOptions);
820
+ this.browser = await playwright[this.browserName].launch(launchOptions);
821
+ this.browserPromise = null;
822
+ return this.browser;
823
+ })();
824
+ return this.browserPromise;
825
+ }
826
+ createMocker() {
827
+ const idPreficates = new Map();
828
+ const sessionIds = new Map();
829
+ function createPredicate(sessionId, url) {
830
+ const moduleUrl = new URL(url, "http://localhost");
831
+ const predicate = (url) => {
832
+ if (url.searchParams.has("_vitest_original")) {
833
+ return false;
834
+ }
835
+ // different modules, ignore request
836
+ if (url.pathname !== moduleUrl.pathname) {
837
+ return false;
838
+ }
839
+ url.searchParams.delete("t");
840
+ url.searchParams.delete("v");
841
+ url.searchParams.delete("import");
842
+ // different search params, ignore request
843
+ if (url.searchParams.size !== moduleUrl.searchParams.size) {
844
+ return false;
845
+ }
846
+ // check that all search params are the same
847
+ for (const [param, value] of url.searchParams.entries()) {
848
+ if (moduleUrl.searchParams.get(param) !== value) {
849
+ return false;
850
+ }
851
+ }
852
+ return true;
853
+ };
854
+ const ids = sessionIds.get(sessionId) || [];
855
+ ids.push(moduleUrl.href);
856
+ sessionIds.set(sessionId, ids);
857
+ idPreficates.set(predicateKey(sessionId, moduleUrl.href), predicate);
858
+ return predicate;
859
+ }
860
+ function predicateKey(sessionId, url) {
861
+ return `${sessionId}:${url}`;
862
+ }
863
+ return {
864
+ register: async (sessionId, module) => {
865
+ const page = this.getPage(sessionId);
866
+ await page.route(createPredicate(sessionId, module.url), async (route) => {
867
+ if (module.type === "manual") {
868
+ const exports = Object.keys(await module.resolve());
869
+ const body = createManualModuleSource(module.url, exports);
870
+ return route.fulfill({
871
+ body,
872
+ headers: getHeaders(this.project.browser.vite.config)
873
+ });
874
+ }
875
+ // webkit doesn't support redirect responses
876
+ // https://github.com/microsoft/playwright/issues/18318
877
+ const isWebkit = this.browserName === "webkit";
878
+ if (isWebkit) {
879
+ let url;
880
+ if (module.type === "redirect") {
881
+ const redirect = new URL(module.redirect);
882
+ url = redirect.href.slice(redirect.origin.length);
883
+ } else {
884
+ const request = new URL(route.request().url());
885
+ request.searchParams.set("mock", module.type);
886
+ url = request.href.slice(request.origin.length);
887
+ }
888
+ const result = await this.project.browser.vite.transformRequest(url).catch(() => null);
889
+ if (!result) {
890
+ return route.continue();
891
+ }
892
+ let content = result.code;
893
+ if (result.map && "version" in result.map && result.map.mappings) {
894
+ const type = isDirectCSSRequest(url) ? "css" : "js";
895
+ content = getCodeWithSourcemap(type, content.toString(), result.map);
896
+ }
897
+ return route.fulfill({
898
+ body: content,
899
+ headers: getHeaders(this.project.browser.vite.config)
900
+ });
901
+ }
902
+ if (module.type === "redirect") {
903
+ return route.fulfill({
904
+ status: 302,
905
+ headers: { Location: module.redirect }
906
+ });
907
+ } else if (module.type === "automock" || module.type === "autospy") {
908
+ const url = new URL(route.request().url());
909
+ url.searchParams.set("mock", module.type);
910
+ return route.fulfill({
911
+ status: 302,
912
+ headers: { Location: url.href }
913
+ });
914
+ } else ;
915
+ });
916
+ },
917
+ delete: async (sessionId, id) => {
918
+ const page = this.getPage(sessionId);
919
+ const key = predicateKey(sessionId, id);
920
+ const predicate = idPreficates.get(key);
921
+ if (predicate) {
922
+ await page.unroute(predicate).finally(() => idPreficates.delete(key));
923
+ }
924
+ },
925
+ clear: async (sessionId) => {
926
+ const page = this.getPage(sessionId);
927
+ const ids = sessionIds.get(sessionId) || [];
928
+ const promises = ids.map((id) => {
929
+ const key = predicateKey(sessionId, id);
930
+ const predicate = idPreficates.get(key);
931
+ if (predicate) {
932
+ return page.unroute(predicate).finally(() => idPreficates.delete(key));
933
+ }
934
+ return null;
935
+ });
936
+ await Promise.all(promises).finally(() => sessionIds.delete(sessionId));
937
+ }
938
+ };
939
+ }
940
+ async createContext(sessionId) {
941
+ await this._throwIfClosing();
942
+ if (this.contexts.has(sessionId)) {
943
+ debug?.("[%s][%s] the context already exists, reusing it", sessionId, this.browserName);
944
+ return this.contexts.get(sessionId);
945
+ }
946
+ const browser = await this.openBrowser();
947
+ await this._throwIfClosing(browser);
948
+ const actionTimeout = this.options.actionTimeout;
949
+ const contextOptions = this.options.contextOptions ?? {};
950
+ const options = {
951
+ ...contextOptions,
952
+ ignoreHTTPSErrors: true
953
+ };
954
+ if (this.project.config.browser.ui) {
955
+ options.viewport = null;
956
+ }
957
+ const context = await browser.newContext(options);
958
+ await this._throwIfClosing(context);
959
+ if (actionTimeout != null) {
960
+ context.setDefaultTimeout(actionTimeout);
961
+ }
962
+ debug?.("[%s][%s] the context is ready", sessionId, this.browserName);
963
+ this.contexts.set(sessionId, context);
964
+ return context;
965
+ }
966
+ getPage(sessionId) {
967
+ const page = this.pages.get(sessionId);
968
+ if (!page) {
969
+ throw new Error(`Page "${sessionId}" not found in ${this.browserName} browser.`);
970
+ }
971
+ return page;
972
+ }
973
+ getCommandsContext(sessionId) {
974
+ const page = this.getPage(sessionId);
975
+ return {
976
+ page,
977
+ context: this.contexts.get(sessionId),
978
+ frame() {
979
+ return new Promise((resolve, reject) => {
980
+ const frame = page.frame("vitest-iframe");
981
+ if (frame) {
982
+ return resolve(frame);
983
+ }
984
+ const timeout = setTimeout(() => {
985
+ const err = new Error(`Cannot find "vitest-iframe" on the page. This is a bug in Vitest, please report it.`);
986
+ reject(err);
987
+ }, 1e3).unref();
988
+ page.on("frameattached", (frame) => {
989
+ clearTimeout(timeout);
990
+ resolve(frame);
991
+ });
992
+ });
993
+ },
994
+ get iframe() {
995
+ return page.frameLocator("[data-vitest=\"true\"]");
996
+ }
997
+ };
998
+ }
999
+ async openBrowserPage(sessionId) {
1000
+ await this._throwIfClosing();
1001
+ if (this.pages.has(sessionId)) {
1002
+ debug?.("[%s][%s] the page already exists, closing the old one", sessionId, this.browserName);
1003
+ const page = this.pages.get(sessionId);
1004
+ await page.close();
1005
+ this.pages.delete(sessionId);
1006
+ }
1007
+ const context = await this.createContext(sessionId);
1008
+ const page = await context.newPage();
1009
+ debug?.("[%s][%s] the page is ready", sessionId, this.browserName);
1010
+ await this._throwIfClosing(page);
1011
+ this.pages.set(sessionId, page);
1012
+ if (process.env.VITEST_PW_DEBUG) {
1013
+ page.on("requestfailed", (request) => {
1014
+ console.error("[PW Error]", request.resourceType(), "request failed for", request.url(), "url:", request.failure()?.errorText);
1015
+ });
1016
+ }
1017
+ return page;
1018
+ }
1019
+ async openPage(sessionId, url) {
1020
+ debug?.("[%s][%s] creating the browser page for %s", sessionId, this.browserName, url);
1021
+ const browserPage = await this.openBrowserPage(sessionId);
1022
+ debug?.("[%s][%s] browser page is created, opening %s", sessionId, this.browserName, url);
1023
+ await browserPage.goto(url, { timeout: 0 });
1024
+ await this._throwIfClosing(browserPage);
1025
+ }
1026
+ async _throwIfClosing(disposable) {
1027
+ if (this.closing) {
1028
+ debug?.("[%s] provider was closed, cannot perform the action on %s", this.browserName, String(disposable));
1029
+ await disposable?.close();
1030
+ this.pages.clear();
1031
+ this.contexts.clear();
1032
+ this.browser = null;
1033
+ this.browserPromise = null;
1034
+ throw new Error(`[vitest] The provider was closed.`);
1035
+ }
1036
+ }
1037
+ async getCDPSession(sessionid) {
1038
+ const page = this.getPage(sessionid);
1039
+ const cdp = await page.context().newCDPSession(page);
1040
+ return {
1041
+ async send(method, params) {
1042
+ const result = await cdp.send(method, params);
1043
+ return result;
1044
+ },
1045
+ on(event, listener) {
1046
+ cdp.on(event, listener);
1047
+ },
1048
+ off(event, listener) {
1049
+ cdp.off(event, listener);
1050
+ },
1051
+ once(event, listener) {
1052
+ cdp.once(event, listener);
1053
+ }
1054
+ };
1055
+ }
1056
+ async close() {
1057
+ debug?.("[%s] closing provider", this.browserName);
1058
+ this.closing = true;
1059
+ if (this.browserPromise) {
1060
+ await this.browserPromise;
1061
+ this.browserPromise = null;
1062
+ }
1063
+ const browser = this.browser;
1064
+ this.browser = null;
1065
+ await Promise.all([...this.pages.values()].map((p) => p.close()));
1066
+ this.pages.clear();
1067
+ await Promise.all([...this.contexts.values()].map((c) => c.close()));
1068
+ this.contexts.clear();
1069
+ await browser?.close();
1070
+ debug?.("[%s] provider is closed", this.browserName);
1071
+ }
1072
+ }
1073
+ function getHeaders(config) {
1074
+ const headers = { "Content-Type": "application/javascript" };
1075
+ for (const name in config.server.headers) {
1076
+ headers[name] = String(config.server.headers[name]);
1077
+ }
1078
+ return headers;
1079
+ }
1080
+ function getCodeWithSourcemap(type, code, map) {
1081
+ if (type === "js") {
1082
+ code += `\n//# sourceMappingURL=${genSourceMapUrl(map)}`;
1083
+ } else if (type === "css") {
1084
+ code += `\n/*# sourceMappingURL=${genSourceMapUrl(map)} */`;
1085
+ }
1086
+ return code;
1087
+ }
1088
+ function genSourceMapUrl(map) {
1089
+ if (typeof map !== "string") {
1090
+ map = JSON.stringify(map);
1091
+ }
1092
+ return `data:application/json;base64,${Buffer.from(map).toString("base64")}`;
1093
+ }
1094
+ const directRequestRE = /[?&]direct\b/;
1095
+ function isDirectCSSRequest(request) {
1096
+ return isCSSRequest(request) && directRequestRE.test(request);
1097
+ }
1098
+
1099
+ export { PlaywrightBrowserProvider, playwright };