@vitest/browser 2.0.0-beta.2 → 2.0.0-beta.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -17,8 +17,8 @@
17
17
  })()
18
18
  </script>
19
19
  <!-- !LOAD_METADATA! -->
20
- <script type="module" crossorigin src="./assets/index-D1SnFeL5.js"></script>
21
- <link rel="stylesheet" crossorigin href="./assets/index-D0Wp7rbG.css">
20
+ <script type="module" crossorigin src="./assets/index-DBVixaI-.js"></script>
21
+ <link rel="stylesheet" crossorigin href="./assets/index-C576npev.css">
22
22
  </head>
23
23
  <body>
24
24
  <div id="app"></div>
@@ -1,4 +1,4 @@
1
- import { c as client, g as getConfig, a as getBrowserState, b as channel, r as rpcDone } from "./rpc-By4jD8av.js";
1
+ import { c as client, g as getConfig, a as getBrowserState, b as channel, r as rpcDone } from "./rpc-Hw6RY18C.js";
2
2
  const url = new URL(location.href);
3
3
  const ID_ALL = "__vitest_all__";
4
4
  const iframes = /* @__PURE__ */ new Map();
@@ -338,6 +338,33 @@ const relative = function(from, to) {
338
338
  }
339
339
  return [..._from.map(() => ".."), ..._to].join("/");
340
340
  };
