@vitest/browser 3.2.4 → 4.0.0-beta.10
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/LICENSE +1 -1
- package/README.md +3 -15
- package/context.d.ts +153 -3
- package/dist/client/.vite/manifest.json +6 -6
- package/dist/client/__vitest__/assets/index-DFDJV2DF.js +53 -0
- package/dist/client/__vitest__/assets/{index-X8b7Z_4p.css → index-KbpJLW--.css} +1 -1
- package/dist/client/__vitest__/index.html +2 -2
- package/dist/client/__vitest_browser__/orchestrator-BXX6oamz.js +296 -0
- package/dist/client/__vitest_browser__/{tester-BYDMHqQ9.js → tester-CMhJ1E1W.js} +301 -580
- package/dist/client/__vitest_browser__/{utils-Owv5OOOf.js → utils-CPmDBIKG.js} +3 -3
- package/dist/client/error-catcher.js +7 -3
- package/dist/client/esm-client-injector.js +1 -0
- package/dist/client/orchestrator.html +2 -2
- package/dist/client/tester/tester.html +2 -2
- package/dist/client.js +24 -8
- package/dist/context.js +34 -24
- package/dist/expect-element.js +10 -8
- package/dist/index-CwoiDq7G.js +6 -0
- package/dist/index-DDlvjJVO.js +1 -0
- package/dist/index.d.ts +20 -15
- package/dist/index.js +550 -98
- package/dist/locators/index.d.ts +8 -7
- package/dist/locators/index.js +1 -1
- package/dist/locators/playwright.js +1 -1
- package/dist/locators/preview.js +1 -1
- package/dist/locators/webdriverio.js +1 -1
- package/dist/providers/playwright.d.ts +103 -0
- package/dist/{webdriver-KA1WiV0q.js → providers/playwright.js} +37 -180
- package/dist/providers/preview.d.ts +16 -0
- package/dist/{providers.js → providers/preview.js} +17 -21
- package/dist/providers/webdriverio.d.ts +50 -0
- package/dist/providers/webdriverio.js +171 -0
- package/dist/shared/screenshotMatcher/types.d.ts +16 -0
- package/dist/state.js +5 -2
- package/dist/types.d.ts +69 -0
- package/jest-dom.d.ts +95 -1
- package/package.json +22 -30
- package/utils.d.ts +1 -1
- package/dist/client/__vitest__/assets/index-D_ryMEPs.js +0 -58
- package/dist/client/__vitest_browser__/orchestrator-Bo1OwGWc.js +0 -3213
- package/dist/index-W1MM53zC.js +0 -1
- package/providers/playwright.d.ts +0 -81
- package/providers/webdriverio.d.ts +0 -22
package/dist/index.js
CHANGED
|
@@ -1,25 +1,29 @@
|
|
|
1
1
|
import { ManualMockedModule, RedirectedModule, AutomockedModule, AutospiedModule, MockerRegistry } from '@vitest/mocker';
|
|
2
2
|
import { dynamicImportPlugin, ServerMockResolver, interceptorPlugin } from '@vitest/mocker/node';
|
|
3
3
|
import c from 'tinyrainbow';
|
|
4
|
-
import {
|
|
5
|
-
import fs, { readFileSync, lstatSync, promises, existsSync } from 'node:fs';
|
|
4
|
+
import { isValidApiRequest, isFileServingAllowed, distDir, resolveApiServerConfig, resolveFsAllow, createDebugger, createViteLogger, createViteServer } from 'vitest/node';
|
|
5
|
+
import fs, { readFileSync, lstatSync, createReadStream, promises, existsSync } from 'node:fs';
|
|
6
6
|
import { createRequire } from 'node:module';
|
|
7
|
-
import { slash as slash$1, toArray, createDefer } from '@vitest/utils';
|
|
7
|
+
import { slash as slash$1, toArray, deepMerge, createDefer } from '@vitest/utils';
|
|
8
8
|
import MagicString from 'magic-string';
|
|
9
9
|
import sirv from 'sirv';
|
|
10
10
|
import * as vite from 'vite';
|
|
11
11
|
import { coverageConfigDefaults } from 'vitest/config';
|
|
12
12
|
import { fileURLToPath } from 'node:url';
|
|
13
13
|
import crypto from 'node:crypto';
|
|
14
|
-
import { mkdir, rm, readFile as readFile$1 } from 'node:fs/promises';
|
|
14
|
+
import { mkdir, rm, readFile as readFile$1, writeFile as writeFile$1 } from 'node:fs/promises';
|
|
15
15
|
import { parseErrorStacktrace, parseStacktrace } from '@vitest/utils/source-map';
|
|
16
|
-
import {
|
|
16
|
+
import { PlaywrightBrowserProvider } from './providers/playwright.js';
|
|
17
|
+
import { WebdriverBrowserProvider } from './providers/webdriverio.js';
|
|
17
18
|
import { resolve as resolve$1, basename as basename$1, dirname as dirname$1, normalize as normalize$1 } from 'node:path';
|
|
18
|
-
import { WebSocketServer } from 'ws';
|
|
19
19
|
import * as nodeos from 'node:os';
|
|
20
|
+
import { platform } from 'node:os';
|
|
21
|
+
import { PNG } from 'pngjs';
|
|
22
|
+
import pm from 'pixelmatch';
|
|
23
|
+
import { WebSocketServer } from 'ws';
|
|
20
24
|
import { performance } from 'node:perf_hooks';
|
|
21
25
|
|
|
22
|
-
var version = "
|
|
26
|
+
var version = "4.0.0-beta.10";
|
|
23
27
|
|
|
24
28
|
const _DRIVE_LETTER_START_RE = /^[A-Za-z]:\//;
|
|
25
29
|
function normalizeWindowsPath(input = "") {
|
|
@@ -324,27 +328,28 @@ const stringify = (value, replacer, space) => {
|
|
|
324
328
|
function replacer(code, values) {
|
|
325
329
|
return code.replace(/\{\s*(\w+)\s*\}/g, (_, key) => values[key] ?? _);
|
|
326
330
|
}
|
|
327
|
-
const builtinProviders = [
|
|
328
|
-
"webdriverio",
|
|
329
|
-
"playwright",
|
|
330
|
-
"preview"
|
|
331
|
-
];
|
|
332
331
|
async function getBrowserProvider(options, project) {
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
332
|
+
const browser = project.config.browser.name;
|
|
333
|
+
const name = project.name ? `[${project.name}] ` : "";
|
|
334
|
+
if (!browser) {
|
|
335
|
+
throw new Error(`${name}Browser name is required. Please, set \`test.browser.instances[].browser\` option manually.`);
|
|
337
336
|
}
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
337
|
+
if (options.provider == null || "_cli" in options.provider && typeof options.provider.factory !== "function") {
|
|
338
|
+
const providers = await import('./index-CwoiDq7G.js');
|
|
339
|
+
const name = options.provider?.name || "preview";
|
|
340
|
+
if (!(name in providers)) {
|
|
341
|
+
throw new Error(`Unknown browser provider "${name}". Available providers: ${Object.keys(providers).join(", ")}.`);
|
|
342
|
+
}
|
|
343
|
+
return providers[name]().factory(project);
|
|
344
|
+
}
|
|
345
|
+
const supportedBrowsers = options.provider.supportedBrowser || [];
|
|
346
|
+
if (supportedBrowsers.length && !supportedBrowsers.includes(browser)) {
|
|
347
|
+
throw new Error(`${name}Browser "${browser}" is not supported by the browser provider "${options.provider.name}". Supported browsers: ${supportedBrowsers.join(", ")}.`);
|
|
343
348
|
}
|
|
344
|
-
if (
|
|
345
|
-
throw new
|
|
349
|
+
if (typeof options.provider.factory !== "function") {
|
|
350
|
+
throw new TypeError(`The "${name}" browser provider does not provide a "factory" function. Received ${typeof options.provider.factory}.`);
|
|
346
351
|
}
|
|
347
|
-
return
|
|
352
|
+
return options.provider.factory(project);
|
|
348
353
|
}
|
|
349
354
|
function slash(path) {
|
|
350
355
|
return path.replace(/\\/g, "/").replace(/\/+/g, "/");
|
|
@@ -355,7 +360,7 @@ async function resolveOrchestrator(globalServer, url, res) {
|
|
|
355
360
|
// it's possible to open the page without a context
|
|
356
361
|
if (!sessionId) {
|
|
357
362
|
const contexts = [...globalServer.children].flatMap((p) => [...p.state.orchestrators.keys()]);
|
|
358
|
-
sessionId = contexts
|
|
363
|
+
sessionId = contexts.at(-1) ?? "none";
|
|
359
364
|
}
|
|
360
365
|
// it's ok to not have a session here, especially in the preview provider
|
|
361
366
|
// because the user could refresh the page which would remove the session id from the url
|
|
@@ -364,13 +369,13 @@ async function resolveOrchestrator(globalServer, url, res) {
|
|
|
364
369
|
if (!browserProject) {
|
|
365
370
|
return;
|
|
366
371
|
}
|
|
367
|
-
// ignore
|
|
372
|
+
// ignore unknown pages
|
|
368
373
|
if (sessionId && sessionId !== "none" && !globalServer.vitest._browserSessions.sessionIds.has(sessionId)) {
|
|
369
374
|
return;
|
|
370
375
|
}
|
|
371
376
|
const injectorJs = typeof globalServer.injectorJs === "string" ? globalServer.injectorJs : await globalServer.injectorJs;
|
|
372
377
|
const injector = replacer(injectorJs, {
|
|
373
|
-
__VITEST_PROVIDER__: JSON.stringify(browserProject.config.browser.provider || "preview"),
|
|
378
|
+
__VITEST_PROVIDER__: JSON.stringify(browserProject.config.browser.provider?.name || "preview"),
|
|
374
379
|
__VITEST_CONFIG__: JSON.stringify(browserProject.wrapSerializedConfig()),
|
|
375
380
|
__VITEST_VITE_CONFIG__: JSON.stringify({ root: browserProject.vite.config.root }),
|
|
376
381
|
__VITEST_METHOD__: JSON.stringify("orchestrate"),
|
|
@@ -388,7 +393,7 @@ async function resolveOrchestrator(globalServer, url, res) {
|
|
|
388
393
|
for (const attr in script.attrs || {}) {
|
|
389
394
|
html += `${attr}="${script.attrs[attr]}" `;
|
|
390
395
|
}
|
|
391
|
-
html += `>${script.children}
|
|
396
|
+
html += `>${script.children}<\/script>`;
|
|
392
397
|
return html;
|
|
393
398
|
}).join("\n");
|
|
394
399
|
}
|
|
@@ -402,15 +407,15 @@ async function resolveOrchestrator(globalServer, url, res) {
|
|
|
402
407
|
"{__VITEST_INJECTOR__}",
|
|
403
408
|
"{__VITEST_ERROR_CATCHER__}",
|
|
404
409
|
"{__VITEST_SCRIPTS__}",
|
|
405
|
-
`<script type="module" crossorigin src="${base}${jsEntry}"
|
|
410
|
+
`<script type="module" crossorigin src="${base}${jsEntry}"><\/script>`
|
|
406
411
|
].join("\n"));
|
|
407
412
|
}
|
|
408
413
|
return replacer(baseHtml, {
|
|
409
414
|
__VITEST_FAVICON__: globalServer.faviconUrl,
|
|
410
415
|
__VITEST_TITLE__: "Vitest Browser Runner",
|
|
411
416
|
__VITEST_SCRIPTS__: globalServer.orchestratorScripts,
|
|
412
|
-
__VITEST_INJECTOR__: `<script type="module">${injector}
|
|
413
|
-
__VITEST_ERROR_CATCHER__: `<script type="module" src="${globalServer.errorCatcherUrl}"
|
|
417
|
+
__VITEST_INJECTOR__: `<script type="module">${injector}<\/script>`,
|
|
418
|
+
__VITEST_ERROR_CATCHER__: `<script type="module" src="${globalServer.errorCatcherUrl}"><\/script>`,
|
|
414
419
|
__VITEST_SESSION_ID__: JSON.stringify(sessionId)
|
|
415
420
|
});
|
|
416
421
|
}
|
|
@@ -671,6 +676,31 @@ var BrowserPlugin = (parentServer, base = "/") => {
|
|
|
671
676
|
}
|
|
672
677
|
next();
|
|
673
678
|
});
|
|
679
|
+
// handle attachments the same way as in packages/ui/node/index.ts
|
|
680
|
+
server.middlewares.use((req, res, next) => {
|
|
681
|
+
if (!req.url) {
|
|
682
|
+
return next();
|
|
683
|
+
}
|
|
684
|
+
const url = new URL(req.url, "http://localhost");
|
|
685
|
+
if (url.pathname !== "/__vitest_attachment__") {
|
|
686
|
+
return next();
|
|
687
|
+
}
|
|
688
|
+
const path = url.searchParams.get("path");
|
|
689
|
+
const contentType = url.searchParams.get("contentType");
|
|
690
|
+
if (!isValidApiRequest(parentServer.config, req) || !contentType || !path) {
|
|
691
|
+
return next();
|
|
692
|
+
}
|
|
693
|
+
const fsPath = decodeURIComponent(path);
|
|
694
|
+
if (!isFileServingAllowed(parentServer.vite.config, fsPath)) {
|
|
695
|
+
return next();
|
|
696
|
+
}
|
|
697
|
+
try {
|
|
698
|
+
res.setHeader("content-type", contentType);
|
|
699
|
+
return createReadStream(fsPath).pipe(res).on("close", () => res.end());
|
|
700
|
+
} catch (err) {
|
|
701
|
+
return next(err);
|
|
702
|
+
}
|
|
703
|
+
});
|
|
674
704
|
}
|
|
675
705
|
},
|
|
676
706
|
{
|
|
@@ -680,8 +710,7 @@ var BrowserPlugin = (parentServer, base = "/") => {
|
|
|
680
710
|
// this plugin can be used in different projects, but all of them
|
|
681
711
|
// have the same `include` pattern, so it doesn't matter which project we use
|
|
682
712
|
const project = parentServer.project;
|
|
683
|
-
const { testFiles:
|
|
684
|
-
const browserTestFiles = allTestFiles.filter((file) => getFilePoolName(project, file) === "browser");
|
|
713
|
+
const { testFiles: browserTestFiles } = await project.globTestFiles();
|
|
685
714
|
const setupFiles = toArray(project.config.setupFiles);
|
|
686
715
|
// replace env values - cannot be reassign at runtime
|
|
687
716
|
const define = {};
|
|
@@ -723,7 +752,7 @@ var BrowserPlugin = (parentServer, base = "/") => {
|
|
|
723
752
|
entries.push(project.config.diff);
|
|
724
753
|
}
|
|
725
754
|
if (parentServer.vitest.coverageProvider) {
|
|
726
|
-
const coverage = parentServer.vitest.
|
|
755
|
+
const coverage = parentServer.vitest._coverageOptions;
|
|
727
756
|
const provider = coverage.provider;
|
|
728
757
|
if (provider === "v8") {
|
|
729
758
|
const path = tryResolve("@vitest/coverage-v8", [parentServer.config.root]);
|
|
@@ -744,9 +773,9 @@ var BrowserPlugin = (parentServer, base = "/") => {
|
|
|
744
773
|
const include = [
|
|
745
774
|
"vitest > expect-type",
|
|
746
775
|
"vitest > @vitest/snapshot > magic-string",
|
|
747
|
-
"vitest > chai",
|
|
748
|
-
"vitest > chai > loupe",
|
|
749
776
|
"vitest > @vitest/runner > strip-literal",
|
|
777
|
+
"vitest > @vitest/expect > chai",
|
|
778
|
+
"vitest > @vitest/expect > chai > loupe",
|
|
750
779
|
"vitest > @vitest/utils > loupe",
|
|
751
780
|
"@vitest/browser > @testing-library/user-event",
|
|
752
781
|
"@vitest/browser > @testing-library/dom"
|
|
@@ -835,7 +864,7 @@ var BrowserPlugin = (parentServer, base = "/") => {
|
|
|
835
864
|
viteConfig.esbuild ||= {};
|
|
836
865
|
viteConfig.esbuild.legalComments = "inline";
|
|
837
866
|
}
|
|
838
|
-
const defaultPort = parentServer.vitest.
|
|
867
|
+
const defaultPort = parentServer.vitest.state._data.browserLastPort++;
|
|
839
868
|
const api = resolveApiServerConfig(viteConfig.test?.browser || {}, defaultPort);
|
|
840
869
|
viteConfig.server = {
|
|
841
870
|
...viteConfig.server,
|
|
@@ -859,7 +888,7 @@ var BrowserPlugin = (parentServer, base = "/") => {
|
|
|
859
888
|
return;
|
|
860
889
|
}
|
|
861
890
|
const s = new MagicString(code, { filename });
|
|
862
|
-
s.prepend(`import.meta
|
|
891
|
+
s.prepend(`Object.defineProperty(import.meta, 'vitest', { get() { return typeof __vitest_worker__ !== 'undefined' && __vitest_worker__.filepath === "${filename.replace(/"/g, "\\\"")}" ? __vitest_index__ : undefined } });\n`);
|
|
863
892
|
return {
|
|
864
893
|
code: s.toString(),
|
|
865
894
|
map: s.generateMap({ hires: true })
|
|
@@ -890,10 +919,6 @@ var BrowserPlugin = (parentServer, base = "/") => {
|
|
|
890
919
|
if (!projectBrowser) {
|
|
891
920
|
return;
|
|
892
921
|
}
|
|
893
|
-
if (!parentServer.testerScripts) {
|
|
894
|
-
const testerScripts = await parentServer.formatScripts(parentServer.config.browser.testerScripts);
|
|
895
|
-
parentServer.testerScripts = testerScripts;
|
|
896
|
-
}
|
|
897
922
|
const stateJs = typeof parentServer.stateJs === "string" ? parentServer.stateJs : await parentServer.stateJs;
|
|
898
923
|
const testerTags = [];
|
|
899
924
|
const isDefaultTemplate = resolve(distRoot, "client/tester/tester.html") === projectBrowser.testerFilepath;
|
|
@@ -976,7 +1001,6 @@ body {
|
|
|
976
1001
|
},
|
|
977
1002
|
injectTo: "head"
|
|
978
1003
|
} : null,
|
|
979
|
-
...parentServer.testerScripts,
|
|
980
1004
|
...testerTags
|
|
981
1005
|
].filter((s) => s != null);
|
|
982
1006
|
}
|
|
@@ -1027,7 +1051,8 @@ function getRequire() {
|
|
|
1027
1051
|
}
|
|
1028
1052
|
function resolveCoverageFolder(vitest) {
|
|
1029
1053
|
const options = vitest.config;
|
|
1030
|
-
const
|
|
1054
|
+
const coverageOptions = vitest._coverageOptions;
|
|
1055
|
+
const htmlReporter = coverageOptions?.enabled ? toArray(options.coverage.reporter).find((reporter) => {
|
|
1031
1056
|
if (typeof reporter === "string") {
|
|
1032
1057
|
return reporter === "html";
|
|
1033
1058
|
}
|
|
@@ -1037,7 +1062,7 @@ function resolveCoverageFolder(vitest) {
|
|
|
1037
1062
|
return undefined;
|
|
1038
1063
|
}
|
|
1039
1064
|
// reportsDirectory not resolved yet
|
|
1040
|
-
const root = resolve(options.root || process.cwd(),
|
|
1065
|
+
const root = resolve(options.root || process.cwd(), coverageOptions.reportsDirectory || coverageConfigDefaults.reportsDirectory);
|
|
1041
1066
|
const subdir = Array.isArray(htmlReporter) && htmlReporter.length > 1 && "subdir" in htmlReporter[1] ? htmlReporter[1].subdir : undefined;
|
|
1042
1067
|
if (!subdir || typeof subdir !== "string") {
|
|
1043
1068
|
return [root, `/${basename(root)}/`];
|
|
@@ -1650,7 +1675,7 @@ _Mime_extensionToType = new WeakMap(), _Mime_typeToExtension = new WeakMap(), _M
|
|
|
1650
1675
|
var mime = new Mime(types)._freeze();
|
|
1651
1676
|
|
|
1652
1677
|
function assertFileAccess(path, project) {
|
|
1653
|
-
if (!isFileServingAllowed(path, project.vite) && !isFileServingAllowed(path, project.vitest.
|
|
1678
|
+
if (!isFileServingAllowed(path, project.vite) && !isFileServingAllowed(path, project.vitest.vite)) {
|
|
1654
1679
|
throw new Error(`Access denied to "${path}". See Vite config documentation for "server.fs": https://vitejs.dev/config/server-options.html#server-fs-strict.`);
|
|
1655
1680
|
}
|
|
1656
1681
|
}
|
|
@@ -2353,44 +2378,73 @@ function selectAll() {
|
|
|
2353
2378
|
}
|
|
2354
2379
|
|
|
2355
2380
|
const screenshot = async (context, name, options = {}) => {
|
|
2356
|
-
if (!context.testPath) {
|
|
2357
|
-
throw new Error(`Cannot take a screenshot without a test path`);
|
|
2358
|
-
}
|
|
2359
2381
|
options.save ??= true;
|
|
2360
2382
|
if (!options.save) {
|
|
2361
2383
|
options.base64 = true;
|
|
2362
2384
|
}
|
|
2363
|
-
const
|
|
2385
|
+
const { buffer, path } = await takeScreenshot(context, name, options);
|
|
2386
|
+
return returnResult(options, path, buffer);
|
|
2387
|
+
};
|
|
2388
|
+
/**
|
|
2389
|
+
* Takes a screenshot using the provided browser context and returns a buffer and the expected screenshot path.
|
|
2390
|
+
*
|
|
2391
|
+
* **Note**: the returned `path` indicates where the screenshot *might* be found.
|
|
2392
|
+
* It is not guaranteed to exist, especially if `options.save` is `false`.
|
|
2393
|
+
*
|
|
2394
|
+
* @throws {Error} If the function is not called within a test or if the browser provider does not support screenshots.
|
|
2395
|
+
*/
|
|
2396
|
+
async function takeScreenshot(context, name, options) {
|
|
2397
|
+
if (!context.testPath) {
|
|
2398
|
+
throw new Error(`Cannot take a screenshot without a test path`);
|
|
2399
|
+
}
|
|
2400
|
+
const path = resolveScreenshotPath(context.testPath, name, context.project.config, options.path);
|
|
2364
2401
|
const savePath = normalize$1(path);
|
|
2365
2402
|
await mkdir(dirname(path), { recursive: true });
|
|
2366
2403
|
if (context.provider instanceof PlaywrightBrowserProvider) {
|
|
2404
|
+
const mask = options.mask?.map((selector) => context.iframe.locator(selector));
|
|
2367
2405
|
if (options.element) {
|
|
2368
2406
|
const { element: selector,...config } = options;
|
|
2369
|
-
const element = context.iframe.locator(
|
|
2407
|
+
const element = context.iframe.locator(selector);
|
|
2370
2408
|
const buffer = await element.screenshot({
|
|
2371
2409
|
...config,
|
|
2410
|
+
mask,
|
|
2372
2411
|
path: options.save ? savePath : undefined
|
|
2373
2412
|
});
|
|
2374
|
-
return
|
|
2413
|
+
return {
|
|
2414
|
+
buffer,
|
|
2415
|
+
path
|
|
2416
|
+
};
|
|
2375
2417
|
}
|
|
2376
2418
|
const buffer = await context.iframe.locator("body").screenshot({
|
|
2377
2419
|
...options,
|
|
2420
|
+
mask,
|
|
2378
2421
|
path: options.save ? savePath : undefined
|
|
2379
2422
|
});
|
|
2380
|
-
return
|
|
2423
|
+
return {
|
|
2424
|
+
buffer,
|
|
2425
|
+
path
|
|
2426
|
+
};
|
|
2381
2427
|
}
|
|
2382
2428
|
if (context.provider instanceof WebdriverBrowserProvider) {
|
|
2383
2429
|
const page = context.provider.browser;
|
|
2384
2430
|
const element = !options.element ? await page.$("body") : await page.$(`${options.element}`);
|
|
2385
|
-
|
|
2431
|
+
// webdriverio expects the path to contain the extension and only works with PNG files
|
|
2432
|
+
const savePathWithExtension = savePath.endsWith(".png") ? savePath : `${savePath}.png`;
|
|
2433
|
+
const buffer = await element.saveScreenshot(savePathWithExtension);
|
|
2386
2434
|
if (!options.save) {
|
|
2387
|
-
await rm(
|
|
2435
|
+
await rm(savePathWithExtension, { force: true });
|
|
2388
2436
|
}
|
|
2389
|
-
return
|
|
2437
|
+
return {
|
|
2438
|
+
buffer,
|
|
2439
|
+
path
|
|
2440
|
+
};
|
|
2390
2441
|
}
|
|
2391
2442
|
throw new Error(`Provider "${context.provider.name}" does not support screenshots`);
|
|
2392
|
-
}
|
|
2393
|
-
function resolveScreenshotPath(testPath, name, config) {
|
|
2443
|
+
}
|
|
2444
|
+
function resolveScreenshotPath(testPath, name, config, customPath) {
|
|
2445
|
+
if (customPath) {
|
|
2446
|
+
return resolve(dirname(testPath), customPath);
|
|
2447
|
+
}
|
|
2394
2448
|
const dir = dirname(testPath);
|
|
2395
2449
|
const base = basename(testPath);
|
|
2396
2450
|
if (config.browser.screenshotDirectory) {
|
|
@@ -2411,6 +2465,401 @@ function returnResult(options, path, buffer) {
|
|
|
2411
2465
|
return path;
|
|
2412
2466
|
}
|
|
2413
2467
|
|
|
2468
|
+
const codec = {
|
|
2469
|
+
decode: (buffer, options) => {
|
|
2470
|
+
const { data, alpha, bpp, color, colorType, depth, height, interlace, palette, width } = PNG.sync.read(Buffer.isBuffer(buffer) ? buffer : Buffer.from(buffer), options);
|
|
2471
|
+
return {
|
|
2472
|
+
metadata: {
|
|
2473
|
+
alpha,
|
|
2474
|
+
bpp,
|
|
2475
|
+
color,
|
|
2476
|
+
colorType,
|
|
2477
|
+
depth,
|
|
2478
|
+
height,
|
|
2479
|
+
interlace,
|
|
2480
|
+
palette,
|
|
2481
|
+
width
|
|
2482
|
+
},
|
|
2483
|
+
data
|
|
2484
|
+
};
|
|
2485
|
+
},
|
|
2486
|
+
encode: ({ data, metadata: { height, width } }, options) => {
|
|
2487
|
+
const png = new PNG({
|
|
2488
|
+
height,
|
|
2489
|
+
width
|
|
2490
|
+
});
|
|
2491
|
+
png.data = Buffer.isBuffer(data) ? data : Buffer.from(data);
|
|
2492
|
+
return PNG.sync.write(png, options);
|
|
2493
|
+
}
|
|
2494
|
+
};
|
|
2495
|
+
|
|
2496
|
+
function getCodec(type) {
|
|
2497
|
+
switch (type) {
|
|
2498
|
+
case "png": return codec;
|
|
2499
|
+
default: throw new Error(`No codec found for type ${type}`);
|
|
2500
|
+
}
|
|
2501
|
+
}
|
|
2502
|
+
|
|
2503
|
+
const defaultOptions$1 = {
|
|
2504
|
+
allowedMismatchedPixelRatio: undefined,
|
|
2505
|
+
allowedMismatchedPixels: undefined,
|
|
2506
|
+
threshold: .1,
|
|
2507
|
+
includeAA: false,
|
|
2508
|
+
alpha: .1,
|
|
2509
|
+
aaColor: [
|
|
2510
|
+
255,
|
|
2511
|
+
255,
|
|
2512
|
+
0
|
|
2513
|
+
],
|
|
2514
|
+
diffColor: [
|
|
2515
|
+
255,
|
|
2516
|
+
0,
|
|
2517
|
+
0
|
|
2518
|
+
],
|
|
2519
|
+
diffColorAlt: undefined,
|
|
2520
|
+
diffMask: false
|
|
2521
|
+
};
|
|
2522
|
+
const pixelmatch = (reference, actual, { createDiff,...options }) => {
|
|
2523
|
+
if (reference.metadata.height !== actual.metadata.height || reference.metadata.width !== actual.metadata.width) {
|
|
2524
|
+
return {
|
|
2525
|
+
pass: false,
|
|
2526
|
+
diff: null,
|
|
2527
|
+
message: `Expected image dimensions to be ${reference.metadata.width}×${reference.metadata.height}px, but received ${actual.metadata.width}×${actual.metadata.height}px.`
|
|
2528
|
+
};
|
|
2529
|
+
}
|
|
2530
|
+
const optionsWithDefaults = {
|
|
2531
|
+
...defaultOptions$1,
|
|
2532
|
+
...options
|
|
2533
|
+
};
|
|
2534
|
+
const diffBuffer = createDiff ? new Uint8Array(reference.data.length) : undefined;
|
|
2535
|
+
const mismatchedPixels = pm(reference.data, actual.data, diffBuffer, reference.metadata.width, reference.metadata.height, optionsWithDefaults);
|
|
2536
|
+
const imageArea = reference.metadata.width * reference.metadata.height;
|
|
2537
|
+
let allowedMismatchedPixels = Math.min(optionsWithDefaults.allowedMismatchedPixels ?? Number.POSITIVE_INFINITY, (optionsWithDefaults.allowedMismatchedPixelRatio ?? Number.POSITIVE_INFINITY) * imageArea);
|
|
2538
|
+
if (allowedMismatchedPixels === Number.POSITIVE_INFINITY) {
|
|
2539
|
+
allowedMismatchedPixels = 0;
|
|
2540
|
+
}
|
|
2541
|
+
const pass = mismatchedPixels <= allowedMismatchedPixels;
|
|
2542
|
+
return {
|
|
2543
|
+
pass,
|
|
2544
|
+
diff: diffBuffer ?? null,
|
|
2545
|
+
message: pass ? null : `${mismatchedPixels} pixels (ratio ${(Math.ceil(mismatchedPixels / imageArea * 100) / 100).toFixed(2)}) differ.`
|
|
2546
|
+
};
|
|
2547
|
+
};
|
|
2548
|
+
|
|
2549
|
+
const comparators = new Map(Object.entries({ pixelmatch }));
|
|
2550
|
+
function getComparator(comparator) {
|
|
2551
|
+
if (comparators.has(comparator)) {
|
|
2552
|
+
return comparators.get(comparator);
|
|
2553
|
+
}
|
|
2554
|
+
throw new Error(`Unrecognized comparator ${comparator}`);
|
|
2555
|
+
}
|
|
2556
|
+
|
|
2557
|
+
const defaultOptions = {
|
|
2558
|
+
comparatorName: "pixelmatch",
|
|
2559
|
+
comparatorOptions: {},
|
|
2560
|
+
screenshotOptions: {
|
|
2561
|
+
animations: "disabled",
|
|
2562
|
+
caret: "hide",
|
|
2563
|
+
fullPage: false,
|
|
2564
|
+
maskColor: "#ff00ff",
|
|
2565
|
+
omitBackground: false,
|
|
2566
|
+
scale: "device"
|
|
2567
|
+
},
|
|
2568
|
+
timeout: 5e3,
|
|
2569
|
+
resolveDiffPath: ({ arg, ext, root, attachmentsDir, browserName, platform, testFileDirectory, testFileName }) => resolve(root, attachmentsDir, testFileDirectory, testFileName, `${arg}-${browserName}-${platform}${ext}`),
|
|
2570
|
+
resolveScreenshotPath: ({ arg, ext, root, screenshotDirectory, testFileDirectory, testFileName, browserName }) => resolve(root, testFileDirectory, screenshotDirectory, testFileName, `${arg}-${browserName}-${platform}${ext}`)
|
|
2571
|
+
};
|
|
2572
|
+
const supportedExtensions = ["png"];
|
|
2573
|
+
function resolveOptions({ context, name, options, testName }) {
|
|
2574
|
+
if (context.testPath === undefined) {
|
|
2575
|
+
throw new Error("`resolveOptions` has to be used in a test file");
|
|
2576
|
+
}
|
|
2577
|
+
const resolvedOptions = deepMerge(Object.create(null), defaultOptions, context.project.config.browser.expect?.toMatchScreenshot ?? {}, options);
|
|
2578
|
+
const extensionFromName = extname(name);
|
|
2579
|
+
// technically the type is a lie, but we check beneath and reassign otherwise
|
|
2580
|
+
let extension = extensionFromName.replace(/^\./, "");
|
|
2581
|
+
// when `type` will be supported in `screenshotOptions`:
|
|
2582
|
+
// - `'png'` should end up in `defaultOptions.screenshotOptions.type`
|
|
2583
|
+
// - this condition should be switched around
|
|
2584
|
+
// - the assignment should be `resolvedOptions.screenshotOptions.type = extension`
|
|
2585
|
+
// - everything using `extension` should use `resolvedOptions.screenshotOptions.type`
|
|
2586
|
+
if (supportedExtensions.includes(extension) === false) {
|
|
2587
|
+
extension = "png";
|
|
2588
|
+
}
|
|
2589
|
+
const { root } = context.project.serializedConfig;
|
|
2590
|
+
const resolvePathData = {
|
|
2591
|
+
arg: sanitizeArg(
|
|
2592
|
+
// remove the extension only if it ends up being used
|
|
2593
|
+
extensionFromName.endsWith(extension) ? basename(name, extensionFromName) : name
|
|
2594
|
+
),
|
|
2595
|
+
ext: `.${extension}`,
|
|
2596
|
+
platform: platform(),
|
|
2597
|
+
root,
|
|
2598
|
+
screenshotDirectory: relative(root, join(root, context.project.config.browser.screenshotDirectory ?? "__screenshots__")),
|
|
2599
|
+
attachmentsDir: relative(root, context.project.config.attachmentsDir),
|
|
2600
|
+
testFileDirectory: relative(root, dirname(context.testPath)),
|
|
2601
|
+
testFileName: basename(context.testPath),
|
|
2602
|
+
testName: sanitize(testName, false),
|
|
2603
|
+
browserName: context.project.config.browser.name
|
|
2604
|
+
};
|
|
2605
|
+
return {
|
|
2606
|
+
codec: getCodec(extension),
|
|
2607
|
+
comparator: getComparator(resolvedOptions.comparatorName),
|
|
2608
|
+
resolvedOptions,
|
|
2609
|
+
paths: {
|
|
2610
|
+
reference: resolvedOptions.resolveScreenshotPath(resolvePathData),
|
|
2611
|
+
get diffs() {
|
|
2612
|
+
const diffs = {
|
|
2613
|
+
reference: resolvedOptions.resolveDiffPath({
|
|
2614
|
+
...resolvePathData,
|
|
2615
|
+
arg: `${resolvePathData.arg}-reference`
|
|
2616
|
+
}),
|
|
2617
|
+
actual: resolvedOptions.resolveDiffPath({
|
|
2618
|
+
...resolvePathData,
|
|
2619
|
+
arg: `${resolvePathData.arg}-actual`
|
|
2620
|
+
}),
|
|
2621
|
+
diff: resolvedOptions.resolveDiffPath({
|
|
2622
|
+
...resolvePathData,
|
|
2623
|
+
arg: `${resolvePathData.arg}-diff`
|
|
2624
|
+
})
|
|
2625
|
+
};
|
|
2626
|
+
Object.defineProperty(this, "diffs", { value: diffs });
|
|
2627
|
+
return diffs;
|
|
2628
|
+
}
|
|
2629
|
+
}
|
|
2630
|
+
};
|
|
2631
|
+
}
|
|
2632
|
+
/**
|
|
2633
|
+
* Sanitizes a string by removing or transforming characters to ensure it is
|
|
2634
|
+
* safe for use as a filename or path segment. It supports two modes:
|
|
2635
|
+
*
|
|
2636
|
+
* 1. Non-path mode (`keepPaths === false`):
|
|
2637
|
+
* - Replaces one or more whitespace characters (`\s+`) with a single hyphen (`-`).
|
|
2638
|
+
* - Removes any character that is not a word character (`\w`) or a hyphen (`-`).
|
|
2639
|
+
* - Collapses multiple consecutive hyphens (`-{2,}`) into a single hyphen.
|
|
2640
|
+
*
|
|
2641
|
+
* 2. Path-preserving mode (`keepPaths === true`):
|
|
2642
|
+
* - Splits the input string on the path separator.
|
|
2643
|
+
* - Sanitizes each path segment individually in non-path mode.
|
|
2644
|
+
* - Joins the sanitized segments back together.
|
|
2645
|
+
*
|
|
2646
|
+
* @param input - The raw string to sanitize.
|
|
2647
|
+
* @param keepPaths - If `false`, performs a flat sanitization (drops path segments).
|
|
2648
|
+
* If `true`, treats `input` as a path: each segment is sanitized independently,
|
|
2649
|
+
* preserving separators.
|
|
2650
|
+
*/
|
|
2651
|
+
function sanitize(input, keepPaths) {
|
|
2652
|
+
if (keepPaths === false) {
|
|
2653
|
+
return input.replace(/\s+/g, "-").replace(/[^\w-]+/g, "").replace(/-{2,}/g, "-");
|
|
2654
|
+
}
|
|
2655
|
+
return input.split("/").map((path) => sanitize(path, false)).join("/");
|
|
2656
|
+
}
|
|
2657
|
+
/**
|
|
2658
|
+
* Takes a string, treats it as a potential path or filename, and ensures it cannot
|
|
2659
|
+
* escape the root directory or contain invalid characters. Internally, it:
|
|
2660
|
+
*
|
|
2661
|
+
* 1. Prepends the path separator to the raw input to form a path-like string.
|
|
2662
|
+
* 2. Uses {@linkcode relative|relative('/', <that-path>)} to compute a relative
|
|
2663
|
+
* path from the root, which effectively strips any leading separators and prevents
|
|
2664
|
+
* traversal above the root.
|
|
2665
|
+
* 3. Passes the resulting relative path into {@linkcode sanitize|sanitize(..., true)},
|
|
2666
|
+
* preserving any path separators but sanitizing each segment.
|
|
2667
|
+
*
|
|
2668
|
+
* @param input - The raw string to clean.
|
|
2669
|
+
*/
|
|
2670
|
+
function sanitizeArg(input) {
|
|
2671
|
+
return sanitize(relative("/", join("/", input)), true);
|
|
2672
|
+
}
|
|
2673
|
+
/**
|
|
2674
|
+
* Takes a screenshot and decodes it using the provided codec.
|
|
2675
|
+
*
|
|
2676
|
+
* The screenshot is taken as a base64 string and then decoded into the format
|
|
2677
|
+
* expected by the comparator.
|
|
2678
|
+
*
|
|
2679
|
+
* @returns `Promise` resolving to the decoded screenshot data
|
|
2680
|
+
*/
|
|
2681
|
+
function takeDecodedScreenshot({ codec, context, element, name, screenshotOptions }) {
|
|
2682
|
+
return takeScreenshot(context, name, {
|
|
2683
|
+
...screenshotOptions,
|
|
2684
|
+
save: false,
|
|
2685
|
+
element
|
|
2686
|
+
}).then(({ buffer }) => codec.decode(buffer, {}));
|
|
2687
|
+
}
|
|
2688
|
+
/**
|
|
2689
|
+
* Creates a promise that resolves to `null` after the specified timeout.
|
|
2690
|
+
* If the timeout is `0`, the promise resolves immediately.
|
|
2691
|
+
*
|
|
2692
|
+
* @param timeout - The delay in milliseconds before the promise resolves
|
|
2693
|
+
* @returns `Promise` that resolves to `null` after the timeout
|
|
2694
|
+
*/
|
|
2695
|
+
function asyncTimeout(timeout) {
|
|
2696
|
+
return new Promise((resolve) => {
|
|
2697
|
+
if (timeout === 0) {
|
|
2698
|
+
resolve(null);
|
|
2699
|
+
} else {
|
|
2700
|
+
setTimeout(() => resolve(null), timeout);
|
|
2701
|
+
}
|
|
2702
|
+
});
|
|
2703
|
+
}
|
|
2704
|
+
|
|
2705
|
+
const screenshotMatcher = async (context, name, testName, options) => {
|
|
2706
|
+
if (!context.testPath) {
|
|
2707
|
+
throw new Error(`Cannot compare screenshots without a test path`);
|
|
2708
|
+
}
|
|
2709
|
+
const { element } = options;
|
|
2710
|
+
const { codec, comparator, paths, resolvedOptions: { comparatorOptions, screenshotOptions, timeout } } = resolveOptions({
|
|
2711
|
+
context,
|
|
2712
|
+
name,
|
|
2713
|
+
testName,
|
|
2714
|
+
options
|
|
2715
|
+
});
|
|
2716
|
+
const referenceFile = await readFile$1(paths.reference).catch(() => null);
|
|
2717
|
+
const reference = referenceFile && await codec.decode(await readFile$1(paths.reference), {});
|
|
2718
|
+
const abortController = new AbortController();
|
|
2719
|
+
const stableScreenshot = getStableScreenshots({
|
|
2720
|
+
codec,
|
|
2721
|
+
comparator,
|
|
2722
|
+
comparatorOptions,
|
|
2723
|
+
context,
|
|
2724
|
+
element,
|
|
2725
|
+
name: `${Date.now()}-${basename(paths.reference)}`,
|
|
2726
|
+
reference,
|
|
2727
|
+
screenshotOptions,
|
|
2728
|
+
signal: abortController.signal
|
|
2729
|
+
});
|
|
2730
|
+
const value = await (timeout === 0 ? stableScreenshot : Promise.race([stableScreenshot, asyncTimeout(timeout).finally(() => {
|
|
2731
|
+
abortController.abort();
|
|
2732
|
+
})]));
|
|
2733
|
+
// case #01
|
|
2734
|
+
// - impossible to get a stable screenshot to compare against
|
|
2735
|
+
// - fail
|
|
2736
|
+
if (value === null || value.actual === null) {
|
|
2737
|
+
return {
|
|
2738
|
+
pass: false,
|
|
2739
|
+
reference: referenceFile && paths.reference,
|
|
2740
|
+
actual: null,
|
|
2741
|
+
diff: null,
|
|
2742
|
+
message: `Could not capture a stable screenshot within ${timeout}ms.`
|
|
2743
|
+
};
|
|
2744
|
+
}
|
|
2745
|
+
const { updateSnapshot } = context.project.serializedConfig.snapshotOptions;
|
|
2746
|
+
// if there's no reference or if we want to update snapshots, we have to finish the comparison early
|
|
2747
|
+
if (reference === null || updateSnapshot === "all") {
|
|
2748
|
+
const shouldCreateReference = updateSnapshot !== "none";
|
|
2749
|
+
const referencePath = shouldCreateReference ? paths.reference : paths.diffs.reference;
|
|
2750
|
+
await writeScreenshot(referencePath, await codec.encode(value.actual, {}));
|
|
2751
|
+
// case #02
|
|
2752
|
+
// - got a stable screenshot, but there is no reference and we don't want to update screenshots
|
|
2753
|
+
// - fail
|
|
2754
|
+
if (updateSnapshot !== "all") {
|
|
2755
|
+
return {
|
|
2756
|
+
pass: false,
|
|
2757
|
+
reference: referencePath,
|
|
2758
|
+
actual: null,
|
|
2759
|
+
diff: null,
|
|
2760
|
+
message: `No existing reference screenshot found${shouldCreateReference ? "; a new one was created. Review it before running tests again." : "."}`
|
|
2761
|
+
};
|
|
2762
|
+
}
|
|
2763
|
+
// case #03
|
|
2764
|
+
// - got a stable screenshot, there is no reference, but we want to update screenshots
|
|
2765
|
+
// - pass
|
|
2766
|
+
return { pass: true };
|
|
2767
|
+
}
|
|
2768
|
+
// case #04
|
|
2769
|
+
// - got a stable screenshot with no retries and there's a reference
|
|
2770
|
+
// - pass
|
|
2771
|
+
if (referenceFile && value.retries === 0) {
|
|
2772
|
+
return { pass: true };
|
|
2773
|
+
}
|
|
2774
|
+
const finalResult = await comparator(reference, value.actual, {
|
|
2775
|
+
createDiff: true,
|
|
2776
|
+
...comparatorOptions
|
|
2777
|
+
});
|
|
2778
|
+
if (finalResult.pass === false && finalResult.diff !== null) {
|
|
2779
|
+
const diff = await codec.encode({
|
|
2780
|
+
data: finalResult.diff,
|
|
2781
|
+
metadata: {
|
|
2782
|
+
height: reference.metadata.height,
|
|
2783
|
+
width: reference.metadata.width
|
|
2784
|
+
}
|
|
2785
|
+
}, {});
|
|
2786
|
+
await writeScreenshot(paths.diffs.diff, diff);
|
|
2787
|
+
}
|
|
2788
|
+
// case #05
|
|
2789
|
+
// - reference matches stable screenshot
|
|
2790
|
+
// - pass
|
|
2791
|
+
if (finalResult.pass === true) {
|
|
2792
|
+
return { pass: true };
|
|
2793
|
+
}
|
|
2794
|
+
const actual = await codec.encode(value.actual, {});
|
|
2795
|
+
await writeScreenshot(paths.diffs.actual, actual);
|
|
2796
|
+
// case #06
|
|
2797
|
+
// - fallback, reference does NOT match stable screenshot
|
|
2798
|
+
// - fail
|
|
2799
|
+
return {
|
|
2800
|
+
pass: false,
|
|
2801
|
+
reference: paths.reference,
|
|
2802
|
+
actual: paths.diffs.actual,
|
|
2803
|
+
diff: finalResult.diff && paths.diffs.diff,
|
|
2804
|
+
message: `Screenshot does not match the stored reference.${finalResult.message === null ? "" : `\n${finalResult.message}`}`
|
|
2805
|
+
};
|
|
2806
|
+
};
|
|
2807
|
+
async function writeScreenshot(path, image) {
|
|
2808
|
+
try {
|
|
2809
|
+
await mkdir(dirname(path), { recursive: true });
|
|
2810
|
+
await writeFile$1(path, image);
|
|
2811
|
+
} catch {
|
|
2812
|
+
throw new Error("Couldn't write file to fs");
|
|
2813
|
+
}
|
|
2814
|
+
}
|
|
2815
|
+
/**
|
|
2816
|
+
* Takes screenshots repeatedly until the page reaches a visually stable state.
|
|
2817
|
+
*
|
|
2818
|
+
* This function compares consecutive screenshots and continues taking new ones
|
|
2819
|
+
* until two consecutive screenshots match according to the provided comparator.
|
|
2820
|
+
*
|
|
2821
|
+
* The process works as follows:
|
|
2822
|
+
*
|
|
2823
|
+
* 1. Uses as baseline an optional reference screenshot or takes a new screenshot
|
|
2824
|
+
* 2. Takes a screenshot and compares with baseline
|
|
2825
|
+
* 3. If they match, the page is considered stable and the function returns
|
|
2826
|
+
* 4. If they don't match, it continues with the newer screenshot as the baseline
|
|
2827
|
+
* 5. Repeats until stability is achieved or the operation is aborted
|
|
2828
|
+
*
|
|
2829
|
+
* @returns `Promise` resolving to an object containing the retry count and
|
|
2830
|
+
* final screenshot
|
|
2831
|
+
*/
|
|
2832
|
+
async function getStableScreenshots({ codec, context, comparator, comparatorOptions, element, name, reference, screenshotOptions, signal }) {
|
|
2833
|
+
const screenshotArgument = {
|
|
2834
|
+
codec,
|
|
2835
|
+
context,
|
|
2836
|
+
element,
|
|
2837
|
+
name,
|
|
2838
|
+
screenshotOptions
|
|
2839
|
+
};
|
|
2840
|
+
let retries = 0;
|
|
2841
|
+
let decodedBaseline = reference;
|
|
2842
|
+
while (signal.aborted === false) {
|
|
2843
|
+
if (decodedBaseline === null) {
|
|
2844
|
+
decodedBaseline = takeDecodedScreenshot(screenshotArgument);
|
|
2845
|
+
}
|
|
2846
|
+
const [image1, image2] = await Promise.all([decodedBaseline, takeDecodedScreenshot(screenshotArgument)]);
|
|
2847
|
+
const comparatorResult = (await comparator(image1, image2, {
|
|
2848
|
+
...comparatorOptions,
|
|
2849
|
+
createDiff: false
|
|
2850
|
+
})).pass;
|
|
2851
|
+
decodedBaseline = image2;
|
|
2852
|
+
if (comparatorResult) {
|
|
2853
|
+
break;
|
|
2854
|
+
}
|
|
2855
|
+
retries += 1;
|
|
2856
|
+
}
|
|
2857
|
+
return {
|
|
2858
|
+
retries,
|
|
2859
|
+
actual: await decodedBaseline
|
|
2860
|
+
};
|
|
2861
|
+
}
|
|
2862
|
+
|
|
2414
2863
|
const selectOptions = async (context, selector, userValues, options = {}) => {
|
|
2415
2864
|
if (context.provider instanceof PlaywrightBrowserProvider) {
|
|
2416
2865
|
const value = userValues;
|
|
@@ -2551,7 +3000,8 @@ var builtinCommands = {
|
|
|
2551
3000
|
__vitest_dragAndDrop: dragAndDrop,
|
|
2552
3001
|
__vitest_hover: hover,
|
|
2553
3002
|
__vitest_cleanup: keyboardCleanup,
|
|
2554
|
-
__vitest_viewport: viewport
|
|
3003
|
+
__vitest_viewport: viewport,
|
|
3004
|
+
__vitest_screenshotMatcher: screenshotMatcher
|
|
2555
3005
|
};
|
|
2556
3006
|
|
|
2557
3007
|
class BrowserServerState {
|
|
@@ -2595,22 +3045,7 @@ class ProjectBrowser {
|
|
|
2595
3045
|
if (this.provider) {
|
|
2596
3046
|
return;
|
|
2597
3047
|
}
|
|
2598
|
-
|
|
2599
|
-
this.provider = new Provider();
|
|
2600
|
-
const browser = project.config.browser.name;
|
|
2601
|
-
const name = project.name ? `[${project.name}] ` : "";
|
|
2602
|
-
if (!browser) {
|
|
2603
|
-
throw new Error(`${name}Browser name is required. Please, set \`test.browser.instances[].browser\` option manually.`);
|
|
2604
|
-
}
|
|
2605
|
-
const supportedBrowsers = this.provider.getSupportedBrowsers();
|
|
2606
|
-
if (supportedBrowsers.length && !supportedBrowsers.includes(browser)) {
|
|
2607
|
-
throw new Error(`${name}Browser "${browser}" is not supported by the browser provider "${this.provider.name}". Supported browsers: ${supportedBrowsers.join(", ")}.`);
|
|
2608
|
-
}
|
|
2609
|
-
const providerOptions = project.config.browser.providerOptions;
|
|
2610
|
-
await this.provider.initialize(project, {
|
|
2611
|
-
browser,
|
|
2612
|
-
options: providerOptions
|
|
2613
|
-
});
|
|
3048
|
+
this.provider = await getBrowserProvider(project.config.browser, project);
|
|
2614
3049
|
}
|
|
2615
3050
|
parseErrorStacktrace(e, options = {}) {
|
|
2616
3051
|
return this.parent.parseErrorStacktrace(e, options);
|
|
@@ -2631,7 +3066,6 @@ function wrapConfig(config) {
|
|
|
2631
3066
|
|
|
2632
3067
|
class ParentBrowserProject {
|
|
2633
3068
|
orchestratorScripts;
|
|
2634
|
-
testerScripts;
|
|
2635
3069
|
faviconUrl;
|
|
2636
3070
|
prefixOrchestratorUrl;
|
|
2637
3071
|
prefixTesterUrl;
|
|
@@ -2719,7 +3153,7 @@ class ParentBrowserProject {
|
|
|
2719
3153
|
"webdriverio",
|
|
2720
3154
|
"preview"
|
|
2721
3155
|
];
|
|
2722
|
-
const providerName = project.config.browser.provider || "preview";
|
|
3156
|
+
const providerName = project.config.browser.provider?.name || "preview";
|
|
2723
3157
|
if (builtinProviders.includes(providerName)) {
|
|
2724
3158
|
this.locatorsUrl = join("/@fs/", distRoot, "locators", `${providerName}.js`);
|
|
2725
3159
|
}
|
|
@@ -2841,7 +3275,7 @@ function defaultSerialize(i) {
|
|
|
2841
3275
|
return i;
|
|
2842
3276
|
}
|
|
2843
3277
|
const defaultDeserialize = defaultSerialize;
|
|
2844
|
-
const { clearTimeout, setTimeout } = globalThis;
|
|
3278
|
+
const { clearTimeout, setTimeout: setTimeout$1 } = globalThis;
|
|
2845
3279
|
const random = Math.random.bind(Math);
|
|
2846
3280
|
function createBirpc(functions, options) {
|
|
2847
3281
|
const {
|
|
@@ -2865,10 +3299,13 @@ function createBirpc(functions, options) {
|
|
|
2865
3299
|
return functions;
|
|
2866
3300
|
if (method === "$close")
|
|
2867
3301
|
return close;
|
|
3302
|
+
if (method === "$rejectPendingCalls") {
|
|
3303
|
+
return rejectPendingCalls;
|
|
3304
|
+
}
|
|
2868
3305
|
if (method === "$closed")
|
|
2869
3306
|
return closed;
|
|
2870
3307
|
if (method === "then" && !eventNames.includes("then") && !("then" in functions))
|
|
2871
|
-
return
|
|
3308
|
+
return void 0;
|
|
2872
3309
|
const sendEvent = (...args) => {
|
|
2873
3310
|
post(serialize({ m: method, a: args, t: TYPE_REQUEST }));
|
|
2874
3311
|
};
|
|
@@ -2883,14 +3320,14 @@ function createBirpc(functions, options) {
|
|
|
2883
3320
|
try {
|
|
2884
3321
|
await _promise;
|
|
2885
3322
|
} finally {
|
|
2886
|
-
_promise =
|
|
3323
|
+
_promise = void 0;
|
|
2887
3324
|
}
|
|
2888
3325
|
}
|
|
2889
3326
|
return new Promise((resolve, reject) => {
|
|
2890
3327
|
const id = nanoid();
|
|
2891
3328
|
let timeoutId;
|
|
2892
3329
|
if (timeout >= 0) {
|
|
2893
|
-
timeoutId = setTimeout(() => {
|
|
3330
|
+
timeoutId = setTimeout$1(() => {
|
|
2894
3331
|
try {
|
|
2895
3332
|
const handleResult = options.onTimeoutError?.(method, args);
|
|
2896
3333
|
if (handleResult !== true)
|
|
@@ -2911,14 +3348,30 @@ function createBirpc(functions, options) {
|
|
|
2911
3348
|
return sendCall;
|
|
2912
3349
|
}
|
|
2913
3350
|
});
|
|
2914
|
-
function close(
|
|
3351
|
+
function close(customError) {
|
|
2915
3352
|
closed = true;
|
|
2916
3353
|
rpcPromiseMap.forEach(({ reject, method }) => {
|
|
2917
|
-
|
|
3354
|
+
const error = new Error(`[birpc] rpc is closed, cannot call "${method}"`);
|
|
3355
|
+
if (customError) {
|
|
3356
|
+
customError.cause ??= error;
|
|
3357
|
+
return reject(customError);
|
|
3358
|
+
}
|
|
3359
|
+
reject(error);
|
|
2918
3360
|
});
|
|
2919
3361
|
rpcPromiseMap.clear();
|
|
2920
3362
|
off(onMessage);
|
|
2921
3363
|
}
|
|
3364
|
+
function rejectPendingCalls(handler) {
|
|
3365
|
+
const entries = Array.from(rpcPromiseMap.values());
|
|
3366
|
+
const handlerResults = entries.map(({ method, reject }) => {
|
|
3367
|
+
if (!handler) {
|
|
3368
|
+
return reject(new Error(`[birpc]: rejected pending call "${method}".`));
|
|
3369
|
+
}
|
|
3370
|
+
return handler({ method, reject });
|
|
3371
|
+
});
|
|
3372
|
+
rpcPromiseMap.clear();
|
|
3373
|
+
return handlerResults;
|
|
3374
|
+
}
|
|
2922
3375
|
async function onMessage(data, ...extra) {
|
|
2923
3376
|
let msg;
|
|
2924
3377
|
try {
|
|
@@ -3018,12 +3471,13 @@ function setupBrowserRpc(globalServer, defaultMockerRegistry) {
|
|
|
3018
3471
|
if (!sessionId || !rpcId || projectName == null) {
|
|
3019
3472
|
return error(new Error(`[vitest] Invalid URL ${request.url}. "projectName", "sessionId" and "rpcId" queries are required.`));
|
|
3020
3473
|
}
|
|
3021
|
-
|
|
3022
|
-
|
|
3474
|
+
const sessions = vitest._browserSessions;
|
|
3475
|
+
if (!sessions.sessionIds.has(sessionId)) {
|
|
3476
|
+
const ids = [...sessions.sessionIds].join(", ");
|
|
3023
3477
|
return error(new Error(`[vitest] Unknown session id "${sessionId}". Expected one of ${ids}.`));
|
|
3024
3478
|
}
|
|
3025
3479
|
if (type === "orchestrator") {
|
|
3026
|
-
const session =
|
|
3480
|
+
const session = sessions.getSession(sessionId);
|
|
3027
3481
|
// it's possible the session was already resolved by the preview provider
|
|
3028
3482
|
session?.connected();
|
|
3029
3483
|
}
|
|
@@ -3043,7 +3497,7 @@ function setupBrowserRpc(globalServer, defaultMockerRegistry) {
|
|
|
3043
3497
|
clients.delete(rpcId);
|
|
3044
3498
|
globalServer.removeCDPHandler(rpcId);
|
|
3045
3499
|
if (type === "orchestrator") {
|
|
3046
|
-
|
|
3500
|
+
sessions.destroySession(sessionId);
|
|
3047
3501
|
}
|
|
3048
3502
|
// this will reject any hanging methods if there are any
|
|
3049
3503
|
rpc.$close(new Error(`[vitest] Browser connection was closed while running tests. Was the page closed unexpectedly?`));
|
|
@@ -3061,7 +3515,7 @@ function setupBrowserRpc(globalServer, defaultMockerRegistry) {
|
|
|
3061
3515
|
}
|
|
3062
3516
|
}
|
|
3063
3517
|
function setupClient(project, rpcId, ws) {
|
|
3064
|
-
const mockResolver = new ServerMockResolver(globalServer.vite, { moduleDirectories: project.config
|
|
3518
|
+
const mockResolver = new ServerMockResolver(globalServer.vite, { moduleDirectories: project.config?.deps?.moduleDirectories });
|
|
3065
3519
|
const mocker = project.browser?.provider.mocker;
|
|
3066
3520
|
const rpc = createBirpc({
|
|
3067
3521
|
async onUnhandledError(error, type) {
|
|
@@ -3252,11 +3706,8 @@ function setupBrowserRpc(globalServer, defaultMockerRegistry) {
|
|
|
3252
3706
|
on: (fn) => ws.on("message", fn),
|
|
3253
3707
|
eventNames: ["onCancel", "cdpEvent"],
|
|
3254
3708
|
serialize: (data) => stringify(data, stringifyReplace),
|
|
3255
|
-
timeout: -1,
|
|
3256
3709
|
deserialize: parse,
|
|
3257
|
-
|
|
3258
|
-
throw new Error(`[vitest-api]: Timeout calling "${functionName}"`);
|
|
3259
|
-
}
|
|
3710
|
+
timeout: -1
|
|
3260
3711
|
});
|
|
3261
3712
|
vitest.onCancel((reason) => rpc.onCancel(reason));
|
|
3262
3713
|
return rpc;
|
|
@@ -3349,14 +3800,15 @@ function createBrowserPool(vitest) {
|
|
|
3349
3800
|
}
|
|
3350
3801
|
const parallelPools = [];
|
|
3351
3802
|
const nonParallelPools = [];
|
|
3352
|
-
for (const
|
|
3353
|
-
if (!
|
|
3803
|
+
for (const pool of initialisedPools) {
|
|
3804
|
+
if (!pool) {
|
|
3805
|
+
// this means it was cancelled
|
|
3354
3806
|
return;
|
|
3355
3807
|
}
|
|
3356
|
-
if (
|
|
3357
|
-
parallelPools.push(
|
|
3808
|
+
if (pool.provider.mocker && pool.provider.supportsParallelism) {
|
|
3809
|
+
parallelPools.push(pool.runTests);
|
|
3358
3810
|
} else {
|
|
3359
|
-
nonParallelPools.push(
|
|
3811
|
+
nonParallelPools.push(pool.runTests);
|
|
3360
3812
|
}
|
|
3361
3813
|
}
|
|
3362
3814
|
await Promise.all(parallelPools.map((runTests) => runTests()));
|
|
@@ -3515,7 +3967,7 @@ class BrowserPool {
|
|
|
3515
3967
|
debug?.("[%s] test %s finished running", sessionId, file);
|
|
3516
3968
|
this.runNextTest(method, sessionId);
|
|
3517
3969
|
}).catch((error) => {
|
|
3518
|
-
// if user
|
|
3970
|
+
// if user cancels the test run manually, ignore the error and exit gracefully
|
|
3519
3971
|
if (this.project.vitest.isCancelling && error instanceof Error && error.message.startsWith("Browser connection was closed while running tests")) {
|
|
3520
3972
|
this.cancel();
|
|
3521
3973
|
this._promise?.resolve();
|