@vitest/browser 2.0.0-beta.9 → 2.0.1

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 CHANGED
@@ -1,14 +1,20 @@
1
+ import { createDebugger, isFileServingAllowed as isFileServingAllowed$1, getFilePoolName, resolveApiServerConfig, resolveFsAllow, distDir, createServer } from 'vitest/node';
2
+ import fs, { existsSync, readdirSync, readFileSync, promises } from 'node:fs';
3
+ import { WebSocketServer } from 'ws';
4
+ import { isFileServingAllowed } from 'vite';
5
+ import { builtinModules, createRequire } from 'node:module';
6
+ import { readFile as readFile$1, mkdir } from 'node:fs/promises';
1
7
  import { fileURLToPath } from 'node:url';
2
- import { readFile as readFile$1 } from 'node:fs/promises';
3
- import { createRequire } from 'node:module';
8
+ import { createDefer, slash, toArray } from '@vitest/utils';
9
+ import { parseErrorStacktrace, parseStacktrace } from '@vitest/utils/source-map';
4
10
  import sirv from 'sirv';
5
- import { isFileServingAllowed, getFilePoolName, distDir } from 'vitest/node';
6
- import { coverageConfigDefaults } from 'vitest/config';
7
- import { slash, toArray } from '@vitest/utils';
8
- import fs, { promises } from 'node:fs';
9
- import { resolve as resolve$1, dirname as dirname$1 } from 'node:path';
11
+ import { defaultBrowserPort, coverageConfigDefaults } from 'vitest/config';
12
+ import { P as PlaywrightBrowserProvider, W as WebdriverBrowserProvider } from './webdriver-BdVqnfdE.js';
13
+ import { resolve as resolve$1, dirname as dirname$1, normalize as normalize$1 } from 'node:path';
10
14
  import MagicString from 'magic-string';
11
15
  import { esmWalker } from '@vitest/utils/ast';
16
+ import * as nodeos from 'node:os';
17
+ import crypto from 'node:crypto';
12
18
 
13
19
  const _DRIVE_LETTER_START_RE = /^[A-Za-z]:\//;