341
+ function generateHash(str) {
342
+ let hash = 0;
343
+ if (str.length === 0)
344
+ return `${hash}`;
345
+ for (let i = 0; i < str.length; i++) {
346
+ const char = str.charCodeAt(i);
347
+ hash = (hash << 5) - hash + char;
348
+ hash = hash & hash;
349
+ }
350
+ return `${hash}`;
351
+ }
352
+ function createFileTask(filepath, root, projectName) {
353
+ const path = relative(root, filepath);
354
+ const file = {
355
+ id: generateHash(`${path}${projectName || ""}`),
356
+ name: path,
357
+ type: "suite",
358
+ mode: "run",
359
+ filepath,
360
+ tasks: [],
361
+ meta: /* @__PURE__ */ Object.create(null),
362
+ projectName,
363
+ file: void 0
364
+ };
365
+ file.file = file;
366
+ return file;
367
+ }
341
368
  function isAggregateError(err) {
342
369
  if (typeof AggregateError !== "undefined" && err instanceof AggregateError)
343
370
  return true;
@@ -409,6 +436,9 @@ class StateManager {
409
436
  files.forEach((file) => {
410
437
  const existing = this.filesMap.get(file.filepath) || [];
411
438
  const otherProject = existing.filter((i) => i.projectName !== file.projectName);
439
+ const currentFile = existing.find((i) => i.projectName === file.projectName);
440
+ if (currentFile)
441
+ file.logs = currentFile.logs;
412
442
  otherProject.push(file);
413
443
  this.filesMap.set(file.filepath, otherProject);
414
444
  this.updateId(file);
@@ -419,13 +449,21 @@ class StateManager {
419
449
  const project = _project;
420
450
  paths.forEach((path) => {
421
451
  const files = this.filesMap.get(path);
422
- if (!files)
452
+ const fileTask = createFileTask(path, project.config.root, project.config.name);
453
+ this.idMap.set(fileTask.id, fileTask);
454
+ if (!files) {
455
+ this.filesMap.set(path, [fileTask]);
423
456
  return;
457
+ }
424
458
  const filtered = files.filter((file) => file.projectName !== project.config.name);
425
- if (!filtered.length)
426
- this.filesMap.delete(path);
427
- else
428
- this.filesMap.set(path, filtered);
459
+ if (!filtered.length) {
460
+ this.filesMap.set(path, [fileTask]);
461
+ } else {
462
+ this.filesMap.set(path, [
463
+ ...filtered,
464
+ fileTask
465
+ ]);
466
+ }
429
467
  });
430
468
  }
431
469
  updateId(task) {
@@ -464,25 +502,7 @@ class StateManager {
464
502
  }).length;
465
503
  }
466
504
  cancelFiles(files, root, projectName) {
467
- this.collectFiles(files.map((filepath) => {
468
- const file = {
469
- filepath,
470
- name: relative(root, filepath),
471
- id: filepath,
472
- mode: "skip",
473
- type: "suite",
474
- result: {
475
- state: "skip"
476
- },
477
- meta: {},
478
- // Cancelled files have not yet collected tests
479
- tasks: [],
480
- projectName,
481
- file: null
482
- };
483
- file.file = file;
484
- return file;
485
- }));
505
+ this.collectFiles(files.map((filepath) => createFileTask(filepath, root, projectName)));
486
506
  }
487
507
  }
488
508
  function createClient(url, options = {}) {
@@ -506,6 +526,13 @@ function createClient(url, options = {}) {
506
526
  ctx.state.idMap = reactive(ctx.state.idMap);
507
527
  let onMessage;
508
528
  const functions = {
529
+ onSpecsCollected(specs) {
530
+ var _a;
531
+ specs == null ? void 0 : specs.forEach(([config, file]) => {
532
+ ctx.state.clearFiles({ config }, [file]);
533
+ });
534
+ (_a = handlers.onSpecsCollected) == null ? void 0 : _a.call(handlers, specs);
535
+ },
509
536
  onPathsCollected(paths) {
510
537
  var _a;
511
538
  ctx.state.collectPaths(paths);
@@ -522,7 +549,9 @@ function createClient(url, options = {}) {
522
549
  (_a = handlers.onTaskUpdate) == null ? void 0 : _a.call(handlers, packs);
523
550
  },
524
551
  onUserConsoleLog(log) {
552
+ var _a;
525
553
  ctx.state.updateUserLog(log);
554
+ (_a = handlers.onUserConsoleLog) == null ? void 0 : _a.call(handlers, log);
526
555
  },
527
556
  onFinished(files, errors) {
528
557
  var _a;
@@ -10,7 +10,7 @@ var __publicField = (obj, key, value) => {
10
10
  __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value);
11
11
  return value;
12
12
  };
13
- import { i as importId, d as rpc$1, g as getConfig, _ as __vitePreload, b as channel, a as getBrowserState, c as client, l as loadSafeRpc, o as onCancel } from "./rpc-By4jD8av.js";
13
+ import { i as importId, d as rpc$1, g as getConfig, _ as __vitePreload, b as channel, a as getBrowserState, c as client, l as loadSafeRpc, o as onCancel } from "./rpc-Hw6RY18C.js";
14
14
  function showPopupWarning(name, value, defaultValue) {
15
15
  return (...params) => {
16
16
  const formatedParams = params.map((p) => JSON.stringify(p)).join(", ");
@@ -23,11 +23,11 @@
23
23
  </style>
24
24
  <script>{__VITEST_INJECTOR__}</script>
25
25
  {__VITEST_SCRIPTS__}
26
- <script type="module" crossorigin src="/__vitest_browser__/main-DmAU-Uff.js"></script>
27
- <link rel="modulepreload" crossorigin href="/__vitest_browser__/rpc-By4jD8av.js">
26
+ <script type="module" crossorigin src="/__vitest_browser__/main-uRgy0zc4.js"></script>
27
+ <link rel="modulepreload" crossorigin href="/__vitest_browser__/rpc-Hw6RY18C.js">
28
28
  </head>
29
29
  <body>
30
- <iframe id="vitest-ui" src=""></iframe>
30
+ <iframe id="vitest-ui" src="/__vitest__/"></iframe>
31
31
  <div id="vitest-tester"></div>
32
32
  </body>
33
33
  </html>
@@ -17,8 +17,8 @@
17
17
  </style>
18
18
  <script>{__VITEST_INJECTOR__}</script>
19
19
  {__VITEST_SCRIPTS__}
20
- <script type="module" crossorigin src="/__vitest_browser__/tester-CrKhlp5g.js"></script>
21
- <link rel="modulepreload" crossorigin href="/__vitest_browser__/rpc-By4jD8av.js">
20
+ <script type="module" crossorigin src="/__vitest_browser__/tester-Ly7RLJnN.js"></script>
21
+ <link rel="modulepreload" crossorigin href="/__vitest_browser__/rpc-Hw6RY18C.js">
22
22
  </head>
23
23
  <body>
24
24
  {__VITEST_APPEND__}
package/dist/index.d.ts CHANGED
@@ -1,5 +1,6 @@
1
- import { Plugin } from 'vite';
2
1
  import { WorkspaceProject } from 'vitest/node';
2
+ export { BrowserCommand } from 'vitest/node';
3
+ import { Plugin } from 'vitest/config';
3
4
 
4
5
  declare const _default: (project: WorkspaceProject, base?: string) => Plugin[];
5
6
 
package/dist/index.js CHANGED
@@ -1,10 +1,13 @@
1
1
  import { fileURLToPath } from 'node:url';
2
- import { readFile } from 'node:fs/promises';
2
+ import { readFile as readFile$1 } from 'node:fs/promises';
3
3
  import sirv from 'sirv';
4
4
  import { coverageConfigDefaults } from 'vitest/config';
5
5
  import { slash } from '@vitest/utils';
6
6
  import MagicString from 'magic-string';
7
7
  import { esmWalker } from '@vitest/utils/ast';
8
+ import fs, { promises } from 'node:fs';
9
+ import { resolve as resolve$1, dirname } from 'node:path';
10
+ import { isFileServingAllowed } from 'vitest/node';
8
11
 
9
12
  const _DRIVE_LETTER_START_RE = /^[A-Za-z]:\//;
10
13
  function normalizeWindowsPath(input = "") {
@@ -398,6 +401,154 @@ export { ${viInjectedKey} }`);
398
401
  };
399
402
  }
400
403
 
404
+ function assertFileAccess(path, project) {
405
+ if (!isFileServingAllowed(path, project.server) && !isFileServingAllowed(path, project.ctx.server))
406
+ throw new Error(`Access denied to "${path}". See Vite config documentation for "server.fs": https://vitejs.dev/config/server-options.html#server-fs-strict.`);
407
+ }
408
+ const readFile = async ({ project, testPath = process.cwd() }, path, options = {}) => {
409
+ const filepath = resolve$1(dirname(testPath), path);
410
+ assertFileAccess(filepath, project);
411
+ if (typeof options === "object" && !options.encoding)
412
+ options.encoding = "utf-8";
413
+ return promises.readFile(filepath, options);
414
+ };
415
+ const writeFile = async ({ project, testPath = process.cwd() }, path, data, options) => {
416
+ const filepath = resolve$1(dirname(testPath), path);
417
+ assertFileAccess(filepath, project);
418
+ const dir = dirname(filepath);
419
+ if (!fs.existsSync(dir))
420
+ await promises.mkdir(dir, { recursive: true });
421
+ await promises.writeFile(filepath, data, options);
422
+ };
423
+ const removeFile = async ({ project, testPath = process.cwd() }, path) => {
424
+ const filepath = resolve$1(dirname(testPath), path);
425
+ assertFileAccess(filepath, project);
426
+ await promises.rm(filepath);
427
+ };
428
+
429
+ function isObject(payload) {
430
+ return payload != null && typeof payload === "object";
431
+ }
432
+ function isSendKeysPayload(payload) {
433
+ const validOptions = ["type", "press", "down", "up"];
434
+ if (!isObject(payload))
435
+ throw new Error("You must provide a `SendKeysPayload` object");
436
+ const numberOfValidOptions = Object.keys(payload).filter(
437
+ (key) => validOptions.includes(key)
438
+ ).length;
439
+ const unknownOptions = Object.keys(payload).filter((key) => !validOptions.includes(key));
440
+ if (numberOfValidOptions > 1) {
441
+ throw new Error(
442
+ `You must provide ONLY one of the following properties to pass to the browser runner: ${validOptions.join(
443
+ ", "
444
+ )}.`
445
+ );
446
+ }
447
+ if (numberOfValidOptions === 0) {
448
+ throw new Error(
449
+ `You must provide one of the following properties to pass to the browser runner: ${validOptions.join(
450
+ ", "
451
+ )}.`
452
+ );
453
+ }
454
+ if (unknownOptions.length > 0)
455
+ throw new Error(`Unknown options \`${unknownOptions.join(", ")}\` present.`);
456
+ return true;
457
+ }
458
+ function isTypePayload(payload) {
459
+ return "type" in payload;
460
+ }
461
+ function isPressPayload(payload) {
462
+ return "press" in payload;
463
+ }
464
+ function isDownPayload(payload) {
465
+ return "down" in payload;
466
+ }
467
+ function isUpPayload(payload) {
468
+ return "up" in payload;
469
+ }
470
+ const sendKeys = async ({ provider }, payload) => {
471
+ if (!isSendKeysPayload(payload) || !payload)
472
+ throw new Error("You must provide a `SendKeysPayload` object");
473
+ if (provider.name === "playwright") {
474
+ const page = provider.page;
475
+ if (isTypePayload(payload))
476
+ await page.keyboard.type(payload.type);
477
+ else if (isPressPayload(payload))
478
+ await page.keyboard.press(payload.press);
479
+ else if (isDownPayload(payload))
480
+ await page.keyboard.down(payload.down);
481
+ else if (isUpPayload(payload))
482
+ await page.keyboard.up(payload.up);
483
+ } else if (provider.name === "webdriverio") {
484
+ const browser = provider.browser;
485
+ if (isTypePayload(payload))
486
+ await browser.keys(payload.type.split(""));
487
+ else if (isPressPayload(payload))
488
+ await browser.keys([payload.press]);
489
+ else
490
+ throw new Error('Only "press" and "type" are supported by webdriverio.');
491
+ } else {
492
+ throw new Error(`"sendKeys" is not supported for ${provider.name} browser provider.`);
493
+ }
494
+ };
495
+
496
+ var builtinCommands = {
497
+ readFile,
498
+ removeFile,
499
+ writeFile,
500
+ sendKeys
501
+ };
502
+
503
+ const VIRTUAL_ID_CONTEXT = "\0@vitest/browser/context";
504
+ const ID_CONTEXT = "@vitest/browser/context";
505
+ function BrowserContext(project) {
506
+ project.config.browser.commands ??= {};
507
+ for (const [name, command] of Object.entries(builtinCommands))
508
+ project.config.browser.commands[name] ??= command;
509
+ for (const command in project.config.browser.commands) {
510
+ if (!/^[a-z_$][\w$]*$/i.test(command))
511
+ throw new Error(`Invalid command name "${command}". Only alphanumeric characters, $ and _ are allowed.`);
512
+ }
513
+ return {
514
+ name: "vitest:browser:virtual-module:context",
515
+ enforce: "pre",
516
+ resolveId(id) {
517
+ if (id === ID_CONTEXT)
518
+ return VIRTUAL_ID_CONTEXT;
519
+ },
520
+ load(id) {
521
+ if (id === VIRTUAL_ID_CONTEXT)
522
+ return generateContextFile(project);
523
+ }
524
+ };
525
+ }
526
+ function generateContextFile(project) {
527
+ const commands = Object.keys(project.config.browser.commands ?? {});
528
+ const filepathCode = "__vitest_worker__.filepath || __vitest_worker__.current?.file?.filepath || undefined";
529
+ const commandsCode = commands.map((command) => {
530
+ return ` ["${command}"]: (...args) => rpc().triggerCommand("${command}", ${filepathCode}, args),`;
531
+ }).join("\n");
532
+ return `
533
+ const rpc = () => __vitest_worker__.rpc
534
+
535
+ export const server = {
536
+ platform: ${JSON.stringify(process.platform)},
537
+ version: ${JSON.stringify(process.version)},
538
+ provider: ${JSON.stringify(project.browserProvider.name)},
539
+ commands: {
540
+ ${commandsCode}
541
+ }
542
+ }
543
+ export const commands = server.commands
544
+ export const page = {
545
+ get config() {
546
+ return __vitest_browser_runner__.config
547
+ }
548
+ }
549
+ `;
550
+ }
551
+
401
552
  var index = (project, base = "/") => {
402
553
  const pkgRoot = resolve(fileURLToPath(import.meta.url), "../..");
403
554
  const distRoot = resolve(pkgRoot, "dist");
@@ -413,9 +564,9 @@ var index = (project, base = "/") => {
413
564
  }
414
565
  },
415
566
  async configureServer(server) {
416
- const testerHtml = readFile(resolve(distRoot, "client/tester.html"), "utf8");
417
- const runnerHtml = readFile(resolve(distRoot, "client/index.html"), "utf8");
418
- const injectorJs = readFile(resolve(distRoot, "client/esm-client-injector.js"), "utf8");
567
+ const testerHtml = readFile$1(resolve(distRoot, "client/tester.html"), "utf8");
568
+ const runnerHtml = readFile$1(resolve(distRoot, "client/index.html"), "utf8");
569
+ const injectorJs = readFile$1(resolve(distRoot, "client/esm-client-injector.js"), "utf8");
419
570
  const favicon = `${base}favicon.svg`;
420
571
  const testerPrefix = `${base}__vitest_test__/__test__/`;
421
572
  server.middlewares.use((_req, res, next) => {
@@ -562,6 +713,7 @@ export default globalThis.loupe`;
562
713
  return useId;
563
714
  }
564
715
  },
716
+ BrowserContext(project),
565
717
  {
566
718
  name: "vitest:browser:esm-injector",
567
719
  enforce: "post",
@@ -600,7 +752,7 @@ function wrapConfig(config) {
600
752
  };
601
753
  }
602
754
  function replacer(code, values) {
603
- return code.replace(/{\s*(\w+)\s*}/g, (_, key) => values[key] ?? "");
755
+ return code.replace(/\{\s*(\w+)\s*\}/g, (_, key) => values[key] ?? "");
604
756
  }
605
757
  async function formatScripts(scripts, server) {
606
758
  if (!scripts?.length)
package/dist/providers.js CHANGED
@@ -1,9 +1,9 @@
1
1
  const playwrightBrowsers = ["firefox", "webkit", "chromium"];
2
2
  class PlaywrightBrowserProvider {
3
3
  name = "playwright";
4
- cachedBrowser = null;
5
- cachedPage = null;
6
- browser;
4
+ browser = null;
5
+ page = null;
6
+ browserName;
7
7
  ctx;
8
8
  options;
9
9
  getSupportedBrowsers() {
@@ -11,31 +11,31 @@ class PlaywrightBrowserProvider {
11
11
  }
12
12
  initialize(project, { browser, options }) {
13
13
  this.ctx = project;
14
- this.browser = browser;
14
+ this.browserName = browser;
15
15
  this.options = options;
16
16
  }
17
17
  async openBrowserPage() {
18
- if (this.cachedPage)
19
- return this.cachedPage;
18
+ if (this.page)
19
+ return this.page;
20
20
  const options = this.ctx.config.browser;
21
21
  const playwright = await import('playwright');
22
- const browser = await playwright[this.browser].launch({
22
+ const browser = await playwright[this.browserName].launch({
23
23
  ...this.options?.launch,
24
24
  headless: options.headless
25
25
  });
26
- this.cachedBrowser = browser;
27
- this.cachedPage = await browser.newPage(this.options?.page);
28
- return this.cachedPage;
26
+ this.browser = browser;
27
+ this.page = await browser.newPage(this.options?.page);
28
+ return this.page;
29
29
  }
30
30
  async openPage(url) {
31
31
  const browserPage = await this.openBrowserPage();
32
32
  await browserPage.goto(url);
33
33
  }
34
34
  async close() {
35
- const page = this.cachedPage;
36
- this.cachedPage = null;
37
- const browser = this.cachedBrowser;
38
- this.cachedBrowser = null;
35
+ const page = this.page;
36
+ this.page = null;
37
+ const browser = this.browser;
38
+ this.browser = null;
39
39
  await page?.close();
40
40
  await browser?.close();
41
41
  }
@@ -44,8 +44,8 @@ class PlaywrightBrowserProvider {
44
44
  const webdriverBrowsers = ["firefox", "chrome", "edge", "safari"];
45
45
  class WebdriverBrowserProvider {
46
46
  name = "webdriverio";
47
- cachedBrowser = null;
48
- browser;
47
+ browser = null;
48
+ browserName;
49
49
  ctx;
50
50
  options;
51
51
  getSupportedBrowsers() {
@@ -53,29 +53,29 @@ class WebdriverBrowserProvider {
53
53
  }
54
54
  async initialize(ctx, { browser, options }) {
55
55
  this.ctx = ctx;
56
- this.browser = browser;
56
+ this.browserName = browser;
57
57
  this.options = options;
58
58
  }
59
59
  async openBrowser() {
60
- if (this.cachedBrowser)
61
- return this.cachedBrowser;
60
+ if (this.browser)
61
+ return this.browser;
62
62
  const options = this.ctx.config.browser;
63
- if (this.browser === "safari") {
63
+ if (this.browserName === "safari") {
64
64
  if (options.headless)
65
65
  throw new Error("You've enabled headless mode for Safari but it doesn't currently support it.");
66
66
  }
67
67
  const { remote } = await import('webdriverio');
68
- this.cachedBrowser = await remote({
68
+ this.browser = await remote({
69
69
  ...this.options,
70
70
  logLevel: "error",
71
71
  capabilities: this.buildCapabilities()
72
72
  });
73
- return this.cachedBrowser;
73
+ return this.browser;
74
74
  }
75
75
  buildCapabilities() {
76
76
  const capabilities = {
77
77
  ...this.options?.capabilities,
78
- browserName: this.browser
78
+ browserName: this.browserName
79
79
  };
80
80
  const headlessMap = {
81
81
  chrome: ["goog:chromeOptions", ["headless", "disable-gpu"]],
@@ -83,7 +83,7 @@ class WebdriverBrowserProvider {
83
83
  edge: ["ms:edgeOptions", ["--headless"]]
84
84
  };
85
85
  const options = this.ctx.config.browser;
86
- const browser = this.browser;
86
+ const browser = this.browserName;
87
87
  if (browser !== "safari" && options.headless) {
88
88
  const [key, args] = headlessMap[browser];
89
89
  const currentValues = this.options?.capabilities?.[key] || {};
@@ -98,7 +98,7 @@ class WebdriverBrowserProvider {
98
98
  }
99
99
  async close() {
100
100
  await Promise.all([
101
- this.cachedBrowser?.sessionId ? this.cachedBrowser?.deleteSession?.() : null
101
+ this.browser?.sessionId ? this.browser?.deleteSession?.() : null
102
102
  ]);
103
103
  process.exit();
104
104
  }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@vitest/browser",
3
3
  "type": "module",
4
- "version": "2.0.0-beta.2",
4
+ "version": "2.0.0-beta.3",
5
5
  "description": "Browser running for Vitest",
6
6
  "license": "MIT",
7
7
  "funding": "https://opencollective.com/vitest",
@@ -24,6 +24,10 @@
24
24
  "types": "./providers.d.ts",
25
25
  "default": "./dist/providers.js"
26
26
  },
27
+ "./context": {
28
+ "types": "./context.d.ts",
29
+ "default": "./context.js"
30
+ },
27
31
  "./providers/webdriverio": {
28
32
  "types": "./providers/webdriverio.d.ts"
29
33
  },
@@ -43,7 +47,7 @@
43
47
  "peerDependencies": {
44
48
  "playwright": "*",
45
49
  "webdriverio": "*",
46
- "vitest": "2.0.0-beta.2"
50
+ "vitest": "2.0.0-beta.3"
47
51
  },
48
52
  "peerDependenciesMeta": {
49
53
  "playwright": {
@@ -59,7 +63,7 @@
59
63
  "dependencies": {
60
64
  "magic-string": "^0.30.10",
61
65
  "sirv": "^2.0.4",
62
- "@vitest/utils": "2.0.0-beta.2"
66
+ "@vitest/utils": "2.0.0-beta.3"
63
67
  },
64
68
  "devDependencies": {
65
69
  "@types/ws": "^8.5.10",
@@ -69,10 +73,10 @@
69
73
  "playwright-core": "^1.44.0",
70
74
  "safaridriver": "^0.1.2",
71
75
  "webdriverio": "^8.36.1",
72
- "@vitest/ui": "2.0.0-beta.2",
73
- "@vitest/ws-client": "2.0.0-beta.2",
74
- "@vitest/runner": "2.0.0-beta.2",
75
- "vitest": "2.0.0-beta.2"
76
+ "@vitest/runner": "2.0.0-beta.3",
77
+ "@vitest/ui": "2.0.0-beta.3",
78
+ "@vitest/ws-client": "2.0.0-beta.3",
79
+ "vitest": "2.0.0-beta.3"
76
80
  },
77
81
  "scripts": {
78
82
  "build": "rimraf dist && pnpm build:node && pnpm build:client",