@vitest/browser 3.2.0-beta.2 → 3.2.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.
- package/context.d.ts +1 -1
- package/dist/client/.vite/manifest.json +2 -2
- package/dist/client/__vitest__/assets/index-C03hLyIS.js +52 -0
- package/dist/client/__vitest__/assets/{index-DJfrXR3P.css → index-CVSF215x.css} +1 -1
- package/dist/client/__vitest__/index.html +2 -2
- package/dist/client/__vitest_browser__/{orchestrator-CuTjqoE1.js → orchestrator-Bsc_nLaw.js} +6 -2
- package/dist/client/__vitest_browser__/{tester-CrBz0KXk.js → tester-BV-CSJ5v.js} +6 -1
- package/dist/client/orchestrator.html +1 -1
- package/dist/client/tester/tester.html +1 -1
- package/dist/client.js +5 -0
- package/dist/context.js +18 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +108 -10
- package/dist/locators/index.d.ts +1 -0
- package/dist/providers.js +2 -1
- package/dist/state.js +3 -0
- package/dist/{webdriver-D7k26Na7.js → webdriver-B1QbgqhC.js} +52 -13
- package/package.json +12 -12
- package/dist/client/__vitest__/assets/index-BZjudRZr.js +0 -52
package/dist/index.js
CHANGED
|
@@ -13,12 +13,12 @@ import { fileURLToPath } from 'node:url';
|
|
|
13
13
|
import crypto from 'node:crypto';
|
|
14
14
|
import { mkdir, rm, readFile as readFile$1 } from 'node:fs/promises';
|
|
15
15
|
import { parseErrorStacktrace, parseStacktrace } from '@vitest/utils/source-map';
|
|
16
|
-
import { P as PlaywrightBrowserProvider, W as WebdriverBrowserProvider } from './webdriver-
|
|
16
|
+
import { P as PlaywrightBrowserProvider, W as WebdriverBrowserProvider } from './webdriver-B1QbgqhC.js';
|
|
17
17
|
import { resolve as resolve$1, basename as basename$1, dirname as dirname$1, normalize as normalize$1 } from 'node:path';
|
|
18
18
|
import { WebSocketServer } from 'ws';
|
|
19
19
|
import * as nodeos from 'node:os';
|
|
20
20
|
|
|
21
|
-
var version = "3.2.0-beta.
|
|
21
|
+
var version = "3.2.0-beta.3";
|
|
22
22
|
|
|
23
23
|
const _DRIVE_LETTER_START_RE = /^[A-Za-z]:\//;
|
|
24
24
|
function normalizeWindowsPath(input = "") {
|
|
@@ -351,15 +351,19 @@ function slash(path) {
|
|
|
351
351
|
|
|
352
352
|
async function resolveOrchestrator(globalServer, url, res) {
|
|
353
353
|
let sessionId = url.searchParams.get("sessionId");
|
|
354
|
+
// it's possible to open the page without a context
|
|
354
355
|
if (!sessionId) {
|
|
355
356
|
const contexts = [...globalServer.children].flatMap((p) => [...p.state.orchestrators.keys()]);
|
|
356
357
|
sessionId = contexts[contexts.length - 1] ?? "none";
|
|
357
358
|
}
|
|
359
|
+
// it's ok to not have a session here, especially in the preview provider
|
|
360
|
+
// because the user could refresh the page which would remove the session id from the url
|
|
358
361
|
const session = globalServer.vitest._browserSessions.getSession(sessionId);
|
|
359
362
|
const browserProject = session?.project.browser || [...globalServer.children][0];
|
|
360
363
|
if (!browserProject) {
|
|
361
364
|
return;
|
|
362
365
|
}
|
|
366
|
+
// ignore uknown pages
|
|
363
367
|
if (sessionId && sessionId !== "none" && !globalServer.vitest._browserSessions.sessionIds.has(sessionId)) {
|
|
364
368
|
return;
|
|
365
369
|
}
|
|
@@ -375,6 +379,7 @@ async function resolveOrchestrator(globalServer, url, res) {
|
|
|
375
379
|
__VITEST_PROVIDED_CONTEXT__: JSON.stringify(stringify(browserProject.project.getProvidedContext())),
|
|
376
380
|
__VITEST_API_TOKEN__: JSON.stringify(globalServer.vitest.config.api.token)
|
|
377
381
|
});
|
|
382
|
+
// disable CSP for the orchestrator as we are the ones controlling it
|
|
378
383
|
res.removeHeader("Content-Security-Policy");
|
|
379
384
|
if (!globalServer.orchestratorScripts) {
|
|
380
385
|
globalServer.orchestratorScripts = (await globalServer.formatScripts(globalServer.config.browser.orchestratorScripts)).map((script) => {
|
|
@@ -387,6 +392,7 @@ async function resolveOrchestrator(globalServer, url, res) {
|
|
|
387
392
|
}).join("\n");
|
|
388
393
|
}
|
|
389
394
|
let baseHtml = typeof globalServer.orchestratorHtml === "string" ? globalServer.orchestratorHtml : await globalServer.orchestratorHtml;
|
|
395
|
+
// if UI is enabled, use UI HTML and inject the orchestrator script
|
|
390
396
|
if (globalServer.config.browser.ui) {
|
|
391
397
|
const manifestContent = globalServer.manifest instanceof Promise ? await globalServer.manifest : globalServer.manifest;
|
|
392
398
|
const jsEntry = manifestContent["orchestrator.html"].file;
|
|
@@ -413,6 +419,7 @@ function disableCache(res) {
|
|
|
413
419
|
res.setHeader("Content-Type", "text/html; charset=utf-8");
|
|
414
420
|
}
|
|
415
421
|
function allowIframes(res) {
|
|
422
|
+
// remove custom iframe related headers to allow the iframe to load
|
|
416
423
|
res.removeHeader("X-Frame-Options");
|
|
417
424
|
}
|
|
418
425
|
|
|
@@ -438,6 +445,8 @@ function createOrchestratorMiddleware(parentServer) {
|
|
|
438
445
|
async function resolveTester(globalServer, url, res, next) {
|
|
439
446
|
const csp = res.getHeader("Content-Security-Policy");
|
|
440
447
|
if (typeof csp === "string") {
|
|
448
|
+
// add frame-ancestors to allow the iframe to be loaded by Vitest,
|
|
449
|
+
// but keep the rest of the CSP
|
|
441
450
|
res.setHeader("Content-Security-Policy", csp.replace(/frame-ancestors [^;]+/, "frame-ancestors *"));
|
|
442
451
|
}
|
|
443
452
|
const sessionId = url.searchParams.get("sessionId") || "none";
|
|
@@ -572,6 +581,7 @@ var BrowserPlugin = (parentServer, base = "/") => {
|
|
|
572
581
|
name: "vitest:browser",
|
|
573
582
|
async configureServer(server) {
|
|
574
583
|
parentServer.setServer(server);
|
|
584
|
+
// eslint-disable-next-line prefer-arrow-callback
|
|
575
585
|
server.middlewares.use(function vitestHeaders(_req, res, next) {
|
|
576
586
|
const headers = server.config.server.headers;
|
|
577
587
|
if (headers) {
|
|
@@ -604,6 +614,7 @@ var BrowserPlugin = (parentServer, base = "/") => {
|
|
|
604
614
|
}
|
|
605
615
|
const uiEnabled = parentServer.config.browser.ui;
|
|
606
616
|
if (uiEnabled) {
|
|
617
|
+
// eslint-disable-next-line prefer-arrow-callback
|
|
607
618
|
server.middlewares.use(`${base}__screenshot-error`, function vitestBrowserScreenshotError(req, res) {
|
|
608
619
|
if (!req.url) {
|
|
609
620
|
res.statusCode = 404;
|
|
@@ -642,6 +653,11 @@ var BrowserPlugin = (parentServer, base = "/") => {
|
|
|
642
653
|
});
|
|
643
654
|
}
|
|
644
655
|
server.middlewares.use((req, res, next) => {
|
|
656
|
+
// 9000 mega head move
|
|
657
|
+
// Vite always caches optimized dependencies, but users might mock
|
|
658
|
+
// them in _some_ tests, while keeping original modules in others
|
|
659
|
+
// there is no way to configure that in Vite, so we patch it here
|
|
660
|
+
// to always ignore the cache-control set by Vite in the next middleware
|
|
645
661
|
if (req.url && versionRegexp.test(req.url) && !req.url.includes("chunk-")) {
|
|
646
662
|
res.setHeader("Cache-Control", "no-cache");
|
|
647
663
|
const setHeader = res.setHeader.bind(res);
|
|
@@ -660,10 +676,13 @@ var BrowserPlugin = (parentServer, base = "/") => {
|
|
|
660
676
|
name: "vitest:browser:tests",
|
|
661
677
|
enforce: "pre",
|
|
662
678
|
async config() {
|
|
679
|
+
// this plugin can be used in different projects, but all of them
|
|
680
|
+
// have the same `include` pattern, so it doesn't matter which project we use
|
|
663
681
|
const project = parentServer.project;
|
|
664
682
|
const { testFiles: allTestFiles } = await project.globTestFiles();
|
|
665
683
|
const browserTestFiles = allTestFiles.filter((file) => getFilePoolName(project, file) === "browser");
|
|
666
684
|
const setupFiles = toArray(project.config.setupFiles);
|
|
685
|
+
// replace env values - cannot be reassign at runtime
|
|
667
686
|
const define = {};
|
|
668
687
|
for (const env in project.config.env || {}) {
|
|
669
688
|
const stringValue = JSON.stringify(project.config.env[env]);
|
|
@@ -735,8 +754,10 @@ var BrowserPlugin = (parentServer, base = "/") => {
|
|
|
735
754
|
if (svelte) {
|
|
736
755
|
exclude.push("vitest-browser-svelte");
|
|
737
756
|
}
|
|
757
|
+
// since we override the resolution in the esbuild plugin, Vite can no longer optimizer it
|
|
738
758
|
const vue = isPackageExists("vitest-browser-vue", fileRoot);
|
|
739
759
|
if (vue) {
|
|
760
|
+
// we override them in the esbuild plugin so optimizer can no longer intercept it
|
|
740
761
|
include.push("vitest-browser-vue", "vitest-browser-vue > @vue/test-utils", "vitest-browser-vue > @vue/test-utils > @vue/compiler-core");
|
|
741
762
|
}
|
|
742
763
|
const vueTestUtils = isPackageExists("@vue/test-utils", fileRoot);
|
|
@@ -787,6 +808,7 @@ var BrowserPlugin = (parentServer, base = "/") => {
|
|
|
787
808
|
},
|
|
788
809
|
transform(code, id) {
|
|
789
810
|
if (id.includes(parentServer.vite.config.cacheDir) && id.includes("loupe.js")) {
|
|
811
|
+
// loupe bundle has a nastry require('util') call that leaves a warning in the console
|
|
790
812
|
const utilRequire = "nodeUtil = require_util();";
|
|
791
813
|
return code.replace(utilRequire, " ".repeat(utilRequire.length));
|
|
792
814
|
}
|
|
@@ -829,11 +851,12 @@ var BrowserPlugin = (parentServer, base = "/") => {
|
|
|
829
851
|
{
|
|
830
852
|
name: "vitest:browser:in-source-tests",
|
|
831
853
|
transform(code, id) {
|
|
854
|
+
const filename = cleanUrl(id);
|
|
832
855
|
const project = parentServer.vitest.getProjectByName(parentServer.config.name);
|
|
833
|
-
if (!project._isCachedTestFile(
|
|
856
|
+
if (!project._isCachedTestFile(filename) || !code.includes("import.meta.vitest")) {
|
|
834
857
|
return;
|
|
835
858
|
}
|
|
836
|
-
const s = new MagicString(code, { filename
|
|
859
|
+
const s = new MagicString(code, { filename });
|
|
837
860
|
s.prepend(`import.meta.vitest = __vitest_index__;\n`);
|
|
838
861
|
return {
|
|
839
862
|
code: s.toString(),
|
|
@@ -844,6 +867,7 @@ var BrowserPlugin = (parentServer, base = "/") => {
|
|
|
844
867
|
{
|
|
845
868
|
name: "vitest:browser:worker",
|
|
846
869
|
transform(code, id, _options) {
|
|
870
|
+
// https://github.com/vitejs/vite/blob/ba56cf43b5480f8519349f7d7fe60718e9af5f1a/packages/vite/src/node/plugins/worker.ts#L46
|
|
847
871
|
if (/(?:\?|&)worker_file&type=\w+(?:&|$)/.test(id)) {
|
|
848
872
|
const s = new MagicString(code);
|
|
849
873
|
s.prepend("globalThis.__vitest_browser_runner__ = { wrapDynamicImport: f => f() };\n");
|
|
@@ -898,6 +922,8 @@ var BrowserPlugin = (parentServer, base = "/") => {
|
|
|
898
922
|
}
|
|
899
923
|
}
|
|
900
924
|
} else {
|
|
925
|
+
// inject the reset style only in the default template,
|
|
926
|
+
// allowing users to customize the style in their own template
|
|
901
927
|
testerTags.push({
|
|
902
928
|
tag: "style",
|
|
903
929
|
children: `
|
|
@@ -969,6 +995,8 @@ body {
|
|
|
969
995
|
const esbuildPlugin = {
|
|
970
996
|
name: "test-utils-rewrite",
|
|
971
997
|
setup(build) {
|
|
998
|
+
// test-utils: resolve to CJS instead of the browser because the browser version expects a global Vue object
|
|
999
|
+
// compiler-core: only CJS version allows slots as strings
|
|
972
1000
|
build.onResolve({ filter: /^@vue\/(test-utils|compiler-core)$/ }, (args) => {
|
|
973
1001
|
const resolved = getRequire().resolve(args.path, { paths: [args.importer] });
|
|
974
1002
|
return { path: resolved };
|
|
@@ -1006,6 +1034,7 @@ function resolveCoverageFolder(vitest) {
|
|
|
1006
1034
|
if (!htmlReporter) {
|
|
1007
1035
|
return undefined;
|
|
1008
1036
|
}
|
|
1037
|
+
// reportsDirectory not resolved yet
|
|
1009
1038
|
const root = resolve(options.root || process.cwd(), options.coverage.reportsDirectory || coverageConfigDefaults.reportsDirectory);
|
|
1010
1039
|
const subdir = Array.isArray(htmlReporter) && htmlReporter.length > 1 && "subdir" in htmlReporter[1] ? htmlReporter[1].subdir : undefined;
|
|
1011
1040
|
if (!subdir || typeof subdir !== "string") {
|
|
@@ -1121,6 +1150,7 @@ const dragAndDrop = async (context, source, target, options_) => {
|
|
|
1121
1150
|
const $target = context.browser.$(target);
|
|
1122
1151
|
const options = options_ || {};
|
|
1123
1152
|
const duration = options.duration ?? 10;
|
|
1153
|
+
// https://github.com/webdriverio/webdriverio/issues/8022#issuecomment-1700919670
|
|
1124
1154
|
await context.browser.action("pointer").move({
|
|
1125
1155
|
duration: 0,
|
|
1126
1156
|
origin: $source,
|
|
@@ -1625,6 +1655,7 @@ function assertFileAccess(path, project) {
|
|
|
1625
1655
|
const readFile = async ({ project }, path, options = {}) => {
|
|
1626
1656
|
const filepath = resolve$1(project.config.root, path);
|
|
1627
1657
|
assertFileAccess(filepath, project);
|
|
1658
|
+
// never return a Buffer
|
|
1628
1659
|
if (typeof options === "object" && !options.encoding) {
|
|
1629
1660
|
options.encoding = "utf-8";
|
|
1630
1661
|
}
|
|
@@ -2015,6 +2046,8 @@ const keyboardCleanup = async (context, state) => {
|
|
|
2015
2046
|
throw new TypeError(`Provider "${context.provider.name}" does not support keyboard api`);
|
|
2016
2047
|
}
|
|
2017
2048
|
};
|
|
2049
|
+
// fallback to insertText for non US key
|
|
2050
|
+
// https://github.com/microsoft/playwright/blob/50775698ae13642742f2a1e8983d1d686d7f192d/packages/playwright-core/src/server/input.ts#L95
|
|
2018
2051
|
const VALID_KEYS = new Set([
|
|
2019
2052
|
"Escape",
|
|
2020
2053
|
"F1",
|
|
@@ -2230,6 +2263,9 @@ async function keyboardImplementation(pressed, provider, sessionId, text, select
|
|
|
2230
2263
|
const actions = parseKeyDef(defaultKeyMap, text);
|
|
2231
2264
|
for (const { releasePrevious, releaseSelf, repeat, keyDef } of actions) {
|
|
2232
2265
|
const key = keyDef.key;
|
|
2266
|
+
// TODO: instead of calling down/up for each key, join non special
|
|
2267
|
+
// together, and call `type` once for all non special keys,
|
|
2268
|
+
// and then `press` for special keys
|
|
2233
2269
|
if (pressed.has(key)) {
|
|
2234
2270
|
if (VALID_KEYS.has(key)) {
|
|
2235
2271
|
await page.keyboard.up(key);
|
|
@@ -2296,6 +2332,7 @@ async function keyboardImplementation(pressed, provider, sessionId, text, select
|
|
|
2296
2332
|
}
|
|
2297
2333
|
}
|
|
2298
2334
|
}
|
|
2335
|
+
// seems like webdriverio doesn't release keys automatically if skipRelease is true and all events are keyUp
|
|
2299
2336
|
const allRelease = keyboard.toJSON().actions.every((action) => action.type === "keyUp");
|
|
2300
2337
|
await keyboard.perform(allRelease ? false : skipRelease);
|
|
2301
2338
|
}
|
|
@@ -2454,12 +2491,12 @@ const upload = async (context, selector, files, options) => {
|
|
|
2454
2491
|
if (!testPath) {
|
|
2455
2492
|
throw new Error(`Cannot upload files outside of a test`);
|
|
2456
2493
|
}
|
|
2457
|
-
const
|
|
2494
|
+
const root = context.project.config.root;
|
|
2458
2495
|
if (context.provider instanceof PlaywrightBrowserProvider) {
|
|
2459
2496
|
const { iframe } = context;
|
|
2460
2497
|
const playwrightFiles = files.map((file) => {
|
|
2461
2498
|
if (typeof file === "string") {
|
|
2462
|
-
return resolve(
|
|
2499
|
+
return resolve(root, file);
|
|
2463
2500
|
}
|
|
2464
2501
|
return {
|
|
2465
2502
|
name: file.name,
|
|
@@ -2476,7 +2513,7 @@ const upload = async (context, selector, files, options) => {
|
|
|
2476
2513
|
}
|
|
2477
2514
|
const element = context.browser.$(selector);
|
|
2478
2515
|
for (const file of files) {
|
|
2479
|
-
const filepath = resolve(
|
|
2516
|
+
const filepath = resolve(root, file);
|
|
2480
2517
|
const remoteFilePath = await context.browser.uploadFile(filepath);
|
|
2481
2518
|
await element.addValue(remoteFilePath);
|
|
2482
2519
|
}
|
|
@@ -2485,6 +2522,14 @@ const upload = async (context, selector, files, options) => {
|
|
|
2485
2522
|
}
|
|
2486
2523
|
};
|
|
2487
2524
|
|
|
2525
|
+
const viewport = async (context, options) => {
|
|
2526
|
+
if (context.provider instanceof WebdriverBrowserProvider) {
|
|
2527
|
+
await context.provider.setViewport(options);
|
|
2528
|
+
} else {
|
|
2529
|
+
throw new TypeError(`Provider ${context.provider.name} doesn't support "viewport" command`);
|
|
2530
|
+
}
|
|
2531
|
+
};
|
|
2532
|
+
|
|
2488
2533
|
var builtinCommands = {
|
|
2489
2534
|
readFile,
|
|
2490
2535
|
removeFile,
|
|
@@ -2503,7 +2548,8 @@ var builtinCommands = {
|
|
|
2503
2548
|
__vitest_selectOptions: selectOptions,
|
|
2504
2549
|
__vitest_dragAndDrop: dragAndDrop,
|
|
2505
2550
|
__vitest_hover: hover,
|
|
2506
|
-
__vitest_cleanup: keyboardCleanup
|
|
2551
|
+
__vitest_cleanup: keyboardCleanup,
|
|
2552
|
+
__vitest_viewport: viewport
|
|
2507
2553
|
};
|
|
2508
2554
|
|
|
2509
2555
|
class BrowserServerState {
|
|
@@ -2599,6 +2645,7 @@ class ParentBrowserProject {
|
|
|
2599
2645
|
children = new Set();
|
|
2600
2646
|
vitest;
|
|
2601
2647
|
config;
|
|
2648
|
+
// cache for non-vite source maps
|
|
2602
2649
|
sourceMapCache = new Map();
|
|
2603
2650
|
constructor(project, base) {
|
|
2604
2651
|
this.project = project;
|
|
@@ -2612,6 +2659,7 @@ class ParentBrowserProject {
|
|
|
2612
2659
|
return this.sourceMapCache.get(id);
|
|
2613
2660
|
}
|
|
2614
2661
|
const result = this.vite.moduleGraph.getModuleById(id)?.transformResult;
|
|
2662
|
+
// this can happen for bundled dependencies in node_modules/.vite
|
|
2615
2663
|
if (result && !result.map) {
|
|
2616
2664
|
const sourceMapUrl = this.retrieveSourceMapURL(result.code);
|
|
2617
2665
|
if (!sourceMapUrl) {
|
|
@@ -2635,6 +2683,8 @@ class ParentBrowserProject {
|
|
|
2635
2683
|
if (modUrl) {
|
|
2636
2684
|
return resolvedPath;
|
|
2637
2685
|
}
|
|
2686
|
+
// some browsers (looking at you, safari) don't report queries in stack traces
|
|
2687
|
+
// the next best thing is to try the first id that this file resolves to
|
|
2638
2688
|
const files = this.vite.moduleGraph.getModulesByFile(resolvedPath);
|
|
2639
2689
|
if (files && files.size) {
|
|
2640
2690
|
return files.values().next().value.id;
|
|
@@ -2645,6 +2695,7 @@ class ParentBrowserProject {
|
|
|
2645
2695
|
for (const [name, command] of Object.entries(builtinCommands)) {
|
|
2646
2696
|
this.commands[name] ??= command;
|
|
2647
2697
|
}
|
|
2698
|
+
// validate names because they can't be used as identifiers
|
|
2648
2699
|
for (const command in project.config.browser.commands) {
|
|
2649
2700
|
if (!/^[a-z_$][\w$]*$/i.test(command)) {
|
|
2650
2701
|
throw new Error(`Invalid command name "${command}". Only alphanumeric characters, $ and _ are allowed.`);
|
|
@@ -2765,7 +2816,10 @@ class ParentBrowserProject {
|
|
|
2765
2816
|
}
|
|
2766
2817
|
retrieveSourceMapURL(source) {
|
|
2767
2818
|
const re = /\/\/[@#]\s*sourceMappingURL=([^\s'"]+)\s*$|\/\*[@#]\s*sourceMappingURL=[^\s*'"]+\s*\*\/\s*$/gm;
|
|
2819
|
+
// Keep executing the search to find the *last* sourceMappingURL to avoid
|
|
2820
|
+
// picking up sourceMappingURLs from comments, strings, etc.
|
|
2768
2821
|
let lastMatch, match;
|
|
2822
|
+
// eslint-disable-next-line no-cond-assign
|
|
2769
2823
|
while (match = re.exec(source)) {
|
|
2770
2824
|
lastMatch = match;
|
|
2771
2825
|
}
|
|
@@ -2964,6 +3018,7 @@ function setupBrowserRpc(globalServer, defaultMockerRegistry) {
|
|
|
2964
3018
|
}
|
|
2965
3019
|
if (type === "orchestrator") {
|
|
2966
3020
|
const session = vitest._browserSessions.getSession(sessionId);
|
|
3021
|
+
// it's possible the session was already resolved by the preview provider
|
|
2967
3022
|
session?.connected();
|
|
2968
3023
|
}
|
|
2969
3024
|
const project = vitest.getProjectByName(projectName);
|
|
@@ -2984,10 +3039,12 @@ function setupBrowserRpc(globalServer, defaultMockerRegistry) {
|
|
|
2984
3039
|
if (type === "orchestrator") {
|
|
2985
3040
|
vitest._browserSessions.destroySession(sessionId);
|
|
2986
3041
|
}
|
|
3042
|
+
// this will reject any hanging methods if there are any
|
|
2987
3043
|
rpc.$close(new Error(`[vitest] Browser connection was closed while running tests. Was the page closed unexpectedly?`));
|
|
2988
3044
|
});
|
|
2989
3045
|
});
|
|
2990
3046
|
});
|
|
3047
|
+
// we don't throw an error inside a stream because this can segfault the process
|
|
2991
3048
|
function error(err) {
|
|
2992
3049
|
console.error(err);
|
|
2993
3050
|
vitest.state.catchError(err, "RPC Error");
|
|
@@ -3124,6 +3181,8 @@ function setupBrowserRpc(globalServer, defaultMockerRegistry) {
|
|
|
3124
3181
|
},
|
|
3125
3182
|
async registerMock(sessionId, module) {
|
|
3126
3183
|
if (!mocker) {
|
|
3184
|
+
// make sure modules are not processed yet in case they were imported before
|
|
3185
|
+
// and were not mocked
|
|
3127
3186
|
mockResolver.invalidate([module.id]);
|
|
3128
3187
|
if (module.type === "manual") {
|
|
3129
3188
|
const mock = ManualMockedModule.fromJSON(module, async () => {
|
|
@@ -3194,7 +3253,10 @@ function setupBrowserRpc(globalServer, defaultMockerRegistry) {
|
|
|
3194
3253
|
return rpc;
|
|
3195
3254
|
}
|
|
3196
3255
|
}
|
|
3256
|
+
// Serialization support utils.
|
|
3197
3257
|
function cloneByOwnProperties(value) {
|
|
3258
|
+
// Clones the value's properties into a new Object. The simpler approach of
|
|
3259
|
+
// Object.assign() won't work in the case that properties are not enumerable.
|
|
3198
3260
|
return Object.getOwnPropertyNames(value).reduce((clone, prop) => ({
|
|
3199
3261
|
...clone,
|
|
3200
3262
|
[prop]: value[prop]
|
|
@@ -3255,7 +3317,7 @@ function createBrowserPool(vitest) {
|
|
|
3255
3317
|
vitest.onCancel(() => {
|
|
3256
3318
|
isCancelled = true;
|
|
3257
3319
|
});
|
|
3258
|
-
await Promise.all([...groupedFiles.entries()].map(async ([project, files]) => {
|
|
3320
|
+
const initialisedPools = await Promise.all([...groupedFiles.entries()].map(async ([project, files]) => {
|
|
3259
3321
|
await project._initBrowserProvider();
|
|
3260
3322
|
if (!project.browser) {
|
|
3261
3323
|
throw new TypeError(`The browser server was not initialized${project.name ? ` for the "${project.name}" project` : ""}. This is a bug in Vitest. Please, open a new issue with reproduction.`);
|
|
@@ -3267,8 +3329,34 @@ function createBrowserPool(vitest) {
|
|
|
3267
3329
|
const pool = ensurePool(project);
|
|
3268
3330
|
vitest.state.clearFiles(project, files);
|
|
3269
3331
|
providers.add(project.browser.provider);
|
|
3270
|
-
|
|
3332
|
+
return {
|
|
3333
|
+
pool,
|
|
3334
|
+
provider: project.browser.provider,
|
|
3335
|
+
runTests: () => pool.runTests(method, files)
|
|
3336
|
+
};
|
|
3271
3337
|
}));
|
|
3338
|
+
if (isCancelled) {
|
|
3339
|
+
return;
|
|
3340
|
+
}
|
|
3341
|
+
const parallelPools = [];
|
|
3342
|
+
const nonParallelPools = [];
|
|
3343
|
+
for (const result of initialisedPools) {
|
|
3344
|
+
if (!result) {
|
|
3345
|
+
return;
|
|
3346
|
+
}
|
|
3347
|
+
if (result.provider.mocker && result.provider.supportsParallelism) {
|
|
3348
|
+
parallelPools.push(result.runTests);
|
|
3349
|
+
} else {
|
|
3350
|
+
nonParallelPools.push(result.runTests);
|
|
3351
|
+
}
|
|
3352
|
+
}
|
|
3353
|
+
await Promise.all(parallelPools.map((runTests) => runTests()));
|
|
3354
|
+
for (const runTests of nonParallelPools) {
|
|
3355
|
+
if (isCancelled) {
|
|
3356
|
+
return;
|
|
3357
|
+
}
|
|
3358
|
+
await runTests();
|
|
3359
|
+
}
|
|
3272
3360
|
};
|
|
3273
3361
|
function getThreadsCount(project) {
|
|
3274
3362
|
const config = project.config.browser;
|
|
@@ -3339,6 +3427,8 @@ class BrowserPool {
|
|
|
3339
3427
|
debug?.("all orchestrators are ready, not creating more");
|
|
3340
3428
|
return this._promise;
|
|
3341
3429
|
}
|
|
3430
|
+
// open the minimum amount of tabs
|
|
3431
|
+
// if there is only 1 file running, we don't need 8 tabs running
|
|
3342
3432
|
const workerCount = Math.min(this.options.maxWorkers - this.orchestrators.size, files.length);
|
|
3343
3433
|
const promises = [];
|
|
3344
3434
|
for (let i = 0; i < workerCount; i++) {
|
|
@@ -3347,6 +3437,7 @@ class BrowserPool {
|
|
|
3347
3437
|
const project = this.project.name;
|
|
3348
3438
|
debug?.("[%s] creating session for %s", sessionId, project);
|
|
3349
3439
|
const page = this.openPage(sessionId).then(() => {
|
|
3440
|
+
// start running tests on the page when it's ready
|
|
3350
3441
|
this.runNextTest(method, sessionId);
|
|
3351
3442
|
});
|
|
3352
3443
|
promises.push(page);
|
|
@@ -3371,6 +3462,7 @@ class BrowserPool {
|
|
|
3371
3462
|
}
|
|
3372
3463
|
finishSession(sessionId) {
|
|
3373
3464
|
this.readySessions.add(sessionId);
|
|
3465
|
+
// the last worker finished running tests
|
|
3374
3466
|
if (this.readySessions.size === this.orchestrators.size) {
|
|
3375
3467
|
this._promise?.resolve();
|
|
3376
3468
|
this._promise = undefined;
|
|
@@ -3384,10 +3476,14 @@ class BrowserPool {
|
|
|
3384
3476
|
if (!file) {
|
|
3385
3477
|
debug?.("[%s] no more tests to run", sessionId);
|
|
3386
3478
|
const isolate = this.project.config.browser.isolate;
|
|
3479
|
+
// we don't need to cleanup testers if isolation is enabled,
|
|
3480
|
+
// because cleanup is done at the end of every test
|
|
3387
3481
|
if (isolate) {
|
|
3388
3482
|
this.finishSession(sessionId);
|
|
3389
3483
|
return;
|
|
3390
3484
|
}
|
|
3485
|
+
// we need to cleanup testers first because there is only
|
|
3486
|
+
// one iframe and it does the cleanup only after everything is completed
|
|
3391
3487
|
const orchestrator = this.getOrchestrator(sessionId);
|
|
3392
3488
|
orchestrator.cleanupTesters().catch((error) => this.reject(error)).finally(() => this.finishSession(sessionId));
|
|
3393
3489
|
return;
|
|
@@ -3398,6 +3494,7 @@ class BrowserPool {
|
|
|
3398
3494
|
const orchestrator = this.getOrchestrator(sessionId);
|
|
3399
3495
|
debug?.("[%s] run test %s", sessionId, file);
|
|
3400
3496
|
this.setBreakpoint(sessionId, file).then(() => {
|
|
3497
|
+
// this starts running tests inside the orchestrator
|
|
3401
3498
|
orchestrator.createTesters({
|
|
3402
3499
|
method,
|
|
3403
3500
|
files: [file],
|
|
@@ -3406,6 +3503,7 @@ class BrowserPool {
|
|
|
3406
3503
|
debug?.("[%s] test %s finished running", sessionId, file);
|
|
3407
3504
|
this.runNextTest(method, sessionId);
|
|
3408
3505
|
}).catch((error) => {
|
|
3506
|
+
// if user cancells the test run manually, ignore the error and exit gracefully
|
|
3409
3507
|
if (this.project.vitest.isCancelling && error instanceof Error && error.message.startsWith("Browser connection was closed while running tests")) {
|
|
3410
3508
|
this.cancel();
|
|
3411
3509
|
this._promise?.resolve();
|
package/dist/locators/index.d.ts
CHANGED
|
@@ -278,6 +278,7 @@ interface SelectorEngine {
|
|
|
278
278
|
queryAll: (root: SelectorRoot, selector: string | any) => Element[];
|
|
279
279
|
}
|
|
280
280
|
|
|
281
|
+
// we prefer using playwright locators because they are more powerful and support Shadow DOM
|
|
281
282
|
declare const selectorEngine: Ivya;
|
|
282
283
|
declare abstract class Locator {
|
|
283
284
|
abstract selector: string;
|
package/dist/providers.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { W as WebdriverBrowserProvider, P as PlaywrightBrowserProvider } from './webdriver-
|
|
1
|
+
import { W as WebdriverBrowserProvider, P as PlaywrightBrowserProvider } from './webdriver-B1QbgqhC.js';
|
|
2
2
|
import '@vitest/mocker/node';
|
|
3
3
|
import 'tinyrainbow';
|
|
4
4
|
import 'vitest/node';
|
|
@@ -9,6 +9,7 @@ class PreviewBrowserProvider {
|
|
|
9
9
|
project;
|
|
10
10
|
open = false;
|
|
11
11
|
getSupportedBrowsers() {
|
|
12
|
+
// `none` is not restricted to certain browsers.
|
|
12
13
|
return [];
|
|
13
14
|
}
|
|
14
15
|
isOpen() {
|
package/dist/state.js
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
|
|
4
4
|
/* @__NO_SIDE_EFFECTS__ */
|
|
5
5
|
function getBrowserState() {
|
|
6
|
+
// @ts-expect-error not typed global
|
|
6
7
|
return window.__vitest_browser_runner__;
|
|
7
8
|
}
|
|
8
9
|
|
|
@@ -40,7 +41,9 @@
|
|
|
40
41
|
},
|
|
41
42
|
providedContext: {}
|
|
42
43
|
};
|
|
44
|
+
// @ts-expect-error not typed global
|
|
43
45
|
globalThis.__vitest_browser__ = true;
|
|
46
|
+
// @ts-expect-error not typed global
|
|
44
47
|
globalThis.__vitest_worker__ = state;
|
|
45
48
|
getBrowserState().cdp = createCdp();
|
|
46
49
|
function rpc() {
|
|
@@ -59,6 +59,7 @@ class PlaywrightBrowserProvider {
|
|
|
59
59
|
headless: options.headless
|
|
60
60
|
};
|
|
61
61
|
if (this.project.config.inspector.enabled) {
|
|
62
|
+
// NodeJS equivalent defaults: https://nodejs.org/en/learn/getting-started/debugging#enable-inspector
|
|
62
63
|
const port = this.project.config.inspector.port || 9229;
|
|
63
64
|
const host = this.project.config.inspector.host || "127.0.0.1";
|
|
64
65
|
launchOptions.args ||= [];
|
|
@@ -66,6 +67,7 @@ class PlaywrightBrowserProvider {
|
|
|
66
67
|
launchOptions.args.push(`--remote-debugging-address=${host}`);
|
|
67
68
|
this.project.vitest.logger.log(`Debugger listening on ws://${host}:${port}`);
|
|
68
69
|
}
|
|
70
|
+
// start Vitest UI maximized only on supported browsers
|
|
69
71
|
if (this.project.config.browser.ui && this.browserName === "chromium") {
|
|
70
72
|
if (!launchOptions.args) {
|
|
71
73
|
launchOptions.args = [];
|
|
@@ -90,15 +92,18 @@ class PlaywrightBrowserProvider {
|
|
|
90
92
|
if (url.searchParams.has("_vitest_original")) {
|
|
91
93
|
return false;
|
|
92
94
|
}
|
|
95
|
+
// different modules, ignore request
|
|
93
96
|
if (url.pathname !== moduleUrl.pathname) {
|
|
94
97
|
return false;
|
|
95
98
|
}
|
|
96
99
|
url.searchParams.delete("t");
|
|
97
100
|
url.searchParams.delete("v");
|
|
98
101
|
url.searchParams.delete("import");
|
|
102
|
+
// different search params, ignore request
|
|
99
103
|
if (url.searchParams.size !== moduleUrl.searchParams.size) {
|
|
100
104
|
return false;
|
|
101
105
|
}
|
|
106
|
+
// check that all search params are the same
|
|
102
107
|
for (const [param, value] of url.searchParams.entries()) {
|
|
103
108
|
if (moduleUrl.searchParams.get(param) !== value) {
|
|
104
109
|
return false;
|
|
@@ -109,9 +114,12 @@ class PlaywrightBrowserProvider {
|
|
|
109
114
|
const ids = sessionIds.get(sessionId) || [];
|
|
110
115
|
ids.push(moduleUrl.href);
|
|
111
116
|
sessionIds.set(sessionId, ids);
|
|
112
|
-
idPreficates.set(moduleUrl.href, predicate);
|
|
117
|
+
idPreficates.set(predicateKey(sessionId, moduleUrl.href), predicate);
|
|
113
118
|
return predicate;
|
|
114
119
|
}
|
|
120
|
+
function predicateKey(sessionId, url) {
|
|
121
|
+
return `${sessionId}:${url}`;
|
|
122
|
+
}
|
|
115
123
|
return {
|
|
116
124
|
register: async (sessionId, module) => {
|
|
117
125
|
const page = this.getPage(sessionId);
|
|
@@ -124,16 +132,19 @@ class PlaywrightBrowserProvider {
|
|
|
124
132
|
headers: getHeaders(this.project.browser.vite.config)
|
|
125
133
|
});
|
|
126
134
|
}
|
|
135
|
+
// webkit doesn't support redirect responses
|
|
136
|
+
// https://github.com/microsoft/playwright/issues/18318
|
|
127
137
|
const isWebkit = this.browserName === "webkit";
|
|
128
138
|
if (isWebkit) {
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
139
|
+
let url;
|
|
140
|
+
if (module.type === "redirect") {
|
|
141
|
+
const redirect = new URL(module.redirect);
|
|
142
|
+
url = redirect.href.slice(redirect.origin.length);
|
|
143
|
+
} else {
|
|
144
|
+
const request = new URL(route.request().url());
|
|
145
|
+
request.searchParams.set("mock", module.type);
|
|
146
|
+
url = request.href.slice(request.origin.length);
|
|
147
|
+
}
|
|
137
148
|
const result = await this.project.browser.vite.transformRequest(url).catch(() => null);
|
|
138
149
|
if (!result) {
|
|
139
150
|
return route.continue();
|
|
@@ -165,18 +176,20 @@ class PlaywrightBrowserProvider {
|
|
|
165
176
|
},
|
|
166
177
|
delete: async (sessionId, id) => {
|
|
167
178
|
const page = this.getPage(sessionId);
|
|
168
|
-
const
|
|
179
|
+
const key = predicateKey(sessionId, id);
|
|
180
|
+
const predicate = idPreficates.get(key);
|
|
169
181
|
if (predicate) {
|
|
170
|
-
await page.unroute(predicate).finally(() => idPreficates.delete(
|
|
182
|
+
await page.unroute(predicate).finally(() => idPreficates.delete(key));
|
|
171
183
|
}
|
|
172
184
|
},
|
|
173
185
|
clear: async (sessionId) => {
|
|
174
186
|
const page = this.getPage(sessionId);
|
|
175
187
|
const ids = sessionIds.get(sessionId) || [];
|
|
176
188
|
const promises = ids.map((id) => {
|
|
177
|
-
const
|
|
189
|
+
const key = predicateKey(sessionId, id);
|
|
190
|
+
const predicate = idPreficates.get(key);
|
|
178
191
|
if (predicate) {
|
|
179
|
-
return page.unroute(predicate).finally(() => idPreficates.delete(
|
|
192
|
+
return page.unroute(predicate).finally(() => idPreficates.delete(key));
|
|
180
193
|
}
|
|
181
194
|
return null;
|
|
182
195
|
});
|
|
@@ -359,6 +372,8 @@ class WebdriverBrowserProvider {
|
|
|
359
372
|
project;
|
|
360
373
|
options;
|
|
361
374
|
closing = false;
|
|
375
|
+
iframeSwitched = false;
|
|
376
|
+
topLevelContext;
|
|
362
377
|
getSupportedBrowsers() {
|
|
363
378
|
return webdriverBrowsers;
|
|
364
379
|
}
|
|
@@ -368,14 +383,19 @@ class WebdriverBrowserProvider {
|
|
|
368
383
|
this.browserName = browser;
|
|
369
384
|
this.options = options;
|
|
370
385
|
}
|
|
386
|
+
isIframeSwitched() {
|
|
387
|
+
return this.iframeSwitched;
|
|
388
|
+
}
|
|
371
389
|
async switchToTestFrame() {
|
|
372
390
|
const page = this.browser;
|
|
391
|
+
// support wdio@9
|
|
373
392
|
if (page.switchFrame) {
|
|
374
393
|
await page.switchFrame(page.$("iframe[data-vitest]"));
|
|
375
394
|
} else {
|
|
376
395
|
const iframe = await page.findElement("css selector", "iframe[data-vitest]");
|
|
377
396
|
await page.switchToFrame(iframe);
|
|
378
397
|
}
|
|
398
|
+
this.iframeSwitched = true;
|
|
379
399
|
}
|
|
380
400
|
async switchToMainFrame() {
|
|
381
401
|
const page = this.browser;
|
|
@@ -384,6 +404,20 @@ class WebdriverBrowserProvider {
|
|
|
384
404
|
} else {
|
|
385
405
|
await page.switchToParentFrame();
|
|
386
406
|
}
|
|
407
|
+
this.iframeSwitched = false;
|
|
408
|
+
}
|
|
409
|
+
async setViewport(options) {
|
|
410
|
+
if (this.topLevelContext == null || !this.browser) {
|
|
411
|
+
throw new Error(`The browser has no open pages.`);
|
|
412
|
+
}
|
|
413
|
+
await this.browser.send({
|
|
414
|
+
method: "browsingContext.setViewport",
|
|
415
|
+
params: {
|
|
416
|
+
context: this.topLevelContext,
|
|
417
|
+
devicePixelRatio: 1,
|
|
418
|
+
viewport: options
|
|
419
|
+
}
|
|
420
|
+
});
|
|
387
421
|
}
|
|
388
422
|
getCommandsContext() {
|
|
389
423
|
return { browser: this.browser };
|
|
@@ -407,6 +441,7 @@ class WebdriverBrowserProvider {
|
|
|
407
441
|
capabilities: this.buildCapabilities()
|
|
408
442
|
};
|
|
409
443
|
debug?.("[%s] opening the browser with options: %O", this.browserName, remoteOptions);
|
|
444
|
+
// TODO: close everything, if browser is closed from the outside
|
|
410
445
|
this.browser = await remote(remoteOptions);
|
|
411
446
|
await this._throwIfClosing();
|
|
412
447
|
return this.browser;
|
|
@@ -432,6 +467,7 @@ class WebdriverBrowserProvider {
|
|
|
432
467
|
args: newArgs
|
|
433
468
|
};
|
|
434
469
|
}
|
|
470
|
+
// start Vitest UI maximized only on supported browsers
|
|
435
471
|
if (options.ui && (browser === "chrome" || browser === "edge")) {
|
|
436
472
|
const key = browser === "chrome" ? "goog:chromeOptions" : "ms:edgeOptions";
|
|
437
473
|
const args = capabilities[key]?.args || [];
|
|
@@ -449,6 +485,7 @@ class WebdriverBrowserProvider {
|
|
|
449
485
|
const browserInstance = await this.openBrowser();
|
|
450
486
|
debug?.("[%s][%s] browser page is created, opening %s", sessionId, this.browserName, url);
|
|
451
487
|
await browserInstance.url(url);
|
|
488
|
+
this.topLevelContext = await browserInstance.getWindowHandle();
|
|
452
489
|
await this._throwIfClosing("opening the url");
|
|
453
490
|
}
|
|
454
491
|
async _throwIfClosing(action) {
|
|
@@ -462,6 +499,8 @@ class WebdriverBrowserProvider {
|
|
|
462
499
|
debug?.("[%s] closing provider", this.browserName);
|
|
463
500
|
this.closing = true;
|
|
464
501
|
await Promise.all([this.browser?.sessionId ? this.browser?.deleteSession?.() : null]);
|
|
502
|
+
// TODO: right now process can only exit with timeout, if we use browser
|
|
503
|
+
// needs investigating
|
|
465
504
|
process.exit();
|
|
466
505
|
}
|
|
467
506
|
}
|