14
20
  function normalizeWindowsPath(input = "") {
@@ -21,6 +27,7 @@ function normalizeWindowsPath(input = "") {
21
27
  const _UNC_REGEX = /^[/\\]{2}/;
22
28
  const _IS_ABSOLUTE_RE = /^[/\\](?![/\\])|^[/\\]{2}(?!\.)|^[A-Za-z]:[/\\]/;
23
29
  const _DRIVE_LETTER_RE = /^[A-Za-z]:$/;
30
+ const _ROOT_FOLDER_RE = /^\/([A-Za-z]:)?$/;
24
31
  const normalize = function(path) {
25
32
  if (path.length === 0) {
26
33
  return ".";
@@ -155,52 +162,1527 @@ function normalizeString(path, allowAboveRoot) {
155
162
  const isAbsolute = function(p) {
156
163
  return _IS_ABSOLUTE_RE.test(p);
157
164
  };
165
+ const _EXTNAME_RE = /.(\.[^./]+)$/;
166
+ const extname = function(p) {
167
+ const match = _EXTNAME_RE.exec(normalizeWindowsPath(p));
168
+ return match && match[1] || "";
169
+ };
170
+ const relative = function(from, to) {
171
+ const _from = resolve(from).replace(_ROOT_FOLDER_RE, "$1").split("/");
172
+ const _to = resolve(to).replace(_ROOT_FOLDER_RE, "$1").split("/");
173
+ if (_to[0][1] === ":" && _from[0][1] === ":" && _from[0] !== _to[0]) {
174
+ return _to.join("/");
175
+ }
176
+ const _fromCopy = [..._from];
177
+ for (const segment of _fromCopy) {
178
+ if (_to[0] !== segment) {
179
+ break;
180
+ }
181
+ _from.shift();
182
+ _to.shift();
183
+ }
184
+ return [..._from.map(() => ".."), ..._to].join("/");
185
+ };
158
186
  const dirname = function(p) {
159
187
  const segments = normalizeWindowsPath(p).replace(/\/$/, "").split("/").slice(0, -1);
160
188
  if (segments.length === 1 && _DRIVE_LETTER_RE.test(segments[0])) {
161
189
  segments[0] += "/";
162
190
  }
163
- return segments.join("/") || (isAbsolute(p) ? "/" : ".");
191
+ return segments.join("/") || (isAbsolute(p) ? "/" : ".");
192
+ };
193
+ const basename = function(p, extension) {
194
+ const lastSegment = normalizeWindowsPath(p).split("/").pop();
195
+ return extension && lastSegment.endsWith(extension) ? lastSegment.slice(0, -extension.length) : lastSegment;
196
+ };
197
+
198
+ const DEFAULT_TIMEOUT = 6e4;
199
+ function defaultSerialize(i) {
200
+ return i;
201
+ }
202
+ const defaultDeserialize = defaultSerialize;
203
+ const { clearTimeout, setTimeout } = globalThis;
204
+ const random = Math.random.bind(Math);
205
+ function createBirpc(functions, options) {
206
+ const {
207
+ post,
208
+ on,
209
+ eventNames = [],
210
+ serialize = defaultSerialize,
211
+ deserialize = defaultDeserialize,
212
+ resolver,
213
+ timeout = DEFAULT_TIMEOUT
214
+ } = options;
215
+ const rpcPromiseMap = /* @__PURE__ */ new Map();
216
+ let _promise;
217
+ const rpc = new Proxy({}, {
218
+ get(_, method) {
219
+ if (method === "$functions")
220
+ return functions;
221
+ if (method === "then" && !eventNames.includes("then") && !("then" in functions))
222
+ return void 0;
223
+ const sendEvent = (...args) => {
224
+ post(serialize({ m: method, a: args, t: "q" }));
225
+ };
226
+ if (eventNames.includes(method)) {
227
+ sendEvent.asEvent = sendEvent;
228
+ return sendEvent;
229
+ }
230
+ const sendCall = async (...args) => {
231
+ await _promise;
232
+ return new Promise((resolve, reject) => {
233
+ const id = nanoid();
234
+ let timeoutId;
235
+ if (timeout >= 0) {
236
+ timeoutId = setTimeout(() => {
237
+ try {
238
+ options.onTimeoutError?.(method, args);
239
+ throw new Error(`[birpc] timeout on calling "${method}"`);
240
+ } catch (e) {
241
+ reject(e);
242
+ }
243
+ rpcPromiseMap.delete(id);
244
+ }, timeout);
245
+ if (typeof timeoutId === "object")
246
+ timeoutId = timeoutId.unref?.();
247
+ }
248
+ rpcPromiseMap.set(id, { resolve, reject, timeoutId });
249
+ post(serialize({ m: method, a: args, i: id, t: "q" }));
250
+ });
251
+ };
252
+ sendCall.asEvent = sendEvent;
253
+ return sendCall;
254
+ }
255
+ });
256
+ _promise = on(async (data, ...extra) => {
257
+ const msg = deserialize(data);
258
+ if (msg.t === "q") {
259
+ const { m: method, a: args } = msg;
260
+ let result, error;
261
+ const fn = resolver ? resolver(method, functions[method]) : functions[method];
262
+ if (!fn) {
263
+ error = new Error(`[birpc] function "${method}" not found`);
264
+ } else {
265
+ try {
266
+ result = await fn.apply(rpc, args);
267
+ } catch (e) {
268
+ error = e;
269
+ }
270
+ }
271
+ if (msg.i) {
272
+ if (error && options.onError)
273
+ options.onError(error, method, args);
274
+ post(serialize({ t: "s", i: msg.i, r: result, e: error }), ...extra);
275
+ }
276
+ } else {
277
+ const { i: ack, r: result, e: error } = msg;
278
+ const promise = rpcPromiseMap.get(ack);
279
+ if (promise) {
280
+ clearTimeout(promise.timeoutId);
281
+ if (error)
282
+ promise.reject(error);
283
+ else
284
+ promise.resolve(result);
285
+ }
286
+ rpcPromiseMap.delete(ack);
287
+ }
288
+ });
289
+ return rpc;
290
+ }
291
+ const urlAlphabet = "useandom-26T198340PX75pxJACKVERYMINDBUSHWOLF_GQZbfghjklqvwyzrict";
292
+ function nanoid(size = 21) {
293
+ let id = "";
294
+ let i = size;
295
+ while (i--)
296
+ id += urlAlphabet[random() * 64 | 0];
297
+ return id;
298
+ }
299
+
300
+ /// <reference types="../types/index.d.ts" />
301
+
302
+ // (c) 2020-present Andrea Giammarchi
303
+
304
+ const {parse: $parse, stringify: $stringify} = JSON;
305
+ const {keys} = Object;
306
+
307
+ const Primitive = String; // it could be Number
308
+ const primitive = 'string'; // it could be 'number'
309
+
310
+ const ignore = {};
311
+ const object = 'object';
312
+
313
+ const noop = (_, value) => value;
314
+
315
+ const primitives = value => (
316
+ value instanceof Primitive ? Primitive(value) : value
317
+ );
318
+
319
+ const Primitives = (_, value) => (
320
+ typeof value === primitive ? new Primitive(value) : value
321
+ );
322
+
323
+ const revive = (input, parsed, output, $) => {
324
+ const lazy = [];
325
+ for (let ke = keys(output), {length} = ke, y = 0; y < length; y++) {
326
+ const k = ke[y];
327
+ const value = output[k];
328
+ if (value instanceof Primitive) {
329
+ const tmp = input[value];
330
+ if (typeof tmp === object && !parsed.has(tmp)) {
331
+ parsed.add(tmp);
332
+ output[k] = ignore;
333
+ lazy.push({k, a: [input, parsed, tmp, $]});
334
+ }
335
+ else
336
+ output[k] = $.call(output, k, tmp);
337
+ }
338
+ else if (output[k] !== ignore)
339
+ output[k] = $.call(output, k, value);
340
+ }
341
+ for (let {length} = lazy, i = 0; i < length; i++) {
342
+ const {k, a} = lazy[i];
343
+ output[k] = $.call(output, k, revive.apply(null, a));
344
+ }
345
+ return output;
346
+ };
347
+
348
+ const set = (known, input, value) => {
349
+ const index = Primitive(input.push(value) - 1);
350
+ known.set(value, index);
351
+ return index;
352
+ };
353
+
354
+ /**
355
+ * Converts a specialized flatted string into a JS value.
356
+ * @param {string} text
357
+ * @param {(this: any, key: string, value: any) => any} [reviver]
358
+ * @returns {any}
359
+ */
360
+ const parse = (text, reviver) => {
361
+ const input = $parse(text, Primitives).map(primitives);
362
+ const value = input[0];
363
+ const $ = reviver || noop;
364
+ const tmp = typeof value === object && value ?
365
+ revive(input, new Set, value, $) :
366
+ value;
367
+ return $.call({'': tmp}, '', tmp);
368
+ };
369
+
370
+ /**
371
+ * Converts a JS value into a specialized flatted string.
372
+ * @param {any} value
373
+ * @param {((this: any, key: string, value: any) => any) | (string | number)[] | null | undefined} [replacer]
374
+ * @param {string | number | undefined} [space]
375
+ * @returns {string}
376
+ */
377
+ const stringify = (value, replacer, space) => {
378
+ const $ = replacer && typeof replacer === object ?
379
+ (k, v) => (k === '' || -1 < replacer.indexOf(k) ? v : void 0) :
380
+ (replacer || noop);
381
+ const known = new Map;
382
+ const input = [];
383
+ const output = [];
384
+ let i = +set(known, input, $.call({'': value}, '', value));
385
+ let firstRun = !i;
386
+ while (i < input.length) {
387
+ firstRun = true;
388
+ output[i] = $stringify(input[i++], replace, space);
389
+ }
390
+ return '[' + output.join(',') + ']';
391
+ function replace(key, value) {
392
+ if (firstRun) {
393
+ firstRun = !firstRun;
394
+ return value;
395
+ }
396
+ const after = $.call(this, key, value);
397
+ switch (typeof after) {
398
+ case object:
399
+ if (after === null) return after;
400
+ case primitive:
401
+ return known.get(after) || set(known, input, after);
402
+ }
403
+ return after;
404
+ }
405
+ };
406
+
407
+ async function resolveMock(project, rawId, importer, hasFactory) {
408
+ const { id, fsPath, external } = await resolveId(project, rawId, importer);
409
+ if (hasFactory) {
410
+ const needsInteropMap = viteDepsInteropMap(project.browser.vite.config);
411
+ const needsInterop = needsInteropMap?.get(fsPath) ?? false;
412
+ return { type: "factory", resolvedId: id, needsInterop };
413
+ }
414
+ const mockPath = resolveMockPath(project.config.root, fsPath, external);
415
+ return {
416
+ type: mockPath === null ? "automock" : "redirect",
417
+ mockPath,
418
+ resolvedId: id
419
+ };
420
+ }
421
+ async function resolveId(project, rawId, importer) {
422
+ const resolved = await project.browser.vite.pluginContainer.resolveId(
423
+ rawId,
424
+ importer,
425
+ {
426
+ ssr: false
427
+ }
428
+ );
429
+ return resolveModule(project, rawId, resolved);
430
+ }
431
+ async function resolveModule(project, rawId, resolved) {
432
+ const id = resolved?.id || rawId;
433
+ const external = !isAbsolute(id) || isModuleDirectory(project.config, id) ? rawId : null;
434
+ return {
435
+ id,
436
+ fsPath: cleanUrl(id),
437
+ external
438
+ };
439
+ }
440
+ function isModuleDirectory(config, path) {
441
+ const moduleDirectories = config.server.deps?.moduleDirectories || [
442
+ "/node_modules/"
443
+ ];
444
+ return moduleDirectories.some((dir) => path.includes(dir));
445
+ }
446
+ function resolveMockPath(root, mockPath, external) {
447
+ const path = external || mockPath;
448
+ if (external || isNodeBuiltin(mockPath) || !existsSync(mockPath)) {
449
+ const mockDirname = dirname(path);
450
+ const mockFolder = join(
451
+ root,
452
+ "__mocks__",
453
+ mockDirname
454
+ );
455
+ if (!existsSync(mockFolder)) {
456
+ return null;
457
+ }
458
+ const files = readdirSync(mockFolder);
459
+ const baseOriginal = basename(path);
460
+ for (const file of files) {
461
+ const baseFile = basename(file, extname(file));
462
+ if (baseFile === baseOriginal) {
463
+ return resolve(mockFolder, file);
464
+ }
465
+ }
466
+ return null;
467
+ }
468
+ const dir = dirname(path);
469
+ const baseId = basename(path);
470
+ const fullPath = resolve(dir, "__mocks__", baseId);
471
+ return existsSync(fullPath) ? fullPath : null;
472
+ }
473
+ const prefixedBuiltins = /* @__PURE__ */ new Set(["node:test"]);
474
+ const builtins = /* @__PURE__ */ new Set([
475
+ ...builtinModules,
476
+ "assert/strict",
477
+ "diagnostics_channel",
478
+ "dns/promises",
479
+ "fs/promises",
480
+ "path/posix",
481
+ "path/win32",
482
+ "readline/promises",
483
+ "stream/consumers",
484
+ "stream/promises",
485
+ "stream/web",
486
+ "timers/promises",
487
+ "util/types",
488
+ "wasi"
489
+ ]);
490
+ const NODE_BUILTIN_NAMESPACE = "node:";
491
+ function isNodeBuiltin(id) {
492
+ if (prefixedBuiltins.has(id)) {
493
+ return true;
494
+ }
495
+ return builtins.has(
496
+ id.startsWith(NODE_BUILTIN_NAMESPACE) ? id.slice(NODE_BUILTIN_NAMESPACE.length) : id
497
+ );
498
+ }
499
+ const postfixRE = /[?#].*$/;
500
+ function cleanUrl(url) {
501
+ return url.replace(postfixRE, "");
502
+ }
503
+ const metadata = /* @__PURE__ */ new WeakMap();
504
+ function viteDepsInteropMap(config) {
505
+ if (metadata.has(config)) {
506
+ return metadata.get(config);
507
+ }
508
+ const cacheDirPath = getDepsCacheDir(config);
509
+ const metadataPath = resolve(cacheDirPath, "_metadata.json");
510
+ if (!existsSync(metadataPath)) {
511
+ return null;
512
+ }
513
+ const { optimized } = JSON.parse(readFileSync(metadataPath, "utf-8"));
514
+ const needsInteropMap = /* @__PURE__ */ new Map();
515
+ for (const name in optimized) {
516
+ const dep = optimized[name];
517
+ const file = resolve(cacheDirPath, dep.file);
518
+ needsInteropMap.set(file, dep.needsInterop);
519
+ }
520
+ metadata.set(config, needsInteropMap);
521
+ return needsInteropMap;
522
+ }
523
+ function getDepsCacheDir(config) {
524
+ return resolve(config.cacheDir, "deps");
525
+ }
526
+
527
+ const debug$1 = createDebugger("vitest:browser:api");
528
+ const BROWSER_API_PATH = "/__vitest_browser_api__";
529
+ function setupBrowserRpc(server) {
530
+ const project = server.project;
531
+ const vite = server.vite;
532
+ const ctx = project.ctx;
533
+ const wss = new WebSocketServer({ noServer: true });
534
+ vite.httpServer?.on("upgrade", (request, socket, head) => {
535
+ if (!request.url) {
536
+ return;
537
+ }
538
+ const { pathname, searchParams } = new URL(request.url, "http://localhost");
539
+ if (pathname !== BROWSER_API_PATH) {
540
+ return;
541
+ }
542
+ const type = searchParams.get("type") ?? "tester";
543
+ const sessionId = searchParams.get("sessionId") ?? "0";
544
+ wss.handleUpgrade(request, socket, head, (ws) => {
545
+ wss.emit("connection", ws, request);
546
+ const rpc = setupClient(sessionId, ws);
547
+ const state = server.state;
548
+ const clients = type === "tester" ? state.testers : state.orchestrators;
549
+ clients.set(sessionId, rpc);
550
+ debug$1?.("[%s] Browser API connected to %s", sessionId, type);
551
+ ws.on("close", () => {
552
+ debug$1?.("[%s] Browser API disconnected from %s", sessionId, type);
553
+ clients.delete(sessionId);
554
+ server.state.removeCDPHandler(sessionId);
555
+ });
556
+ });
557
+ });
558
+ function checkFileAccess(path) {
559
+ if (!isFileServingAllowed(path, vite)) {
560
+ throw new Error(
561
+ `Access denied to "${path}". See Vite config documentation for "server.fs": https://vitejs.dev/config/server-options.html#server-fs-strict.`
562
+ );
563
+ }
564
+ }
565
+ function setupClient(sessionId, ws) {
566
+ const rpc = createBirpc(
567
+ {
568
+ async onUnhandledError(error, type) {
569
+ ctx.state.catchError(error, type);
570
+ },
571
+ async onCollected(files) {
572
+ ctx.state.collectFiles(files);
573
+ await ctx.report("onCollected", files);
574
+ },
575
+ async onTaskUpdate(packs) {
576
+ ctx.state.updateTasks(packs);
577
+ await ctx.report("onTaskUpdate", packs);
578
+ },
579
+ onAfterSuiteRun(meta) {
580
+ ctx.coverageProvider?.onAfterSuiteRun(meta);
581
+ },
582
+ sendLog(log) {
583
+ return ctx.report("onUserConsoleLog", log);
584
+ },
585
+ resolveSnapshotPath(testPath) {
586
+ return ctx.snapshot.resolvePath(testPath);
587
+ },
588
+ resolveSnapshotRawPath(testPath, rawPath) {
589
+ return ctx.snapshot.resolveRawPath(testPath, rawPath);
590
+ },
591
+ snapshotSaved(snapshot) {
592
+ ctx.snapshot.add(snapshot);
593
+ },
594
+ async readSnapshotFile(snapshotPath) {
595
+ checkFileAccess(snapshotPath);
596
+ if (!existsSync(snapshotPath)) {
597
+ return null;
598
+ }
599
+ return promises.readFile(snapshotPath, "utf-8");
600
+ },
601
+ async saveSnapshotFile(id, content) {
602
+ checkFileAccess(id);
603
+ await promises.mkdir(dirname(id), { recursive: true });
604
+ return promises.writeFile(id, content, "utf-8");
605
+ },
606
+ async removeSnapshotFile(id) {
607
+ checkFileAccess(id);
608
+ if (!existsSync(id)) {
609
+ throw new Error(`Snapshot file "${id}" does not exist.`);
610
+ }
611
+ return promises.unlink(id);
612
+ },
613
+ getBrowserFileSourceMap(id) {
614
+ const mod = server.vite.moduleGraph.getModuleById(id);
615
+ return mod?.transformResult?.map;
616
+ },
617
+ onCancel(reason) {
618
+ ctx.cancelCurrentRun(reason);
619
+ },
620
+ async resolveId(id, importer) {
621
+ const result = await project.server.pluginContainer.resolveId(
622
+ id,
623
+ importer,
624
+ {
625
+ ssr: false
626
+ }
627
+ );
628
+ return result;
629
+ },
630
+ debug(...args) {
631
+ ctx.logger.console.debug(...args);
632
+ },
633
+ getCountOfFailedTests() {
634
+ return ctx.state.getCountOfFailedTests();
635
+ },
636
+ async triggerCommand(contextId, command, testPath, payload) {
637
+ debug$1?.('[%s] Triggering command "%s"', contextId, command);
638
+ const provider = server.provider;
639
+ if (!provider) {
640
+ throw new Error("Commands are only available for browser tests.");
641
+ }
642
+ const commands = project.config.browser?.commands;
643
+ if (!commands || !commands[command]) {
644
+ throw new Error(`Unknown command "${command}".`);
645
+ }
646
+ if (provider.beforeCommand) {
647
+ await provider.beforeCommand(command, payload);
648
+ }
649
+ const context = Object.assign(
650
+ {
651
+ testPath,
652
+ project,
653
+ provider,
654
+ contextId
655
+ },
656
+ provider.getCommandsContext(contextId)
657
+ );
658
+ let result;
659
+ try {
660
+ result = await commands[command](context, ...payload);
661
+ } finally {
662
+ if (provider.afterCommand) {
663
+ await provider.afterCommand(command, payload);
664
+ }
665
+ }
666
+ return result;
667
+ },
668
+ finishBrowserTests(contextId) {
669
+ debug$1?.("[%s] Finishing browser tests for context", contextId);
670
+ return server.state.getContext(contextId)?.resolve();
671
+ },
672
+ resolveMock(rawId, importer, hasFactory) {
673
+ return resolveMock(project, rawId, importer, hasFactory);
674
+ },
675
+ invalidate(ids) {
676
+ ids.forEach((id) => {
677
+ const moduleGraph = server.vite.moduleGraph;
678
+ const module = moduleGraph.getModuleById(id);
679
+ if (module) {
680
+ moduleGraph.invalidateModule(module, /* @__PURE__ */ new Set(), Date.now(), true);
681
+ }
682
+ });
683
+ },
684
+ // CDP
685
+ async sendCdpEvent(contextId, event, payload) {
686
+ const cdp = await server.ensureCDPHandler(contextId, sessionId);
687
+ return cdp.send(event, payload);
688
+ },
689
+ async trackCdpEvent(contextId, type, event, listenerId) {
690
+ const cdp = await server.ensureCDPHandler(contextId, sessionId);
691
+ cdp[type](event, listenerId);
692
+ }
693
+ },
694
+ {
695
+ post: (msg) => ws.send(msg),
696
+ on: (fn) => ws.on("message", fn),
697
+ eventNames: ["onCancel", "cdpEvent"],
698
+ serialize: (data) => stringify(data, stringifyReplace),
699
+ deserialize: parse,
700
+ onTimeoutError(functionName) {
701
+ throw new Error(`[vitest-api]: Timeout calling "${functionName}"`);
702
+ }
703
+ }
704
+ );
705
+ ctx.onCancel((reason) => rpc.onCancel(reason));
706
+ return rpc;
707
+ }
708
+ }
709
+ function cloneByOwnProperties(value) {
710
+ return Object.getOwnPropertyNames(value).reduce(
711
+ (clone, prop) => ({
712
+ ...clone,
713
+ [prop]: value[prop]
714
+ }),
715
+ {}
716
+ );
717
+ }
718
+ function stringifyReplace(key, value) {
719
+ if (value instanceof Error) {
720
+ const cloned = cloneByOwnProperties(value);
721
+ return {
722
+ name: value.name,
723
+ message: value.message,
724
+ stack: value.stack,
725
+ ...cloned
726
+ };
727
+ } else {
728
+ return value;
729
+ }
730
+ }
731
+
732
+ class BrowserServerState {
733
+ orchestrators = /* @__PURE__ */ new Map();
734
+ testers = /* @__PURE__ */ new Map();
735
+ cdps = /* @__PURE__ */ new Map();
736
+ contexts = /* @__PURE__ */ new Map();
737
+ getContext(contextId) {
738
+ return this.contexts.get(contextId);
739
+ }
740
+ createAsyncContext(method, contextId, files) {
741
+ const defer = createDefer();
742
+ this.contexts.set(contextId, {
743
+ files,
744
+ method,
745
+ resolve: () => {
746
+ defer.resolve();
747
+ this.contexts.delete(contextId);
748
+ },
749
+ reject: defer.reject
750
+ });
751
+ return defer;
752
+ }
753
+ async removeCDPHandler(sessionId) {
754
+ this.cdps.delete(sessionId);
755
+ }
756
+ }
757
+
758
+ function replacer(code, values) {
759
+ return code.replace(/\{\s*(\w+)\s*\}/g, (_, key) => values[key] ?? "");
760
+ }
761
+ const builtinProviders = ["webdriverio", "playwright", "preview"];
762
+ async function getBrowserProvider(options, project) {
763
+ if (options.provider == null || builtinProviders.includes(options.provider)) {
764
+ const providers = await import('./providers.js');
765
+ const provider = options.provider || "preview";
766
+ return providers[provider];
767
+ }
768
+ let customProviderModule;
769
+ try {
770
+ customProviderModule = await project.runner.executeId(
771
+ options.provider
772
+ );
773
+ } catch (error) {
774
+ throw new Error(
775
+ `Failed to load custom BrowserProvider from ${options.provider}`,
776
+ { cause: error }
777
+ );
778
+ }
779
+ if (customProviderModule.default == null) {
780
+ throw new Error(
781
+ `Custom BrowserProvider loaded from ${options.provider} was not the default export`
782
+ );
783
+ }
784
+ return customProviderModule.default;
785
+ }
786
+
787
+ class BrowserServerCDPHandler {
788
+ constructor(session, tester) {
789
+ this.session = session;
790
+ this.tester = tester;
791
+ }
792
+ listenerIds = {};
793
+ listeners = {};
794
+ send(method, params) {
795
+ return this.session.send(method, params);
796
+ }
797
+ on(event, id, once = false) {
798
+ if (!this.listenerIds[event]) {
799
+ this.listenerIds[event] = [];
800
+ }
801
+ this.listenerIds[event].push(id);
802
+ if (!this.listeners[event]) {
803
+ this.listeners[event] = (payload) => {
804
+ this.tester.cdpEvent(
805
+ event,
806
+ payload
807
+ );
808
+ if (once) {
809
+ this.off(event, id);
810
+ }
811
+ };
812
+ this.session.on(event, this.listeners[event]);
813
+ }
814
+ }
815
+ off(event, id) {
816
+ if (!this.listenerIds[event]) {
817
+ this.listenerIds[event] = [];
818
+ }
819
+ this.listenerIds[event] = this.listenerIds[event].filter((l) => l !== id);
820
+ if (!this.listenerIds[event].length) {
821
+ this.session.off(event, this.listeners[event]);
822
+ delete this.listeners[event];
823
+ }
824
+ }
825
+ once(event, listener) {
826
+ this.on(event, listener, true);
827
+ }
828
+ }
829
+
830
+ class BrowserServer {
831
+ constructor(project, base) {
832
+ this.project = project;
833
+ this.base = base;
834
+ this.stackTraceOptions = {
835
+ frameFilter: project.config.onStackTrace,
836
+ getSourceMap: (id) => {
837
+ const result = this.vite.moduleGraph.getModuleById(id)?.transformResult;
838
+ return result?.map;
839
+ },
840
+ getFileName: (id) => {
841
+ const mod = this.vite.moduleGraph.getModuleById(id);
842
+ if (mod?.file) {
843
+ return mod.file;
844
+ }
845
+ const modUrl = this.vite.moduleGraph.urlToModuleMap.get(id);
846
+ if (modUrl?.file) {
847
+ return modUrl.file;
848
+ }
849
+ return id;
850
+ }
851
+ };
852
+ this.state = new BrowserServerState();
853
+ const pkgRoot = resolve(fileURLToPath(import.meta.url), "../..");
854
+ const distRoot = resolve(pkgRoot, "dist");
855
+ this.prefixTesterUrl = `${base}__vitest_test__/__test__/`;
856
+ this.faviconUrl = `${base}__vitest__/favicon.svg`;
857
+ this.manifest = (async () => {
858
+ return JSON.parse(
859
+ await readFile$1(`${distRoot}/client/.vite/manifest.json`, "utf8")
860
+ );
861
+ })().then((manifest) => this.manifest = manifest);
862
+ this.testerHtml = readFile$1(
863
+ resolve(distRoot, "client/tester/tester.html"),
864
+ "utf8"
865
+ ).then((html) => this.testerHtml = html);
866
+ this.orchestratorHtml = (project.config.browser.ui ? readFile$1(resolve(distRoot, "client/__vitest__/index.html"), "utf8") : readFile$1(resolve(distRoot, "client/orchestrator.html"), "utf8")).then((html) => this.orchestratorHtml = html);
867
+ this.injectorJs = readFile$1(
868
+ resolve(distRoot, "client/esm-client-injector.js"),
869
+ "utf8"
870
+ ).then((js) => this.injectorJs = js);
871
+ this.stateJs = readFile$1(
872
+ resolve(distRoot, "state.js"),
873
+ "utf-8"
874
+ ).then((js) => this.stateJs = js);
875
+ }
876
+ faviconUrl;
877
+ prefixTesterUrl;
878
+ orchestratorScripts;
879
+ testerScripts;
880
+ manifest;
881
+ testerHtml;
882
+ orchestratorHtml;
883
+ injectorJs;
884
+ stateJs;
885
+ state;
886
+ provider;
887
+ vite;
888
+ stackTraceOptions;
889
+ setServer(server) {
890
+ this.vite = server;
891
+ }
892
+ getSerializableConfig() {
893
+ const config = wrapConfig(this.project.getSerializableConfig());
894
+ config.env ??= {};
895
+ config.env.VITEST_BROWSER_DEBUG = process.env.VITEST_BROWSER_DEBUG || "";
896
+ return config;
897
+ }
898
+ resolveTesterUrl(pathname) {
899
+ const [contextId, testFile] = pathname.slice(this.prefixTesterUrl.length).split("/");
900
+ const decodedTestFile = decodeURIComponent(testFile);
901
+ return { contextId, testFile: decodedTestFile };
902
+ }
903
+ async formatScripts(scripts) {
904
+ if (!scripts?.length) {
905
+ return "";
906
+ }
907
+ const server = this.vite;
908
+ const promises = scripts.map(
909
+ async ({ content, src, async, id, type = "module" }, index) => {
910
+ const srcLink = (src ? (await server.pluginContainer.resolveId(src))?.id : void 0) || src;
911
+ const transformId = srcLink || join(server.config.root, `virtual__${id || `injected-${index}.js`}`);
912
+ await server.moduleGraph.ensureEntryFromUrl(transformId);
913
+ const contentProcessed = content && type === "module" ? (await server.pluginContainer.transform(content, transformId)).code : content;
914
+ return `<script type="${type}"${async ? " async" : ""}${srcLink ? ` src="${slash(`/@fs/${srcLink}`)}"` : ""}>${contentProcessed || ""}<\/script>`;
915
+ }
916
+ );
917
+ return (await Promise.all(promises)).join("\n");
918
+ }
919
+ async initBrowserProvider() {
920
+ if (this.provider) {
921
+ return;
922
+ }
923
+ const Provider = await getBrowserProvider(this.project.config.browser, this.project);
924
+ this.provider = new Provider();
925
+ const browser = this.project.config.browser.name;
926
+ if (!browser) {
927
+ throw new Error(
928
+ `[${this.project.getName()}] Browser name is required. Please, set \`test.browser.name\` option manually.`
929
+ );
930
+ }
931
+ const supportedBrowsers = this.provider.getSupportedBrowsers();
932
+ if (supportedBrowsers.length && !supportedBrowsers.includes(browser)) {
933
+ throw new Error(
934
+ `[${this.project.getName()}] Browser "${browser}" is not supported by the browser provider "${this.provider.name}". Supported browsers: ${supportedBrowsers.join(", ")}.`
935
+ );
936
+ }
937
+ const providerOptions = this.project.config.browser.providerOptions;
938
+ await this.provider.initialize(this.project, {
939
+ browser,
940
+ options: providerOptions
941
+ });
942
+ }
943
+ parseErrorStacktrace(e, options = {}) {
944
+ return parseErrorStacktrace(e, {
945
+ ...this.stackTraceOptions,
946
+ ...options
947
+ });
948
+ }
949
+ parseStacktrace(trace, options = {}) {
950
+ return parseStacktrace(trace, {
951
+ ...this.stackTraceOptions,
952
+ ...options
953
+ });
954
+ }
955
+ cdpSessions = /* @__PURE__ */ new Map();
956
+ async ensureCDPHandler(contextId, sessionId) {
957
+ const cachedHandler = this.state.cdps.get(sessionId);
958
+ if (cachedHandler) {
959
+ return cachedHandler;
960
+ }
961
+ const provider = this.provider;
962
+ if (!provider.getCDPSession) {
963
+ throw new Error(`CDP is not supported by the provider "${provider.name}".`);
964
+ }
965
+ const promise = this.cdpSessions.get(sessionId) ?? await (async () => {
966
+ const promise2 = provider.getCDPSession(contextId).finally(() => {
967
+ this.cdpSessions.delete(sessionId);
968
+ });
969
+ this.cdpSessions.set(sessionId, promise2);
970
+ return promise2;
971
+ })();
972
+ const session = await promise;
973
+ const rpc = this.state.testers.get(sessionId);
974
+ if (!rpc) {
975
+ throw new Error(`Tester RPC "${sessionId}" was not established.`);
976
+ }
977
+ const handler = new BrowserServerCDPHandler(session, rpc);
978
+ this.state.cdps.set(
979
+ sessionId,
980
+ handler
981
+ );
982
+ return handler;
983
+ }
984
+ async close() {
985
+ await this.vite.close();
986
+ }
987
+ }
988
+ function wrapConfig(config) {
989
+ return {
990
+ ...config,
991
+ // workaround RegExp serialization
992
+ testNamePattern: config.testNamePattern ? config.testNamePattern.toString() : void 0
993
+ };
994
+ }
995
+
996
+ const click = async (context, selector, options = {}) => {
997
+ const provider = context.provider;
998
+ if (provider instanceof PlaywrightBrowserProvider) {
999
+ const tester = context.iframe;
1000
+ await tester.locator(`css=${selector}`).click({
1001
+ timeout: 1e3,
1002
+ ...options
1003
+ });
1004
+ } else if (provider instanceof WebdriverBrowserProvider) {
1005
+ const browser = context.browser;
1006
+ await browser.$(selector).click(options);
1007
+ } else {
1008
+ throw new TypeError(`Provider "${provider.name}" doesn't support click command`);
1009
+ }
1010
+ };
1011
+ const dblClick = async (context, selector, options = {}) => {
1012
+ const provider = context.provider;
1013
+ if (provider instanceof PlaywrightBrowserProvider) {
1014
+ const tester = context.iframe;
1015
+ await tester.locator(`css=${selector}`).dblclick(options);
1016
+ } else if (provider instanceof WebdriverBrowserProvider) {
1017
+ const browser = context.browser;
1018
+ await browser.$(selector).doubleClick();
1019
+ } else {
1020
+ throw new TypeError(`Provider "${provider.name}" doesn't support dblClick command`);
1021
+ }
1022
+ };
1023
+ const tripleClick = async (context, selector, options = {}) => {
1024
+ const provider = context.provider;
1025
+ if (provider instanceof PlaywrightBrowserProvider) {
1026
+ const tester = context.iframe;
1027
+ await tester.locator(`css=${selector}`).click({
1028
+ timeout: 1e3,
1029
+ ...options,
1030
+ clickCount: 3
1031
+ });
1032
+ } else if (provider instanceof WebdriverBrowserProvider) {
1033
+ const browser = context.browser;
1034
+ await browser.action("pointer", { parameters: { pointerType: "mouse" } }).move({ origin: await browser.$(selector) }).down().up().pause(50).down().up().pause(50).down().up().pause(50).perform();
1035
+ } else {
1036
+ throw new TypeError(`Provider "${provider.name}" doesn't support tripleClick command`);
1037
+ }
1038
+ };
1039
+
1040
+ var clickableInputTypes;
1041
+ (function(clickableInputTypes) {
1042
+ clickableInputTypes["button"] = "button";
1043
+ clickableInputTypes["color"] = "color";
1044
+ clickableInputTypes["file"] = "file";
1045
+ clickableInputTypes["image"] = "image";
1046
+ clickableInputTypes["reset"] = "reset";
1047
+ clickableInputTypes["submit"] = "submit";
1048
+ clickableInputTypes["checkbox"] = "checkbox";
1049
+ clickableInputTypes["radio"] = "radio";
1050
+ })(clickableInputTypes || (clickableInputTypes = {}));
1051
+
1052
+ const ClipboardStubControl = Symbol('Manage ClipboardSub');
1053
+ function isClipboardStub(clipboard) {
1054
+ return !!(clipboard === null || clipboard === void 0 ? void 0 : clipboard[ClipboardStubControl]);
1055
+ }
1056
+ function resetClipboardStubOnView(window) {
1057
+ if (isClipboardStub(window.navigator.clipboard)) {
1058
+ window.navigator.clipboard[ClipboardStubControl].resetClipboardStub();
1059
+ }
1060
+ }
1061
+ function detachClipboardStubFromView(window) {
1062
+ if (isClipboardStub(window.navigator.clipboard)) {
1063
+ window.navigator.clipboard[ClipboardStubControl].detachClipboardStub();
1064
+ }
1065
+ }
1066
+ const g = globalThis;
1067
+ /* istanbul ignore else */ if (typeof g.afterEach === 'function') {
1068
+ g.afterEach(()=>resetClipboardStubOnView(globalThis.window));
1069
+ }
1070
+ /* istanbul ignore else */ if (typeof g.afterAll === 'function') {
1071
+ g.afterAll(()=>detachClipboardStubFromView(globalThis.window));
1072
+ }
1073
+
1074
+ var editableInputTypes;
1075
+ (function(editableInputTypes) {
1076
+ editableInputTypes["text"] = "text";
1077
+ editableInputTypes["date"] = "date";
1078
+ editableInputTypes["datetime-local"] = "datetime-local";
1079
+ editableInputTypes["email"] = "email";
1080
+ editableInputTypes["month"] = "month";
1081
+ editableInputTypes["number"] = "number";
1082
+ editableInputTypes["password"] = "password";
1083
+ editableInputTypes["search"] = "search";
1084
+ editableInputTypes["tel"] = "tel";
1085
+ editableInputTypes["time"] = "time";
1086
+ editableInputTypes["url"] = "url";
1087
+ editableInputTypes["week"] = "week";
1088
+ })(editableInputTypes || (editableInputTypes = {}));
1089
+
1090
+ var maxLengthSupportedTypes;
1091
+ (function(maxLengthSupportedTypes) {
1092
+ maxLengthSupportedTypes["email"] = "email";
1093
+ maxLengthSupportedTypes["password"] = "password";
1094
+ maxLengthSupportedTypes["search"] = "search";
1095
+ maxLengthSupportedTypes["telephone"] = "telephone";
1096
+ maxLengthSupportedTypes["text"] = "text";
1097
+ maxLengthSupportedTypes["url"] = "url";
1098
+ })(maxLengthSupportedTypes || (maxLengthSupportedTypes = {}));
1099
+
1100
+ var bracketDict;
1101
+ (function(bracketDict) {
1102
+ bracketDict["{"] = "}";
1103
+ bracketDict["["] = "]";
1104
+ })(bracketDict || (bracketDict = {}));
1105
+ /**
1106
+ * Read the next key definition from user input
1107
+ *
1108
+ * Describe key per `{descriptor}` or `[descriptor]`.
1109
+ * Everything else will be interpreted as a single character as descriptor - e.g. `a`.
1110
+ * Brackets `{` and `[` can be escaped by doubling - e.g. `foo[[bar` translates to `foo[bar`.
1111
+ * A previously pressed key can be released per `{/descriptor}`.
1112
+ * Keeping the key pressed can be written as `{descriptor>}`.
1113
+ * When keeping the key pressed you can choose how long the key is pressed `{descriptor>3}`.
1114
+ * You can then release the key per `{descriptor>3/}` or keep it pressed and continue with the next key.
1115
+ */ function readNextDescriptor(text, context) {
1116
+ let pos = 0;
1117
+ const startBracket = text[pos] in bracketDict ? text[pos] : '';
1118
+ pos += startBracket.length;
1119
+ const isEscapedChar = new RegExp(`^\\${startBracket}{2}`).test(text);
1120
+ const type = isEscapedChar ? '' : startBracket;
1121
+ return {
1122
+ type,
1123
+ ...type === '' ? readPrintableChar(text, pos) : readTag(text, pos, type)
1124
+ };
1125
+ }
1126
+ function readPrintableChar(text, pos, context) {
1127
+ const descriptor = text[pos];
1128
+ assertDescriptor(descriptor, text, pos);
1129
+ pos += descriptor.length;
1130
+ return {
1131
+ consumedLength: pos,
1132
+ descriptor,
1133
+ releasePrevious: false,
1134
+ releaseSelf: true,
1135
+ repeat: 1
1136
+ };
1137
+ }
1138
+ function readTag(text, pos, startBracket, context) {
1139
+ var _text_slice_match, _text_slice_match1;
1140
+ const releasePreviousModifier = text[pos] === '/' ? '/' : '';
1141
+ pos += releasePreviousModifier.length;
1142
+ const escapedDescriptor = startBracket === '{' && text[pos] === '\\';
1143
+ pos += Number(escapedDescriptor);
1144
+ const descriptor = escapedDescriptor ? text[pos] : (_text_slice_match = text.slice(pos).match(startBracket === '{' ? /^\w+|^[^}>/]/ : /^\w+/)) === null || _text_slice_match === void 0 ? void 0 : _text_slice_match[0];
1145
+ assertDescriptor(descriptor, text, pos);
1146
+ pos += descriptor.length;
1147
+ var _text_slice_match_;
1148
+ const repeatModifier = (_text_slice_match_ = (_text_slice_match1 = text.slice(pos).match(/^>\d+/)) === null || _text_slice_match1 === void 0 ? void 0 : _text_slice_match1[0]) !== null && _text_slice_match_ !== void 0 ? _text_slice_match_ : '';
1149
+ pos += repeatModifier.length;
1150
+ const releaseSelfModifier = text[pos] === '/' || !repeatModifier && text[pos] === '>' ? text[pos] : '';
1151
+ pos += releaseSelfModifier.length;
1152
+ const expectedEndBracket = bracketDict[startBracket];
1153
+ const endBracket = text[pos] === expectedEndBracket ? expectedEndBracket : '';
1154
+ if (!endBracket) {
1155
+ throw new Error(getErrorMessage([
1156
+ !repeatModifier && 'repeat modifier',
1157
+ !releaseSelfModifier && 'release modifier',
1158
+ `"${expectedEndBracket}"`
1159
+ ].filter(Boolean).join(' or '), text[pos], text));
1160
+ }
1161
+ pos += endBracket.length;
1162
+ return {
1163
+ consumedLength: pos,
1164
+ descriptor,
1165
+ releasePrevious: !!releasePreviousModifier,
1166
+ repeat: repeatModifier ? Math.max(Number(repeatModifier.substr(1)), 1) : 1,
1167
+ releaseSelf: hasReleaseSelf(releaseSelfModifier, repeatModifier)
1168
+ };
1169
+ }
1170
+ function assertDescriptor(descriptor, text, pos, context) {
1171
+ if (!descriptor) {
1172
+ throw new Error(getErrorMessage('key descriptor', text[pos], text));
1173
+ }
1174
+ }
1175
+ function hasReleaseSelf(releaseSelfModifier, repeatModifier) {
1176
+ if (releaseSelfModifier) {
1177
+ return releaseSelfModifier === '/';
1178
+ }
1179
+ if (repeatModifier) {
1180
+ return false;
1181
+ }
1182
+ }
1183
+ function getErrorMessage(expected, found, text, context) {
1184
+ return `Expected ${expected} but found "${found !== null && found !== void 0 ? found : ''}" in "${text}"
1185
+ See ${`https://testing-library.com/docs/user-event/keyboard`}
1186
+ for more information about how userEvent parses your input.`;
1187
+ }
1188
+
1189
+ var ApiLevel;
1190
+ (function(ApiLevel) {
1191
+ ApiLevel[ApiLevel["Trigger"] = 2] = "Trigger";
1192
+ ApiLevel[ApiLevel["Call"] = 1] = "Call";
1193
+ })(ApiLevel || (ApiLevel = {}));
1194
+
1195
+ var PointerEventsCheckLevel;
1196
+ (function(PointerEventsCheckLevel) {
1197
+ /**
1198
+ * Check pointer events on every user interaction that triggers a bunch of events.
1199
+ * E.g. once for releasing a mouse button even though this triggers `pointerup`, `mouseup`, `click`, etc...
1200
+ */ PointerEventsCheckLevel[PointerEventsCheckLevel["EachTrigger"] = 4] = "EachTrigger";
1201
+ /** Check each target once per call to pointer (related) API */ PointerEventsCheckLevel[PointerEventsCheckLevel["EachApiCall"] = 2] = "EachApiCall";
1202
+ /** Check each event target once */ PointerEventsCheckLevel[PointerEventsCheckLevel["EachTarget"] = 1] = "EachTarget";
1203
+ /** No pointer events check */ PointerEventsCheckLevel[PointerEventsCheckLevel["Never"] = 0] = "Never";
1204
+ })(PointerEventsCheckLevel || (PointerEventsCheckLevel = {}));
1205
+
1206
+ /**
1207
+ * Parse key defintions per `keyboardMap`
1208
+ *
1209
+ * Keys can be referenced by `{key}` or `{special}` as well as physical locations per `[code]`.
1210
+ * Everything else will be interpreted as a typed character - e.g. `a`.
1211
+ * Brackets `{` and `[` can be escaped by doubling - e.g. `foo[[bar` translates to `foo[bar`.
1212
+ * Keeping the key pressed can be written as `{key>}`.
1213
+ * When keeping the key pressed you can choose how long (how many keydown and keypress) the key is pressed `{key>3}`.
1214
+ * You can then release the key per `{key>3/}` or keep it pressed and continue with the next key.
1215
+ */ function parseKeyDef(keyboardMap, text) {
1216
+ const defs = [];
1217
+ do {
1218
+ const { type, descriptor, consumedLength, releasePrevious, releaseSelf = true, repeat } = readNextDescriptor(text);
1219
+ var _keyboardMap_find;
1220
+ const keyDef = (_keyboardMap_find = keyboardMap.find((def)=>{
1221
+ if (type === '[') {
1222
+ var _def_code;
1223
+ return ((_def_code = def.code) === null || _def_code === void 0 ? void 0 : _def_code.toLowerCase()) === descriptor.toLowerCase();
1224
+ } else if (type === '{') {
1225
+ var _def_key;
1226
+ return ((_def_key = def.key) === null || _def_key === void 0 ? void 0 : _def_key.toLowerCase()) === descriptor.toLowerCase();
1227
+ }
1228
+ return def.key === descriptor;
1229
+ })) !== null && _keyboardMap_find !== void 0 ? _keyboardMap_find : {
1230
+ key: 'Unknown',
1231
+ code: 'Unknown',
1232
+ [type === '[' ? 'code' : 'key']: descriptor
1233
+ };
1234
+ defs.push({
1235
+ keyDef,
1236
+ releasePrevious,
1237
+ releaseSelf,
1238
+ repeat
1239
+ });
1240
+ text = text.slice(consumedLength);
1241
+ }while (text)
1242
+ return defs;
1243
+ }
1244
+
1245
+ var DOM_KEY_LOCATION;
1246
+ (function(DOM_KEY_LOCATION) {
1247
+ DOM_KEY_LOCATION[DOM_KEY_LOCATION["STANDARD"] = 0] = "STANDARD";
1248
+ DOM_KEY_LOCATION[DOM_KEY_LOCATION["LEFT"] = 1] = "LEFT";
1249
+ DOM_KEY_LOCATION[DOM_KEY_LOCATION["RIGHT"] = 2] = "RIGHT";
1250
+ DOM_KEY_LOCATION[DOM_KEY_LOCATION["NUMPAD"] = 3] = "NUMPAD";
1251
+ })(DOM_KEY_LOCATION || (DOM_KEY_LOCATION = {}));
1252
+
1253
+ /**
1254
+ * Mapping for a default US-104-QWERTY keyboard
1255
+ */ const defaultKeyMap = [
1256
+ // alphanumeric keys
1257
+ ...'0123456789'.split('').map((c)=>({
1258
+ code: `Digit${c}`,
1259
+ key: c
1260
+ })),
1261
+ ...')!@#$%^&*('.split('').map((c, i)=>({
1262
+ code: `Digit${i}`,
1263
+ key: c,
1264
+ shiftKey: true
1265
+ })),
1266
+ ...'abcdefghijklmnopqrstuvwxyz'.split('').map((c)=>({
1267
+ code: `Key${c.toUpperCase()}`,
1268
+ key: c
1269
+ })),
1270
+ ...'ABCDEFGHIJKLMNOPQRSTUVWXYZ'.split('').map((c)=>({
1271
+ code: `Key${c}`,
1272
+ key: c,
1273
+ shiftKey: true
1274
+ })),
1275
+ // alphanumeric block - functional
1276
+ {
1277
+ code: 'Space',
1278
+ key: ' '
1279
+ },
1280
+ {
1281
+ code: 'AltLeft',
1282
+ key: 'Alt',
1283
+ location: DOM_KEY_LOCATION.LEFT
1284
+ },
1285
+ {
1286
+ code: 'AltRight',
1287
+ key: 'Alt',
1288
+ location: DOM_KEY_LOCATION.RIGHT
1289
+ },
1290
+ {
1291
+ code: 'ShiftLeft',
1292
+ key: 'Shift',
1293
+ location: DOM_KEY_LOCATION.LEFT
1294
+ },
1295
+ {
1296
+ code: 'ShiftRight',
1297
+ key: 'Shift',
1298
+ location: DOM_KEY_LOCATION.RIGHT
1299
+ },
1300
+ {
1301
+ code: 'ControlLeft',
1302
+ key: 'Control',
1303
+ location: DOM_KEY_LOCATION.LEFT
1304
+ },
1305
+ {
1306
+ code: 'ControlRight',
1307
+ key: 'Control',
1308
+ location: DOM_KEY_LOCATION.RIGHT
1309
+ },
1310
+ {
1311
+ code: 'MetaLeft',
1312
+ key: 'Meta',
1313
+ location: DOM_KEY_LOCATION.LEFT
1314
+ },
1315
+ {
1316
+ code: 'MetaRight',
1317
+ key: 'Meta',
1318
+ location: DOM_KEY_LOCATION.RIGHT
1319
+ },
1320
+ {
1321
+ code: 'OSLeft',
1322
+ key: 'OS',
1323
+ location: DOM_KEY_LOCATION.LEFT
1324
+ },
1325
+ {
1326
+ code: 'OSRight',
1327
+ key: 'OS',
1328
+ location: DOM_KEY_LOCATION.RIGHT
1329
+ },
1330
+ {
1331
+ code: 'Tab',
1332
+ key: 'Tab'
1333
+ },
1334
+ {
1335
+ code: 'CapsLock',
1336
+ key: 'CapsLock'
1337
+ },
1338
+ {
1339
+ code: 'Backspace',
1340
+ key: 'Backspace'
1341
+ },
1342
+ {
1343
+ code: 'Enter',
1344
+ key: 'Enter'
1345
+ },
1346
+ // function
1347
+ {
1348
+ code: 'Escape',
1349
+ key: 'Escape'
1350
+ },
1351
+ // arrows
1352
+ {
1353
+ code: 'ArrowUp',
1354
+ key: 'ArrowUp'
1355
+ },
1356
+ {
1357
+ code: 'ArrowDown',
1358
+ key: 'ArrowDown'
1359
+ },
1360
+ {
1361
+ code: 'ArrowLeft',
1362
+ key: 'ArrowLeft'
1363
+ },
1364
+ {
1365
+ code: 'ArrowRight',
1366
+ key: 'ArrowRight'
1367
+ },
1368
+ // control pad
1369
+ {
1370
+ code: 'Home',
1371
+ key: 'Home'
1372
+ },
1373
+ {
1374
+ code: 'End',
1375
+ key: 'End'
1376
+ },
1377
+ {
1378
+ code: 'Delete',
1379
+ key: 'Delete'
1380
+ },
1381
+ {
1382
+ code: 'PageUp',
1383
+ key: 'PageUp'
1384
+ },
1385
+ {
1386
+ code: 'PageDown',
1387
+ key: 'PageDown'
1388
+ },
1389
+ // Special keys that are not part of a default US-layout but included for specific behavior
1390
+ {
1391
+ code: 'Fn',
1392
+ key: 'Fn'
1393
+ },
1394
+ {
1395
+ code: 'Symbol',
1396
+ key: 'Symbol'
1397
+ },
1398
+ {
1399
+ code: 'AltRight',
1400
+ key: 'AltGraph'
1401
+ }
1402
+ ];
1403
+
1404
+ const keyboard = async (context, text) => {
1405
+ function focusIframe() {
1406
+ if (!document.activeElement || document.activeElement.ownerDocument !== document || document.activeElement === document.body) {
1407
+ window.focus();
1408
+ }
1409
+ }
1410
+ if (context.provider instanceof PlaywrightBrowserProvider) {
1411
+ const frame = await context.frame();
1412
+ await frame.evaluate(focusIframe);
1413
+ } else if (context.provider instanceof WebdriverBrowserProvider) {
1414
+ await context.browser.execute(focusIframe);
1415
+ }
1416
+ await keyboardImplementation(
1417
+ context.provider,
1418
+ context.contextId,
1419
+ text,
1420
+ async () => {
1421
+ function selectAll() {
1422
+ const element = document.activeElement;
1423
+ if (element && element.select) {
1424
+ element.select();
1425
+ }
1426
+ }
1427
+ if (context.provider instanceof PlaywrightBrowserProvider) {
1428
+ const frame = await context.frame();
1429
+ await frame.evaluate(selectAll);
1430
+ } else if (context.provider instanceof WebdriverBrowserProvider) {
1431
+ await context.browser.execute(selectAll);
1432
+ } else {
1433
+ throw new TypeError(`Provider "${context.provider.name}" does not support selecting all text`);
1434
+ }
1435
+ },
1436
+ false
1437
+ );
1438
+ };
1439
+ async function keyboardImplementation(provider, contextId, text, selectAll, skipRelease) {
1440
+ const pressed = /* @__PURE__ */ new Set();
1441
+ if (provider instanceof PlaywrightBrowserProvider) {
1442
+ const page = provider.getPage(contextId);
1443
+ const actions = parseKeyDef(defaultKeyMap, text);
1444
+ for (const { releasePrevious, releaseSelf, repeat, keyDef } of actions) {
1445
+ const key = keyDef.key;
1446
+ if (pressed.has(key)) {
1447
+ await page.keyboard.up(key);
1448
+ pressed.delete(key);
1449
+ }
1450
+ if (!releasePrevious) {
1451
+ if (key === "selectall") {
1452
+ await selectAll();
1453
+ continue;
1454
+ }
1455
+ for (let i = 1; i <= repeat; i++) {
1456
+ await page.keyboard.down(key);
1457
+ }
1458
+ if (releaseSelf) {
1459
+ await page.keyboard.up(key);
1460
+ } else {
1461
+ pressed.add(key);
1462
+ }
1463
+ }
1464
+ }
1465
+ if (!skipRelease && pressed.size) {
1466
+ for (const key of pressed) {
1467
+ await page.keyboard.up(key);
1468
+ }
1469
+ }
1470
+ } else if (provider instanceof WebdriverBrowserProvider) {
1471
+ const { Key } = await import('webdriverio');
1472
+ const browser = provider.browser;
1473
+ const actions = parseKeyDef(defaultKeyMap, text);
1474
+ let keyboard2 = browser.action("key");
1475
+ for (const { releasePrevious, releaseSelf, repeat, keyDef } of actions) {
1476
+ let key = keyDef.key;
1477
+ const code = "location" in keyDef ? keyDef.key : keyDef.code;
1478
+ const special = Key[code];
1479
+ if (special) {
1480
+ key = special;
1481
+ }
1482
+ if (pressed.has(key)) {
1483
+ keyboard2.up(key);
1484
+ pressed.delete(key);
1485
+ }
1486
+ if (!releasePrevious) {
1487
+ if (key === "selectall") {
1488
+ await keyboard2.perform();
1489
+ keyboard2 = browser.action("key");
1490
+ await selectAll();
1491
+ continue;
1492
+ }
1493
+ for (let i = 1; i <= repeat; i++) {
1494
+ keyboard2.down(key);
1495
+ }
1496
+ if (releaseSelf) {
1497
+ keyboard2.up(key);
1498
+ } else {
1499
+ pressed.add(key);
1500
+ }
1501
+ }
1502
+ }
1503
+ await keyboard2.perform(skipRelease);
1504
+ }
1505
+ return {
1506
+ pressed
1507
+ };
1508
+ }
1509
+
1510
+ const type = async (context, selector, text, options = {}) => {
1511
+ const { skipClick = false, skipAutoClose = false } = options;
1512
+ if (context.provider instanceof PlaywrightBrowserProvider) {
1513
+ const { iframe } = context;
1514
+ const element = iframe.locator(`css=${selector}`);
1515
+ if (!skipClick) {
1516
+ await element.focus();
1517
+ }
1518
+ await keyboardImplementation(
1519
+ context.provider,
1520
+ context.contextId,
1521
+ text,
1522
+ () => element.selectText(),
1523
+ skipAutoClose
1524
+ );
1525
+ } else if (context.provider instanceof WebdriverBrowserProvider) {
1526
+ const browser = context.browser;
1527
+ const element = browser.$(selector);
1528
+ if (!skipClick && !await element.isFocused()) {
1529
+ await element.click();
1530
+ }
1531
+ await keyboardImplementation(
1532
+ context.provider,
1533
+ context.contextId,
1534
+ text,
1535
+ () => browser.execute(() => {
1536
+ const element2 = document.activeElement;
1537
+ if (element2) {
1538
+ element2.select();
1539
+ }
1540
+ }),
1541
+ skipAutoClose
1542
+ );
1543
+ } else {
1544
+ throw new TypeError(`Provider "${context.provider.name}" does not support typing`);
1545
+ }
164
1546
  };
165
- const basename = function(p, extension) {
166
- const lastSegment = normalizeWindowsPath(p).split("/").pop();
167
- return extension && lastSegment.endsWith(extension) ? lastSegment.slice(0, -extension.length) : lastSegment;
1547
+
1548
+ const clear = async (context, selector) => {
1549
+ if (context.provider instanceof PlaywrightBrowserProvider) {
1550
+ const { iframe } = context;
1551
+ const element = iframe.locator(`css=${selector}`);
1552
+ await element.clear({
1553
+ timeout: 1e3
1554
+ });
1555
+ } else if (context.provider instanceof WebdriverBrowserProvider) {
1556
+ const browser = context.browser;
1557
+ const element = await browser.$(selector);
1558
+ await element.clearValue();
1559
+ } else {
1560
+ throw new TypeError(`Provider "${context.provider.name}" does not support clearing elements`);
1561
+ }
1562
+ };
1563
+
1564
+ const fill = async (context, selector, text, options = {}) => {
1565
+ if (context.provider instanceof PlaywrightBrowserProvider) {
1566
+ const { iframe } = context;
1567
+ const element = iframe.locator(`css=${selector}`);
1568
+ await element.fill(text, { timeout: 1e3, ...options });
1569
+ } else if (context.provider instanceof WebdriverBrowserProvider) {
1570
+ const browser = context.browser;
1571
+ await browser.$(selector).setValue(text);
1572
+ } else {
1573
+ throw new TypeError(`Provider "${context.provider.name}" does not support clearing elements`);
1574
+ }
1575
+ };
1576
+
1577
+ const selectOptions = async (context, selector, userValues, options = {}) => {
1578
+ if (context.provider instanceof PlaywrightBrowserProvider) {
1579
+ const value = userValues;
1580
+ const { iframe } = context;
1581
+ const selectElement = iframe.locator(`css=${selector}`);
1582
+ const values = await Promise.all(value.map(async (v) => {
1583
+ if (typeof v === "string") {
1584
+ return v;
1585
+ }
1586
+ const elementHandler = await iframe.locator(`css=${v.element}`).elementHandle();
1587
+ if (!elementHandler) {
1588
+ throw new Error(`Element not found: ${v.element}`);
1589
+ }
1590
+ return elementHandler;
1591
+ }));
1592
+ await selectElement.selectOption(values, {
1593
+ timeout: 1e3,
1594
+ ...options
1595
+ });
1596
+ } else if (context.provider instanceof WebdriverBrowserProvider) {
1597
+ const values = userValues;
1598
+ if (!values.length) {
1599
+ return;
1600
+ }
1601
+ const browser = context.browser;
1602
+ if (values.length === 1 && "index" in values[0]) {
1603
+ const selectElement = browser.$(selector);
1604
+ await selectElement.selectByIndex(values[0].index);
1605
+ } else {
1606
+ throw new Error(`Provider "webdriverio" doesn't support selecting multiple values at once`);
1607
+ }
1608
+ } else {
1609
+ throw new TypeError(`Provider "${context.provider.name}" doesn't support selectOptions command`);
1610
+ }
168
1611
  };
169
1612
 
170
- const click = async ({ provider }, element, options = {}) => {
171
- if (provider.name === "playwright") {
172
- const page = provider.page;
173
- await page.frameLocator("iframe[data-vitest]").locator(`xpath=${element}`).click(options);
1613
+ const tab = async (context, options = {}) => {
1614
+ const provider = context.provider;
1615
+ if (provider instanceof PlaywrightBrowserProvider) {
1616
+ const page = context.page;
1617
+ await page.keyboard.press(options.shift === true ? "Shift+Tab" : "Tab");
174
1618
  return;
175
1619
  }
176
- if (provider.name === "webdriverio") {
177
- const page = provider.browser;
178
- const frame = await page.findElement("css selector", "iframe[data-vitest]");
179
- await page.switchToFrame(frame);
180
- const xpath = `//${element}`;
181
- await (await page.$(xpath)).click(options);
1620
+ if (provider instanceof WebdriverBrowserProvider) {
1621
+ const { Key } = await import('webdriverio');
1622
+ const browser = context.browser;
1623
+ await browser.keys(options.shift === true ? [Key.Shift, Key.Tab] : [Key.Tab]);
182
1624
  return;
183
1625
  }
184
- throw new Error(`Provider "${provider.name}" doesn't support click command`);
1626
+ throw new Error(`Provider "${provider.name}" doesn't support tab command`);
1627
+ };
1628
+
1629
+ const dragAndDrop = async (context, source, target, options) => {
1630
+ if (context.provider instanceof PlaywrightBrowserProvider) {
1631
+ const frame = await context.frame();
1632
+ await frame.dragAndDrop(
1633
+ `css=${source}`,
1634
+ `css=${target}`,
1635
+ {
1636
+ timeout: 1e3,
1637
+ ...options
1638
+ }
1639
+ );
1640
+ } else if (context.provider instanceof WebdriverBrowserProvider) {
1641
+ const $source = context.browser.$(source);
1642
+ const $target = context.browser.$(target);
1643
+ const duration = options?.duration ?? 10;
1644
+ await context.browser.action("pointer").move({ duration: 0, origin: $source, x: 0, y: 0 }).down({ button: 0 }).move({ duration: 0, origin: "pointer", x: 0, y: 0 }).pause(duration).move({ duration: 0, origin: $target, x: 0, y: 0 }).move({ duration: 0, origin: "pointer", x: 1, y: 0 }).move({ duration: 0, origin: "pointer", x: -1, y: 0 }).up({ button: 0 }).perform();
1645
+ } else {
1646
+ throw new TypeError(`Provider "${context.provider.name}" does not support dragging elements`);
1647
+ }
1648
+ };
1649
+
1650
+ const hover = async (context, selector, options = {}) => {
1651
+ if (context.provider instanceof PlaywrightBrowserProvider) {
1652
+ await context.iframe.locator(`css=${selector}`).hover({
1653
+ timeout: 1e3,
1654
+ ...options
1655
+ });
1656
+ } else if (context.provider instanceof WebdriverBrowserProvider) {
1657
+ const browser = context.browser;
1658
+ await browser.$(selector).moveTo(options);
1659
+ } else {
1660
+ throw new TypeError(`Provider "${context.provider.name}" does not support hover`);
1661
+ }
185
1662
  };
186
1663
 
187
1664
  function assertFileAccess(path, project) {
188
- if (!isFileServingAllowed(path, project.server) && !isFileServingAllowed(path, project.ctx.server))
189
- throw new Error(`Access denied to "${path}". See Vite config documentation for "server.fs": https://vitejs.dev/config/server-options.html#server-fs-strict.`);
1665
+ if (!isFileServingAllowed$1(path, project.server) && !isFileServingAllowed$1(path, project.ctx.server)) {
1666
+ throw new Error(
1667
+ `Access denied to "${path}". See Vite config documentation for "server.fs": https://vitejs.dev/config/server-options.html#server-fs-strict.`
1668
+ );
1669
+ }
190
1670
  }
191
1671
  const readFile = async ({ project, testPath = process.cwd() }, path, options = {}) => {
192
1672
  const filepath = resolve$1(dirname$1(testPath), path);
193
1673
  assertFileAccess(filepath, project);
194
- if (typeof options === "object" && !options.encoding)
1674
+ if (typeof options === "object" && !options.encoding) {
195
1675
  options.encoding = "utf-8";
1676
+ }
196
1677
  return promises.readFile(filepath, options);
197
1678
  };
198
1679
  const writeFile = async ({ project, testPath = process.cwd() }, path, data, options) => {
199
1680
  const filepath = resolve$1(dirname$1(testPath), path);
200
1681
  assertFileAccess(filepath, project);
201
1682
  const dir = dirname$1(filepath);
202
- if (!fs.existsSync(dir))
1683
+ if (!fs.existsSync(dir)) {
203
1684
  await promises.mkdir(dir, { recursive: true });
1685
+ }
204
1686
  await promises.writeFile(filepath, data, options);
205
1687
  };
206
1688
  const removeFile = async ({ project, testPath = process.cwd() }, path) => {
@@ -209,354 +1691,179 @@ const removeFile = async ({ project, testPath = process.cwd() }, path) => {
209
1691
  await promises.rm(filepath);
210
1692
  };
211
1693
 
212
- function isObject(payload) {
213
- return payload != null && typeof payload === "object";
214
- }
215
- function isSendKeysPayload(payload) {
216
- const validOptions = ["type", "press", "down", "up"];
217
- if (!isObject(payload))
218
- throw new Error("You must provide a `SendKeysPayload` object");
219
- const numberOfValidOptions = Object.keys(payload).filter(
220
- (key) => validOptions.includes(key)
221
- ).length;
222
- const unknownOptions = Object.keys(payload).filter((key) => !validOptions.includes(key));
223
- if (numberOfValidOptions > 1) {
224
- throw new Error(
225
- `You must provide ONLY one of the following properties to pass to the browser runner: ${validOptions.join(
226
- ", "
227
- )}.`
228
- );
1694
+ const screenshot = async (context, name, options = {}) => {
1695
+ if (!context.testPath) {
1696
+ throw new Error(`Cannot take a screenshot without a test path`);
229
1697
  }
230
- if (numberOfValidOptions === 0) {
231
- throw new Error(
232
- `You must provide one of the following properties to pass to the browser runner: ${validOptions.join(
233
- ", "
234
- )}.`
235
- );
1698
+ const path = resolveScreenshotPath(
1699
+ context.testPath,
1700
+ name,
1701
+ context.project.config
1702
+ );
1703
+ const savePath = normalize$1(path);
1704
+ await mkdir(dirname(path), { recursive: true });
1705
+ if (context.provider instanceof PlaywrightBrowserProvider) {
1706
+ if (options.element) {
1707
+ const { element: css, ...config } = options;
1708
+ const element = context.iframe.locator(`css=${css}`);
1709
+ const buffer2 = await element.screenshot({
1710
+ timeout: 1e3,
1711
+ ...config,
1712
+ path: savePath
1713
+ });
1714
+ return returnResult(options, path, buffer2);
1715
+ }
1716
+ const buffer = await context.iframe.locator("body").screenshot({
1717
+ ...options,
1718
+ path: savePath
1719
+ });
1720
+ return returnResult(options, path, buffer);
236
1721
  }
237
- if (unknownOptions.length > 0)
238
- throw new Error(`Unknown options \`${unknownOptions.join(", ")}\` present.`);
239
- return true;
240
- }
241
- function isTypePayload(payload) {
242
- return "type" in payload;
243
- }
244
- function isPressPayload(payload) {
245
- return "press" in payload;
246
- }
247
- function isDownPayload(payload) {
248
- return "down" in payload;
249
- }
250
- function isUpPayload(payload) {
251
- return "up" in payload;
252
- }
253
- const sendKeys = async ({ provider }, payload) => {
254
- if (!isSendKeysPayload(payload) || !payload)
255
- throw new Error("You must provide a `SendKeysPayload` object");
256
- if (provider.name === "playwright") {
257
- const page = provider.page;
258
- if (isTypePayload(payload))
259
- await page.keyboard.type(payload.type);
260
- else if (isPressPayload(payload))
261
- await page.keyboard.press(payload.press);
262
- else if (isDownPayload(payload))
263
- await page.keyboard.down(payload.down);
264
- else if (isUpPayload(payload))
265
- await page.keyboard.up(payload.up);
266
- } else if (provider.name === "webdriverio") {
267
- const browser = provider.browser;
268
- if (isTypePayload(payload))
269
- await browser.keys(payload.type.split(""));
270
- else if (isPressPayload(payload))
271
- await browser.keys([payload.press]);
272
- else
273
- throw new Error('Only "press" and "type" are supported by webdriverio.');
274
- } else {
275
- throw new Error(`"sendKeys" is not supported for ${provider.name} browser provider.`);
1722
+ if (context.provider instanceof WebdriverBrowserProvider) {
1723
+ const page = context.provider.browser;
1724
+ if (!options.element) {
1725
+ const body = await page.$("body");
1726
+ const buffer2 = await body.saveScreenshot(savePath);
1727
+ return returnResult(options, path, buffer2);
1728
+ }
1729
+ const element = await page.$(`${options.element}`);
1730
+ const buffer = await element.saveScreenshot(savePath);
1731
+ return returnResult(options, path, buffer);
276
1732
  }
1733
+ throw new Error(
1734
+ `Provider "${context.provider.name}" does not support screenshots`
1735
+ );
277
1736
  };
1737
+ function resolveScreenshotPath(testPath, name, config) {
1738
+ const dir = dirname(testPath);
1739
+ const base = basename(testPath);
1740
+ if (config.browser.screenshotDirectory) {
1741
+ return resolve(
1742
+ config.browser.screenshotDirectory,
1743
+ relative(config.root, dir),
1744
+ base,
1745
+ name
1746
+ );
1747
+ }
1748
+ return resolve(dir, "__screenshots__", base, name);
1749
+ }
1750
+ function returnResult(options, path, buffer) {
1751
+ if (options.base64) {
1752
+ return { path, base64: buffer.toString("base64") };
1753
+ }
1754
+ return path;
1755
+ }
278
1756
 
279
1757
  var builtinCommands = {
280
1758
  readFile,
281
1759
  removeFile,
282
1760
  writeFile,
283
- sendKeys,
284
- __vitest_click: click
1761
+ __vitest_click: click,
1762
+ __vitest_dblClick: dblClick,
1763
+ __vitest_tripleClick: tripleClick,
1764
+ __vitest_screenshot: screenshot,
1765
+ __vitest_type: type,
1766
+ __vitest_clear: clear,
1767
+ __vitest_fill: fill,
1768
+ __vitest_tab: tab,
1769
+ __vitest_keyboard: keyboard,
1770
+ __vitest_selectOptions: selectOptions,
1771
+ __vitest_dragAndDrop: dragAndDrop,
1772
+ __vitest_hover: hover
285
1773
  };
286
1774
 
287
1775
  const VIRTUAL_ID_CONTEXT = "\0@vitest/browser/context";
288
1776
  const ID_CONTEXT = "@vitest/browser/context";
289
1777
  const __dirname = dirname(fileURLToPath(import.meta.url));
290
- function BrowserContext(project) {
1778
+ function BrowserContext(server) {
1779
+ const project = server.project;
291
1780
  project.config.browser.commands ??= {};
292
- for (const [name, command] of Object.entries(builtinCommands))
1781
+ for (const [name, command] of Object.entries(builtinCommands)) {
293
1782
  project.config.browser.commands[name] ??= command;
1783
+ }
294
1784
  for (const command in project.config.browser.commands) {
295
- if (!/^[a-z_$][\w$]*$/i.test(command))
296
- throw new Error(`Invalid command name "${command}". Only alphanumeric characters, $ and _ are allowed.`);
1785
+ if (!/^[a-z_$][\w$]*$/i.test(command)) {
1786
+ throw new Error(
1787
+ `Invalid command name "${command}". Only alphanumeric characters, $ and _ are allowed.`
1788
+ );
1789
+ }
297
1790
  }
298
1791
  return {
299
1792
  name: "vitest:browser:virtual-module:context",
300
1793
  enforce: "pre",
301
1794
  resolveId(id) {
302
- if (id === ID_CONTEXT)
1795
+ if (id === ID_CONTEXT) {
303
1796
  return VIRTUAL_ID_CONTEXT;
1797
+ }
304
1798
  },
305
1799
  load(id) {
306
- if (id === VIRTUAL_ID_CONTEXT)
307
- return generateContextFile.call(this, project);
1800
+ if (id === VIRTUAL_ID_CONTEXT) {
1801
+ return generateContextFile.call(this, server);
1802
+ }
308
1803
  }
309
1804
  };
310
1805
  }
311
- async function generateContextFile(project) {
312
- const commands = Object.keys(project.config.browser.commands ?? {});
1806
+ async function generateContextFile(server) {
1807
+ const commands = Object.keys(server.project.config.browser.commands ?? {});
313
1808
  const filepathCode = "__vitest_worker__.filepath || __vitest_worker__.current?.file?.filepath || undefined";
314
- const provider = project.browserProvider;
315
- const commandsCode = commands.map((command) => {
316
- return ` ["${command}"]: (...args) => rpc().triggerCommand("${command}", filepath(), args),`;
1809
+ const provider = server.provider;
1810
+ const commandsCode = commands.filter((command) => !command.startsWith("__vitest")).map((command) => {
1811
+ return ` ["${command}"]: (...args) => rpc().triggerCommand(contextId, "${command}", filepath(), args),`;
317
1812
  }).join("\n");
318
- const userEventNonProviderImport = await getUserEventImport(provider, this.resolve.bind(this));
1813
+ const userEventNonProviderImport = await getUserEventImport(
1814
+ provider,
1815
+ this.resolve.bind(this)
1816
+ );
1817
+ const distContextPath = slash(`/@fs/${resolve(__dirname, "context.js")}`);
319
1818
  return `
1819
+ import { page, userEvent as __userEvent_CDP__, cdp } from '${distContextPath}'
320
1820
  ${userEventNonProviderImport}
321
1821
  const filepath = () => ${filepathCode}
322
1822
  const rpc = () => __vitest_worker__.rpc
323
- const channel = new BroadcastChannel('vitest')
1823
+ const contextId = __vitest_browser_runner__.contextId
324
1824
 
325
1825
  export const server = {
326
1826
  platform: ${JSON.stringify(process.platform)},
327
1827
  version: ${JSON.stringify(process.version)},
328
1828
  provider: ${JSON.stringify(provider.name)},
329
- browser: ${JSON.stringify(project.config.browser.name)},
1829
+ browser: ${JSON.stringify(server.project.config.browser.name)},
330
1830
  commands: {
331
1831
  ${commandsCode}
332
1832
  }
333
1833
  }
334
1834
  export const commands = server.commands
335
- export const page = {
336
- get config() {
337
- return __vitest_browser_runner__.config
338
- },
339
- viewport(width, height) {
340
- const id = __vitest_browser_runner__.iframeId
341
- channel.postMessage({ type: 'viewport', width, height, id })
342
- return new Promise((resolve, reject) => {
343
- channel.addEventListener('message', function handler(e) {
344
- if (e.data.type === 'viewport:done' && e.data.id === id) {
345
- channel.removeEventListener('message', handler)
346
- resolve()
347
- }
348
- if (e.data.type === 'viewport:fail' && e.data.id === id) {
349
- channel.removeEventListener('message', handler)
350
- reject(new Error(e.data.error))
351
- }
352
- })
353
- })
354
- },
355
- }
356
-
357
- export const userEvent = ${getUserEventScript(project)}
358
-
359
- function convertElementToXPath(element) {
360
- if (!element || !(element instanceof Element)) {
361
- // TODO: better error message
362
- throw new Error('Expected element to be an instance of Element')
363
- }
364
- return getPathTo(element)
365
- }
366
-
367
- function getPathTo(element) {
368
- if (element.id !== '')
369
- return \`id("\${element.id}")\`
370
-
371
- if (!element.parentNode || element === document.documentElement)
372
- return element.tagName
373
-
374
- let ix = 0
375
- const siblings = element.parentNode.childNodes
376
- for (let i = 0; i < siblings.length; i++) {
377
- const sibling = siblings[i]
378
- if (sibling === element)
379
- return \`\${getPathTo(element.parentNode)}/\${element.tagName}[\${ix + 1}]\`
380
- if (sibling.nodeType === 1 && sibling.tagName === element.tagName)
381
- ix++
382
- }
383
- }
1835
+ export const userEvent = ${getUserEvent(provider)}
1836
+ export { page, cdp }
384
1837
  `;
385
1838
  }
386
- async function getUserEventImport(provider, resolve) {
387
- if (provider.name !== "preview")
388
- return "";
389
- const resolved = await resolve("@testing-library/user-event", __dirname);
390
- if (!resolved)
391
- throw new Error(`Failed to resolve user-event package from ${__dirname}`);
392
- return `import { userEvent as __vitest_user_event__ } from '${slash(`/@fs/${resolved.id}`)}'`;
393
- }
394
- function getUserEventScript(project) {
395
- if (project.browserProvider?.name === "preview")
396
- return `__vitest_user_event__`;
1839
+ function getUserEvent(provider) {
1840
+ if (provider.name !== "preview") {
1841
+ return "__userEvent_CDP__";
1842
+ }
397
1843
  return `{
398
- async click(element, options) {
399
- const xpath = convertElementToXPath(element)
400
- return rpc().triggerCommand('__vitest_click', filepath(), options ? [xpath, options] : [xpath]);
1844
+ ...__vitest_user_event__,
1845
+ fill: async (element, text) => {
1846
+ await __vitest_user_event__.clear(element)
1847
+ await __vitest_user_event__.type(element, text)
401
1848
  },
1849
+ dragAndDrop: async () => {
1850
+ throw new Error('Provider "preview" does not support dragging elements')
1851
+ }
402
1852
  }`;
403
1853
  }
404
-
405
- function automockModule(code, parse) {
406
- const ast = parse(code);
407
- const m = new MagicString(code);
408
- const allSpecifiers = [];
409
- let importIndex = 0;
410
- for (const _node of ast.body) {
411
- if (_node.type === "ExportAllDeclaration") {
412
- throw new Error(
413
- `automocking files with \`export *\` is not supported in browser mode because it cannot be statically analysed`
414
- );
415
- }
416
- if (_node.type === "ExportNamedDeclaration") {
417
- let traversePattern2 = function(expression) {
418
- if (expression.type === "Identifier") {
419
- allSpecifiers.push({ name: expression.name });
420
- } else if (expression.type === "ArrayPattern") {
421
- expression.elements.forEach((element) => {
422
- if (!element)
423
- return;
424
- traversePattern2(element);
425
- });
426
- } else if (expression.type === "ObjectPattern") {
427
- expression.properties.forEach((property) => {
428
- if (property.type === "RestElement")
429
- traversePattern2(property);
430
- else if (property.type === "Property")
431
- traversePattern2(property.value);
432
- else
433
- ;
434
- });
435
- } else if (expression.type === "RestElement") {
436
- traversePattern2(expression.argument);
437
- } else if (expression.type === "AssignmentPattern") {
438
- throw new Error(`AssignmentPattern is not supported. Please open a new bug report.`);
439
- } else if (expression.type === "MemberExpression") {
440
- throw new Error(`MemberExpression is not supported. Please open a new bug report.`);
441
- } else ;
442
- };
443
- const node = _node;
444
- const declaration = node.declaration;
445
- if (declaration) {
446
- if (declaration.type === "FunctionDeclaration") {
447
- allSpecifiers.push({ name: declaration.id.name });
448
- } else if (declaration.type === "VariableDeclaration") {
449
- declaration.declarations.forEach((declaration2) => {
450
- traversePattern2(declaration2.id);
451
- });
452
- } else if (declaration.type === "ClassDeclaration") {
453
- allSpecifiers.push({ name: declaration.id.name });
454
- } else ;
455
- m.remove(node.start, declaration.start);
456
- }
457
- const specifiers = node.specifiers || [];
458
- const source = node.source;
459
- if (!source && specifiers.length) {
460
- specifiers.forEach((specifier) => {
461
- const exported = specifier.exported;
462
- allSpecifiers.push({
463
- alias: exported.type === "Literal" ? exported.raw : exported.name,
464
- name: specifier.local.name
465
- });
466
- });
467
- m.remove(node.start, node.end);
468
- } else if (source && specifiers.length) {
469
- const importNames = [];
470
- specifiers.forEach((specifier) => {
471
- const importedName = `__vitest_imported_${importIndex++}__`;
472
- const exported = specifier.exported;
473
- importNames.push([specifier.local.name, importedName]);
474
- allSpecifiers.push({
475
- name: importedName,
476
- alias: exported.type === "Literal" ? exported.raw : exported.name
477
- });
478
- });
479
- const importString = `import { ${importNames.map(([name, alias]) => `${name} as ${alias}`).join(", ")} } from '${source.value}'`;
480
- m.overwrite(node.start, node.end, importString);
481
- }
482
- }
483
- if (_node.type === "ExportDefaultDeclaration") {
484
- const node = _node;
485
- const declaration = node.declaration;
486
- allSpecifiers.push({ name: "__vitest_default", alias: "default" });
487
- m.overwrite(node.start, declaration.start, `const __vitest_default = `);
488
- }
1854
+ async function getUserEventImport(provider, resolve2) {
1855
+ if (provider.name !== "preview") {
1856
+ return "";
489
1857
  }
490
- const moduleObject = `
491
- const __vitest_es_current_module__ = {
492
- __esModule: true,
493
- ${allSpecifiers.map(({ name }) => `["${name}"]: ${name},`).join("\n ")}
494
- }
495
- const __vitest_mocked_module__ = __vitest_mocker__.mockObject(__vitest_es_current_module__)
496
- `;
497
- const assigning = allSpecifiers.map(({ name }, index) => {
498
- return `const __vitest_mocked_${index}__ = __vitest_mocked_module__["${name}"]`;
499
- }).join("\n");
500
- const redeclarations = allSpecifiers.map(({ name, alias }, index) => {
501
- return ` __vitest_mocked_${index}__ as ${alias || name},`;
502
- }).join("\n");
503
- const specifiersExports = `
504
- export {
505
- ${redeclarations}
506
- }
507
- `;
508
- m.append(moduleObject + assigning + specifiersExports);
509
- return m;
1858
+ const resolved = await resolve2("@testing-library/user-event", __dirname);
1859
+ if (!resolved) {
1860
+ throw new Error(`Failed to resolve user-event package from ${__dirname}`);
1861
+ }
1862
+ return `import { userEvent as __vitest_user_event__ } from '${slash(
1863
+ `/@fs/${resolved.id}`
1864
+ )}'`;
510
1865
  }
511
1866
 
512
- var BrowserMocker = (project) => {
513
- return [
514
- {
515
- name: "vitest:browser:mocker",
516
- enforce: "pre",
517
- async load(id) {
518
- const data = project.browserMocker.mocks.get(id);
519
- if (!data)
520
- return;
521
- const { mock, sessionId } = data;
522
- if (mock === void 0) {
523
- const rpc = project.browserRpc.testers.get(sessionId);
524
- if (!rpc)
525
- throw new Error(`WebSocket rpc was destroyed for session ${sessionId}`);
526
- const exports = await rpc.startMocking(id);
527
- const module = `const module = __vitest_mocker__.get('${id}');`;
528
- const keys = exports.map((name) => {
529
- if (name === "default")
530
- return `export default module['default'];`;
531
- return `export const ${name} = module['${name}'];`;
532
- }).join("\n");
533
- return `${module}
534
- ${keys}`;
535
- }
536
- if (mock === null)
537
- return;
538
- return readFile$1(mock, "utf-8");
539
- }
540
- },
541
- {
542
- name: "vitest:browser:automocker",
543
- enforce: "post",
544
- transform(code, id) {
545
- const data = project.browserMocker.mocks.get(id);
546
- if (!data)
547
- return;
548
- if (data.mock === null) {
549
- const m = automockModule(code, this.parse);
550
- return {
551
- code: m.toString(),
552
- map: m.generateMap({ hires: "boundary", source: id })
553
- };
554
- }
555
- }
556
- }
557
- ];
558
- };
559
-
560
1867
  function injectDynamicImport(code, id, parse) {
561
1868
  const s = new MagicString(code);
562
1869
  let ast;
@@ -572,8 +1879,14 @@ ${err.message}`);
572
1879
  onImportMeta() {
573
1880
  },
574
1881
  onDynamicImport(node) {
575
- const replace = "__vitest_browser_runner__.wrapModule(() => import(";
576
- s.overwrite(node.start, node.source.start, replace);
1882
+ const replaceString = "__vitest_browser_runner__.wrapModule(() => import(";
1883
+ const importSubstring = code.substring(node.start, node.end);
1884
+ const hasIgnore = importSubstring.includes("/* @vite-ignore */");
1885
+ s.overwrite(
1886
+ node.start,
1887
+ node.source.start,
1888
+ replaceString + (hasIgnore ? "/* @vite-ignore */ " : "")
1889
+ );
577
1890
  s.overwrite(node.end - 1, node.end, "))");
578
1891
  }
579
1892
  });
@@ -590,137 +1903,189 @@ var DynamicImport = () => {
590
1903
  name: "vitest:browser:esm-injector",
591
1904
  enforce: "post",
592
1905
  transform(source, id) {
593
- if (!regexDynamicImport.test(source))
1906
+ if (!regexDynamicImport.test(source)) {
594
1907
  return;
1908
+ }
595
1909
  return injectDynamicImport(source, id, this.parse);
596
1910
  }
597
1911
  };
598
1912
  };
599
1913
 
600
- var index = (project, base = "/") => {
1914
+ async function resolveOrchestrator(server, url, res) {
1915
+ const project = server.project;
1916
+ let contextId = url.searchParams.get("contextId");
1917
+ if (!contextId) {
1918
+ const contexts = [...server.state.orchestrators.keys()];
1919
+ contextId = contexts[contexts.length - 1] ?? "none";
1920
+ }
1921
+ const files = server.state.getContext(contextId)?.files ?? [];
1922
+ const config = server.getSerializableConfig();
1923
+ const injectorJs = typeof server.injectorJs === "string" ? server.injectorJs : await server.injectorJs;
1924
+ const injector = replacer(injectorJs, {
1925
+ __VITEST_PROVIDER__: JSON.stringify(server.provider.name),
1926
+ __VITEST_CONFIG__: JSON.stringify(config),
1927
+ __VITEST_VITE_CONFIG__: JSON.stringify({
1928
+ root: server.vite.config.root
1929
+ }),
1930
+ __VITEST_FILES__: JSON.stringify(files),
1931
+ __VITEST_TYPE__: '"orchestrator"',
1932
+ __VITEST_CONTEXT_ID__: JSON.stringify(contextId),
1933
+ __VITEST_PROVIDED_CONTEXT__: "{}"
1934
+ });
1935
+ res.removeHeader("Content-Security-Policy");
1936
+ if (!server.orchestratorScripts) {
1937
+ server.orchestratorScripts = await server.formatScripts(
1938
+ project.config.browser.orchestratorScripts
1939
+ );
1940
+ }
1941
+ let baseHtml = typeof server.orchestratorHtml === "string" ? server.orchestratorHtml : await server.orchestratorHtml;
1942
+ if (project.config.browser.ui) {
1943
+ const manifestContent = server.manifest instanceof Promise ? await server.manifest : server.manifest;
1944
+ const jsEntry = manifestContent["orchestrator.html"].file;
1945
+ const base = server.vite.config.base || "/";
1946
+ baseHtml = baseHtml.replaceAll("./assets/", `${base}__vitest__/assets/`).replace(
1947
+ "<!-- !LOAD_METADATA! -->",
1948
+ [
1949
+ "<script>{__VITEST_INJECTOR__}<\/script>",
1950
+ "{__VITEST_SCRIPTS__}",
1951
+ `<script type="module" crossorigin src="${base}${jsEntry}"><\/script>`
1952
+ ].join("\n")
1953
+ );
1954
+ }
1955
+ return replacer(baseHtml, {
1956
+ __VITEST_FAVICON__: server.faviconUrl,
1957
+ __VITEST_TITLE__: "Vitest Browser Runner",
1958
+ __VITEST_SCRIPTS__: server.orchestratorScripts,
1959
+ __VITEST_INJECTOR__: injector,
1960
+ __VITEST_CONTEXT_ID__: JSON.stringify(contextId)
1961
+ });
1962
+ }
1963
+
1964
+ async function resolveTester(server, url, res) {
1965
+ const csp = res.getHeader("Content-Security-Policy");
1966
+ if (typeof csp === "string") {
1967
+ res.setHeader(
1968
+ "Content-Security-Policy",
1969
+ csp.replace(/frame-ancestors [^;]+/, "frame-ancestors *")
1970
+ );
1971
+ }
1972
+ const { contextId, testFile } = server.resolveTesterUrl(url.pathname);
1973
+ const project = server.project;
1974
+ const state = server.state;
1975
+ const testFiles = await project.globTestFiles();
1976
+ const tests = testFile === "__vitest_all__" || !testFiles.includes(testFile) ? "__vitest_browser_runner__.files" : JSON.stringify([testFile]);
1977
+ const iframeId = JSON.stringify(testFile);
1978
+ const context = state.getContext(contextId);
1979
+ const files = context?.files ?? [];
1980
+ const method = context?.method ?? "run";
1981
+ const injectorJs = typeof server.injectorJs === "string" ? server.injectorJs : await server.injectorJs;
1982
+ const config = server.getSerializableConfig();
1983
+ const injector = replacer(injectorJs, {
1984
+ __VITEST_PROVIDER__: JSON.stringify(server.provider.name),
1985
+ __VITEST_CONFIG__: JSON.stringify(config),
1986
+ __VITEST_FILES__: JSON.stringify(files),
1987
+ __VITEST_VITE_CONFIG__: JSON.stringify({
1988
+ root: server.vite.config.root
1989
+ }),
1990
+ __VITEST_TYPE__: '"tester"',
1991
+ __VITEST_CONTEXT_ID__: JSON.stringify(contextId),
1992
+ __VITEST_PROVIDED_CONTEXT__: JSON.stringify(stringify(project.getProvidedContext()))
1993
+ });
1994
+ if (!server.testerScripts) {
1995
+ const testerScripts = await server.formatScripts(
1996
+ project.config.browser.testerScripts
1997
+ );
1998
+ const clientScript = `<script type="module" src="${config.base || "/"}@vite/client"><\/script>`;
1999
+ const stateJs = typeof server.stateJs === "string" ? server.stateJs : await server.stateJs;
2000
+ const stateScript = `<script type="module">${stateJs}<\/script>`;
2001
+ server.testerScripts = `${stateScript}${clientScript}${testerScripts}`;
2002
+ }
2003
+ const testerHtml = typeof server.testerHtml === "string" ? server.testerHtml : await server.testerHtml;
2004
+ return replacer(testerHtml, {
2005
+ __VITEST_FAVICON__: server.faviconUrl,
2006
+ __VITEST_TITLE__: "Vitest Browser Tester",
2007
+ __VITEST_SCRIPTS__: server.testerScripts,
2008
+ __VITEST_INJECTOR__: injector,
2009
+ __VITEST_APPEND__: `<script type="module">
2010
+ __vitest_browser_runner__.runningFiles = ${tests}
2011
+ __vitest_browser_runner__.iframeId = ${iframeId}
2012
+ __vitest_browser_runner__.${method === "run" ? "runTests" : "collectTests"}(__vitest_browser_runner__.runningFiles)
2013
+ <\/script>`
2014
+ });
2015
+ }
2016
+
2017
+ var BrowserPlugin = (browserServer, base = "/") => {
601
2018
  const pkgRoot = resolve(fileURLToPath(import.meta.url), "../..");
602
2019
  const distRoot = resolve(pkgRoot, "dist");
2020
+ const project = browserServer.project;
2021
+ let loupePath;
603
2022
  return [
604
- ...BrowserMocker(project),
605
2023
  {
606
2024
  enforce: "pre",
607
2025
  name: "vitest:browser",
608
- async config(viteConfig) {
609
- // Enables using ignore hint for coverage providers with @preserve keyword
610
- if (viteConfig.esbuild !== false) {
611
- viteConfig.esbuild ||= {};
612
- viteConfig.esbuild.legalComments = "inline";
613
- }
614
- },
615
2026
  async configureServer(server) {
616
- const testerHtml = readFile$1(resolve(distRoot, "client/tester.html"), "utf8");
617
- const orchestratorHtml = project.config.browser.ui ? readFile$1(resolve(distRoot, "client/__vitest__/index.html"), "utf8") : readFile$1(resolve(distRoot, "client/orchestrator.html"), "utf8");
618
- const injectorJs = readFile$1(resolve(distRoot, "client/esm-client-injector.js"), "utf8");
619
- const manifest = (async () => {
620
- return JSON.parse(await readFile$1(`${distRoot}/client/.vite/manifest.json`, "utf8"));
621
- })();
622
- const favicon = `${base}favicon.svg`;
623
- const testerPrefix = `${base}__vitest_test__/__test__/`;
624
- server.middlewares.use((_req, res, next) => {
2027
+ browserServer.setServer(server);
2028
+ server.middlewares.use(function vitestHeaders(_req, res, next) {
625
2029
  const headers = server.config.server.headers;
626
2030
  if (headers) {
627
- for (const name in headers)
2031
+ for (const name in headers) {
628
2032
  res.setHeader(name, headers[name]);
2033
+ }
629
2034
  }
630
2035
  next();
631
2036
  });
632
- let orchestratorScripts;
633
- let testerScripts;
634
- server.middlewares.use(async (req, res, next) => {
635
- if (!req.url)
2037
+ server.middlewares.use(async function vitestBrowserMode(req, res, next) {
2038
+ if (!req.url) {
636
2039
  return next();
2040
+ }
637
2041
  const url = new URL(req.url, "http://localhost");
638
- if (!url.pathname.startsWith(testerPrefix) && url.pathname !== base)
2042
+ if (!url.pathname.startsWith(browserServer.prefixTesterUrl) && url.pathname !== base) {
639
2043
  return next();
640
- res.setHeader("Cache-Control", "no-cache, max-age=0, must-revalidate");
2044
+ }
2045
+ res.setHeader(
2046
+ "Cache-Control",
2047
+ "no-cache, max-age=0, must-revalidate"
2048
+ );
641
2049
  res.setHeader("Content-Type", "text/html; charset=utf-8");
642
- const files = project.browserState?.files ?? [];
643
- const config = wrapConfig(project.getSerializableConfig());
644
- config.env ??= {};
645
- config.env.VITEST_BROWSER_DEBUG = process.env.VITEST_BROWSER_DEBUG || "";
646
- const injector = replacer(await injectorJs, {
647
- __VITEST_CONFIG__: JSON.stringify(config),
648
- __VITEST_FILES__: JSON.stringify(files),
649
- __VITEST_TYPE__: url.pathname === base ? '"orchestrator"' : '"tester"'
650
- });
651
2050
  res.removeHeader("X-Frame-Options");
652
2051
  if (url.pathname === base) {
653
- res.removeHeader("Content-Security-Policy");
654
- if (!orchestratorScripts)
655
- orchestratorScripts = await formatScripts(project.config.browser.orchestratorScripts, server);
656
- let baseHtml = await orchestratorHtml;
657
- if (project.config.browser.ui) {
658
- const manifestContent = await manifest;
659
- const jsEntry = manifestContent["orchestrator.html"].file;
660
- baseHtml = baseHtml.replaceAll("./assets/", `${base}__vitest__/assets/`).replace(
661
- "<!-- !LOAD_METADATA! -->",
662
- [
663
- "<script>{__VITEST_INJECTOR__}<\/script>",
664
- "{__VITEST_SCRIPTS__}",
665
- `<script type="module" crossorigin src="${jsEntry}"><\/script>`
666
- ].join("\n")
667
- );
668
- }
669
- const html2 = replacer(baseHtml, {
670
- __VITEST_FAVICON__: favicon,
671
- __VITEST_TITLE__: "Vitest Browser Runner",
672
- __VITEST_SCRIPTS__: orchestratorScripts,
673
- __VITEST_INJECTOR__: injector
674
- });
2052
+ const html2 = await resolveOrchestrator(browserServer, url, res);
675
2053
  res.write(html2, "utf-8");
676
2054
  res.end();
677
2055
  return;
678
2056
  }
679
- const csp = res.getHeader("Content-Security-Policy");
680
- if (typeof csp === "string") {
681
- res.setHeader("Content-Security-Policy", csp.replace(/frame-ancestors [^;]+/, "frame-ancestors *"));
682
- }
683
- const decodedTestFile = decodeURIComponent(url.pathname.slice(testerPrefix.length));
684
- const testFiles = await project.globTestFiles();
685
- const tests = decodedTestFile === "__vitest_all__" || !testFiles.includes(decodedTestFile) ? "__vitest_browser_runner__.files" : JSON.stringify([decodedTestFile]);
686
- const iframeId = JSON.stringify(decodedTestFile);
687
- if (!testerScripts)
688
- testerScripts = await formatScripts(project.config.browser.testerScripts, server);
689
- const html = replacer(await testerHtml, {
690
- __VITEST_FAVICON__: favicon,
691
- __VITEST_TITLE__: "Vitest Browser Tester",
692
- __VITEST_SCRIPTS__: testerScripts,
693
- __VITEST_INJECTOR__: injector,
694
- __VITEST_APPEND__: (
695
- // TODO: have only a single global variable to not pollute the global scope
696
- `<script type="module">
697
- __vitest_browser_runner__.runningFiles = ${tests}
698
- __vitest_browser_runner__.iframeId = ${iframeId}
699
- __vitest_browser_runner__.runTests(__vitest_browser_runner__.runningFiles)
700
- <\/script>`
701
- )
702
- });
2057
+ const html = await resolveTester(browserServer, url, res);
703
2058
  res.write(html, "utf-8");
704
2059
  res.end();
705
2060
  });
706
2061
  server.middlewares.use(
707
- base,
708
- sirv(resolve(distRoot, "client"), {
709
- single: false,
710
- dev: true
711
- })
2062
+ `${base}favicon.svg`,
2063
+ (_, res) => {
2064
+ const content = readFileSync(resolve(distRoot, "client/favicon.svg"));
2065
+ res.write(content, "utf-8");
2066
+ res.end();
2067
+ }
712
2068
  );
713
2069
  const coverageFolder = resolveCoverageFolder(project);
714
2070
  const coveragePath = coverageFolder ? coverageFolder[1] : void 0;
715
- if (coveragePath && base === coveragePath)
716
- throw new Error(`The ui base path and the coverage path cannot be the same: ${base}, change coverage.reportsDirectory`);
717
- coverageFolder && server.middlewares.use(coveragePath, sirv(coverageFolder[0], {
718
- single: true,
719
- dev: true,
720
- setHeaders: (res) => {
721
- res.setHeader("Cache-Control", "public,max-age=0,must-revalidate");
722
- }
723
- }));
2071
+ if (coveragePath && base === coveragePath) {
2072
+ throw new Error(
2073
+ `The ui base path and the coverage path cannot be the same: ${base}, change coverage.reportsDirectory`
2074
+ );
2075
+ }
2076
+ coverageFolder && server.middlewares.use(
2077
+ coveragePath,
2078
+ sirv(coverageFolder[0], {
2079
+ single: true,
2080
+ dev: true,
2081
+ setHeaders: (res) => {
2082
+ res.setHeader(
2083
+ "Cache-Control",
2084
+ "public,max-age=0,must-revalidate"
2085
+ );
2086
+ }
2087
+ })
2088
+ );
724
2089
  }
725
2090
  },
726
2091
  {
@@ -728,27 +2093,59 @@ var index = (project, base = "/") => {
728
2093
  enforce: "pre",
729
2094
  async config() {
730
2095
  const allTestFiles = await project.globTestFiles();
731
- const browserTestFiles = allTestFiles.filter((file) => getFilePoolName(project, file) === "browser");
2096
+ const browserTestFiles = allTestFiles.filter(
2097
+ (file) => getFilePoolName(project, file) === "browser"
2098
+ );
732
2099
  const setupFiles = toArray(project.config.setupFiles);
733
- const vitestPaths = [
2100
+ const define = {};
2101
+ for (const env in project.config.env || {}) {
2102
+ const stringValue = JSON.stringify(project.config.env[env]);
2103
+ define[`process.env.${env}`] = stringValue;
2104
+ define[`import.meta.env.${env}`] = stringValue;
2105
+ }
2106
+ const entries = [
2107
+ ...browserTestFiles,
2108
+ ...setupFiles,
734
2109
  resolve(distDir, "index.js"),
735
2110
  resolve(distDir, "browser.js"),
736
2111
  resolve(distDir, "runners.js"),
737
- resolve(distDir, "utils.js")
2112
+ resolve(distDir, "utils.js"),
2113
+ ...project.config.snapshotSerializers || []
738
2114
  ];
2115
+ if (project.config.diff) {
2116
+ entries.push(project.config.diff);
2117
+ }
2118
+ if (project.ctx.coverageProvider) {
2119
+ const coverage = project.ctx.config.coverage;
2120
+ const provider = coverage.provider;
2121
+ if (provider === "v8") {
2122
+ const path = tryResolve("@vitest/coverage-v8", [project.ctx.config.root]);
2123
+ if (path) {
2124
+ entries.push(path);
2125
+ }
2126
+ } else if (provider === "istanbul") {
2127
+ const path = tryResolve("@vitest/coverage-istanbul", [project.ctx.config.root]);
2128
+ if (path) {
2129
+ entries.push(path);
2130
+ }
2131
+ } else if (provider === "custom" && coverage.customProviderModule) {
2132
+ entries.push(coverage.customProviderModule);
2133
+ }
2134
+ }
739
2135
  return {
2136
+ define,
2137
+ resolve: {
2138
+ dedupe: ["vitest"]
2139
+ },
740
2140
  optimizeDeps: {
741
- entries: [
742
- ...browserTestFiles,
743
- ...setupFiles,
744
- ...vitestPaths
745
- ],
2141
+ entries,
746
2142
  exclude: [
747
2143
  "vitest",
748
2144
  "vitest/utils",
749
2145
  "vitest/browser",
750
2146
  "vitest/runners",
751
2147
  "@vitest/utils",
2148
+ "@vitest/utils/source-map",
752
2149
  "@vitest/runner",
753
2150
  "@vitest/spy",
754
2151
  "@vitest/utils/error",
@@ -757,7 +2154,9 @@ var index = (project, base = "/") => {
757
2154
  "std-env",
758
2155
  "tinybench",
759
2156
  "tinyspy",
760
- "pathe"
2157
+ "pathe",
2158
+ "msw",
2159
+ "msw/browser"
761
2160
  ],
762
2161
  include: [
763
2162
  "vitest > @vitest/utils > pretty-format",
@@ -768,8 +2167,9 @@ var index = (project, base = "/") => {
768
2167
  "vitest > pretty-format > ansi-regex",
769
2168
  "vitest > chai",
770
2169
  "vitest > chai > loupe",
771
- "vitest > @vitest/runner > p-limit",
2170
+ "vitest > @vitest/runner > pretty-format",
772
2171
  "vitest > @vitest/utils > diff-sequences",
2172
+ "vitest > @vitest/utils > loupe",
773
2173
  "@vitest/browser > @testing-library/user-event",
774
2174
  "@vitest/browser > @testing-library/dom"
775
2175
  ]
@@ -777,32 +2177,122 @@ var index = (project, base = "/") => {
777
2177
  };
778
2178
  },
779
2179
  async resolveId(id) {
780
- if (!/\?browserv=\w+$/.test(id))
2180
+ if (!/\?browserv=\w+$/.test(id)) {
781
2181
  return;
2182
+ }
782
2183
  let useId = id.slice(0, id.lastIndexOf("?"));
783
- if (useId.startsWith("/@fs/"))
2184
+ if (useId.startsWith("/@fs/")) {
784
2185
  useId = useId.slice(5);
785
- if (/^\w:/.test(useId))
2186
+ }
2187
+ if (/^\w:/.test(useId)) {
786
2188
  useId = useId.replace(/\\/g, "/");
2189
+ }
787
2190
  return useId;
788
2191
  }
789
2192
  },
790
- BrowserContext(project),
2193
+ {
2194
+ name: "vitest:browser:resolve-virtual",
2195
+ async resolveId(rawId) {
2196
+ if (rawId.startsWith("/__virtual_vitest__")) {
2197
+ const url = new URL(rawId, "http://localhost");
2198
+ if (!url.searchParams.has("id")) {
2199
+ return;
2200
+ }
2201
+ const id = decodeURIComponent(url.searchParams.get("id"));
2202
+ const resolved = await this.resolve(id, distRoot, {
2203
+ skipSelf: true
2204
+ });
2205
+ return resolved;
2206
+ }
2207
+ if (rawId === "/__vitest_msw__") {
2208
+ return this.resolve("msw/mockServiceWorker.js", distRoot, {
2209
+ skipSelf: true
2210
+ });
2211
+ }
2212
+ }
2213
+ },
2214
+ {
2215
+ name: "vitest:browser:assets",
2216
+ resolveId(id) {
2217
+ if (id.startsWith("/__vitest_browser__/") || id.startsWith("/__vitest__/")) {
2218
+ return resolve(distRoot, "client", id.slice(1));
2219
+ }
2220
+ },
2221
+ configResolved(config) {
2222
+ loupePath = resolve(config.cacheDir, "deps/loupe.js");
2223
+ },
2224
+ transform(code, id) {
2225
+ if (id.startsWith(loupePath)) {
2226
+ const utilRequire = "nodeUtil = require_util();";
2227
+ return code.replace(utilRequire, " ".repeat(utilRequire.length));
2228
+ }
2229
+ }
2230
+ },
2231
+ BrowserContext(browserServer),
791
2232
  DynamicImport(),
2233
+ {
2234
+ name: "vitest:browser:config",
2235
+ enforce: "post",
2236
+ async config(viteConfig) {
2237
+ // Enables using ignore hint for coverage providers with @preserve keyword
2238
+ viteConfig.esbuild ||= {};
2239
+ viteConfig.esbuild.legalComments = "inline";
2240
+ const server = resolveApiServerConfig(
2241
+ viteConfig.test?.browser || {},
2242
+ defaultBrowserPort
2243
+ ) || {
2244
+ port: defaultBrowserPort
2245
+ };
2246
+ server.middlewareMode = false;
2247
+ viteConfig.server = {
2248
+ ...viteConfig.server,
2249
+ ...server,
2250
+ open: false
2251
+ };
2252
+ viteConfig.server.fs ??= {};
2253
+ viteConfig.server.fs.allow = viteConfig.server.fs.allow || [];
2254
+ viteConfig.server.fs.allow.push(
2255
+ ...resolveFsAllow(
2256
+ project.ctx.config.root,
2257
+ project.ctx.server.config.configFile
2258
+ )
2259
+ );
2260
+ return {
2261
+ resolve: {
2262
+ alias: viteConfig.test?.alias
2263
+ }
2264
+ };
2265
+ }
2266
+ },
792
2267
  // TODO: remove this when @testing-library/vue supports ESM
793
2268
  {
794
- name: "vitest:browser:support-vue-testing-library",
2269
+ name: "vitest:browser:support-testing-library",
795
2270
  config() {
796
2271
  return {
2272
+ define: {
2273
+ // testing-library/preact
2274
+ "process.env.PTL_SKIP_AUTO_CLEANUP": !!process.env.PTL_SKIP_AUTO_CLEANUP,
2275
+ // testing-library/react
2276
+ "process.env.RTL_SKIP_AUTO_CLEANUP": !!process.env.RTL_SKIP_AUTO_CLEANUP,
2277
+ "process.env?.RTL_SKIP_AUTO_CLEANUP": !!process.env.RTL_SKIP_AUTO_CLEANUP,
2278
+ // testing-library/svelte, testing-library/solid
2279
+ "process.env.STL_SKIP_AUTO_CLEANUP": !!process.env.STL_SKIP_AUTO_CLEANUP,
2280
+ // testing-library/vue
2281
+ "process.env.VTL_SKIP_AUTO_CLEANUP": !!process.env.VTL_SKIP_AUTO_CLEANUP,
2282
+ // dom.debug()
2283
+ "process.env.DEBUG_PRINT_LIMIT": process.env.DEBUG_PRINT_LIMIT || 7e3
2284
+ },
797
2285
  optimizeDeps: {
798
2286
  esbuildOptions: {
799
2287
  plugins: [
800
2288
  {
801
2289
  name: "test-utils-rewrite",
802
2290
  setup(build) {
803
- const _require = createRequire(import.meta.url);
804
2291
  build.onResolve({ filter: /@vue\/test-utils/ }, (args) => {
805
- const resolved = _require.resolve(args.path, { paths: [args.importer] });
2292
+ const _require2 = getRequire();
2293
+ const resolved = _require2.resolve(args.path, {
2294
+ paths: [args.importer]
2295
+ });
806
2296
  return { path: resolved };
807
2297
  });
808
2298
  }
@@ -815,45 +2305,172 @@ var index = (project, base = "/") => {
815
2305
  }
816
2306
  ];
817
2307
  };
2308
+ function tryResolve(path, paths) {
2309
+ try {
2310
+ const _require2 = getRequire();
2311
+ return _require2.resolve(path, { paths });
2312
+ } catch {
2313
+ return void 0;
2314
+ }
2315
+ }
2316
+ let _require;
2317
+ function getRequire() {
2318
+ if (!_require) {
2319
+ _require = createRequire(import.meta.url);
2320
+ }
2321
+ return _require;
2322
+ }
818
2323
  function resolveCoverageFolder(project) {
819
2324
  const options = project.ctx.config;
820
- const htmlReporter = options.coverage?.enabled ? options.coverage.reporter.find((reporter) => {
821
- if (typeof reporter === "string")
2325
+ const htmlReporter = options.coverage?.enabled ? toArray(options.coverage.reporter).find((reporter) => {
2326
+ if (typeof reporter === "string") {
822
2327
  return reporter === "html";
2328
+ }
823
2329
  return reporter[0] === "html";
824
2330
  }) : void 0;
825
- if (!htmlReporter)
2331
+ if (!htmlReporter) {
826
2332
  return void 0;
2333
+ }
827
2334
  const root = resolve(
828
2335
  options.root || process.cwd(),
829
2336
  options.coverage.reportsDirectory || coverageConfigDefaults.reportsDirectory
830
2337
  );
831
2338
  const subdir = Array.isArray(htmlReporter) && htmlReporter.length > 1 && "subdir" in htmlReporter[1] ? htmlReporter[1].subdir : void 0;
832
- if (!subdir || typeof subdir !== "string")
2339
+ if (!subdir || typeof subdir !== "string") {
833
2340
  return [root, `/${basename(root)}/`];
2341
+ }
834
2342
  return [resolve(root, subdir), `/${basename(root)}/${subdir}/`];
835
2343
  }
836
- function wrapConfig(config) {
2344
+
2345
+ const debug = createDebugger("vitest:browser:pool");
2346
+ function createBrowserPool(ctx) {
2347
+ const providers = /* @__PURE__ */ new Set();
2348
+ const waitForTests = async (method, contextId, project, files) => {
2349
+ const context = project.browser.state.createAsyncContext(method, contextId, files);
2350
+ return await context;
2351
+ };
2352
+ const executeTests = async (method, project, files) => {
2353
+ ctx.state.clearFiles(project, files);
2354
+ const browser = project.browser;
2355
+ const threadsCount = getThreadsCount(project);
2356
+ const provider = browser.provider;
2357
+ providers.add(provider);
2358
+ const resolvedUrls = browser.vite.resolvedUrls;
2359
+ const origin = resolvedUrls?.local[0] ?? resolvedUrls?.network[0];
2360
+ if (!origin) {
2361
+ throw new Error(
2362
+ `Can't find browser origin URL for project "${project.getName()}" when running tests for files "${files.join('", "')}"`
2363
+ );
2364
+ }
2365
+ const filesPerThread = Math.ceil(files.length / threadsCount);
2366
+ const chunks = [];
2367
+ for (let i = 0; i < files.length; i += filesPerThread) {
2368
+ const chunk = files.slice(i, i + filesPerThread);
2369
+ chunks.push(chunk);
2370
+ }
2371
+ debug?.(
2372
+ `[%s] Running %s tests in %s chunks (%s threads)`,
2373
+ project.getName() || "core",
2374
+ files.length,
2375
+ chunks.length,
2376
+ threadsCount
2377
+ );
2378
+ const orchestrators = [...browser.state.orchestrators.entries()];
2379
+ const promises = [];
2380
+ chunks.forEach((files2, index) => {
2381
+ if (orchestrators[index]) {
2382
+ const [contextId, orchestrator] = orchestrators[index];
2383
+ debug?.(
2384
+ "Reusing orchestrator (context %s) for files: %s",
2385
+ contextId,
2386
+ [...files2.map((f) => relative(project.config.root, f))].join(", ")
2387
+ );
2388
+ const promise = waitForTests(method, contextId, project, files2);
2389
+ promises.push(promise);
2390
+ orchestrator.createTesters(files2);
2391
+ } else {
2392
+ const contextId = crypto.randomUUID();
2393
+ const waitPromise = waitForTests(method, contextId, project, files2);
2394
+ debug?.(
2395
+ "Opening a new context %s for files: %s",
2396
+ contextId,
2397
+ [...files2.map((f) => relative(project.config.root, f))].join(", ")
2398
+ );
2399
+ const url = new URL("/", origin);
2400
+ url.searchParams.set("contextId", contextId);
2401
+ const page = provider.openPage(contextId, url.toString()).then(() => waitPromise);
2402
+ promises.push(page);
2403
+ }
2404
+ });
2405
+ await Promise.all(promises);
2406
+ };
2407
+ const runWorkspaceTests = async (method, specs) => {
2408
+ const groupedFiles = /* @__PURE__ */ new Map();
2409
+ for (const [project, file] of specs) {
2410
+ const files = groupedFiles.get(project) || [];
2411
+ files.push(file);
2412
+ groupedFiles.set(project, files);
2413
+ }
2414
+ let isCancelled = false;
2415
+ ctx.onCancel(() => {
2416
+ isCancelled = true;
2417
+ });
2418
+ for (const [project, files] of groupedFiles.entries()) {
2419
+ if (isCancelled) {
2420
+ break;
2421
+ }
2422
+ await executeTests(method, project, files);
2423
+ }
2424
+ };
2425
+ const numCpus = typeof nodeos.availableParallelism === "function" ? nodeos.availableParallelism() : nodeos.cpus().length;
2426
+ function getThreadsCount(project) {
2427
+ const config = project.config.browser;
2428
+ if (!config.headless || !project.browser.provider.supportsParallelism) {
2429
+ return 1;
2430
+ }
2431
+ if (!config.fileParallelism) {
2432
+ return 1;
2433
+ }
2434
+ return ctx.config.watch ? Math.max(Math.floor(numCpus / 2), 1) : Math.max(numCpus - 1, 1);
2435
+ }
837
2436
  return {
838
- ...config,
839
- // workaround RegExp serialization
840
- testNamePattern: config.testNamePattern ? config.testNamePattern.toString() : void 0
2437
+ name: "browser",
2438
+ async close() {
2439
+ await Promise.all([...providers].map((provider) => provider.close()));
2440
+ providers.clear();
2441
+ },
2442
+ runTests: (files) => runWorkspaceTests("run", files),
2443
+ collectTests: (files) => runWorkspaceTests("collect", files)
841
2444
  };
842
2445
  }
843
- function replacer(code, values) {
844
- return code.replace(/\{\s*(\w+)\s*\}/g, (_, key) => values[key] ?? "");
845
- }
846
- async function formatScripts(scripts, server) {
847
- if (!scripts?.length)
848
- return "";
849
- const promises = scripts.map(async ({ content, src, async, id, type = "module" }, index) => {
850
- const srcLink = (src ? (await server.pluginContainer.resolveId(src))?.id : void 0) || src;
851
- const transformId = srcLink || join(server.config.root, `virtual__${id || `injected-${index}.js`}`);
852
- await server.moduleGraph.ensureEntryFromUrl(transformId);
853
- const contentProcessed = content && type === "module" ? (await server.pluginContainer.transform(content, transformId)).code : content;
854
- return `<script type="${type}"${async ? " async" : ""}${srcLink ? ` src="${slash(`/@fs/${srcLink}`)}"` : ""}>${contentProcessed || ""}<\/script>`;
2446
+
2447
+ async function createBrowserServer(project, configFile, prePlugins = [], postPlugins = []) {
2448
+ const server = new BrowserServer(project, "/");
2449
+ const root = project.config.root;
2450
+ await project.ctx.packageInstaller.ensureInstalled("@vitest/browser", root);
2451
+ const configPath = typeof configFile === "string" ? configFile : false;
2452
+ const vite = await createServer({
2453
+ ...project.options,
2454
+ // spread project config inlined in root workspace config
2455
+ base: "/",
2456
+ logLevel: process.env.VITEST_BROWSER_DEBUG ?? "info",
2457
+ mode: project.config.mode,
2458
+ configFile: configPath,
2459
+ // watch is handled by Vitest
2460
+ server: {
2461
+ hmr: false,
2462
+ watch: null
2463
+ },
2464
+ plugins: [
2465
+ ...prePlugins,
2466
+ ...project.options?.plugins || [],
2467
+ BrowserPlugin(server),
2468
+ ...postPlugins
2469
+ ]
855
2470
  });
856
- return (await Promise.all(promises)).join("\n");
2471
+ await vite.listen();
2472
+ setupBrowserRpc(server);
2473
+ return server;
857
2474
  }
858
2475
 
859
- export { index as default };
2476
+ export { createBrowserPool, createBrowserServer };