@vitest/browser-playwright 5.0.0-beta.5 → 5.0.0-beta.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +132 -26
- package/package.json +6 -6
package/dist/index.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { parseKeyDef, resolveScreenshotPath, defineBrowserProvider } from '@vitest/browser';
|
|
1
|
+
import { parseKeyDef, resolveScreenshotPath, assertBrowserApiWrite, assertBrowserFileAccess, defineBrowserProvider } from '@vitest/browser';
|
|
2
2
|
export { defineBrowserCommand } from '@vitest/browser';
|
|
3
3
|
import { createManualModuleSource } from '@vitest/mocker/node';
|
|
4
4
|
import c from 'tinyrainbow';
|
|
@@ -522,6 +522,8 @@ async function takeScreenshot(context, name, options) {
|
|
|
522
522
|
let savePath;
|
|
523
523
|
if (options.save) {
|
|
524
524
|
savePath = normalize(path);
|
|
525
|
+
assertBrowserApiWrite(context.project, savePath);
|
|
526
|
+
assertBrowserFileAccess(context.project, savePath);
|
|
525
527
|
await mkdir(dirname(savePath), { recursive: true });
|
|
526
528
|
}
|
|
527
529
|
const mask = options.mask?.map((selector) => getDescribedLocator(context, selector));
|
|
@@ -622,6 +624,8 @@ const startChunkTrace = async (command, { name, title }) => {
|
|
|
622
624
|
const stopChunkTrace = async (context, { name }) => {
|
|
623
625
|
if (isPlaywrightProvider(context.provider)) {
|
|
624
626
|
const path = resolveTracesPath(context, name);
|
|
627
|
+
assertBrowserApiWrite(context.project, path);
|
|
628
|
+
assertBrowserFileAccess(context.project, path);
|
|
625
629
|
context.provider.pendingTraces.delete(path);
|
|
626
630
|
await context.context.tracing.stopChunk({ path });
|
|
627
631
|
return { tracePath: path };
|
|
@@ -708,6 +712,10 @@ const deleteTracing = async (context, { traces }) => {
|
|
|
708
712
|
throw new Error(`stopChunkTrace cannot be called outside of the test file.`);
|
|
709
713
|
}
|
|
710
714
|
if (isPlaywrightProvider(context.provider)) {
|
|
715
|
+
for (const trace of traces) {
|
|
716
|
+
assertBrowserApiWrite(context.project, trace);
|
|
717
|
+
assertBrowserFileAccess(context.project, trace);
|
|
718
|
+
}
|
|
711
719
|
return Promise.all(traces.map((trace) => unlink(trace).catch((err) => {
|
|
712
720
|
if (err.code === "ENOENT") {
|
|
713
721
|
// Ignore the error if the file doesn't exist
|
|
@@ -722,6 +730,8 @@ const deleteTracing = async (context, { traces }) => {
|
|
|
722
730
|
const annotateTraces = async ({ project }, { testId, traces }) => {
|
|
723
731
|
const vitest = project.vitest;
|
|
724
732
|
await Promise.all(traces.map((trace) => {
|
|
733
|
+
assertBrowserApiWrite(project, trace);
|
|
734
|
+
assertBrowserFileAccess(project, trace);
|
|
725
735
|
const entity = vitest.state.getReportedEntityById(testId);
|
|
726
736
|
const location = entity?.location ? {
|
|
727
737
|
file: entity.module.moduleId,
|
|
@@ -766,7 +776,9 @@ const upload = async (context, selector, files, options) => {
|
|
|
766
776
|
const root = context.project.config.root;
|
|
767
777
|
const playwrightFiles = files.map((file) => {
|
|
768
778
|
if (typeof file === "string") {
|
|
769
|
-
|
|
779
|
+
const filepath = resolve(root, file);
|
|
780
|
+
assertBrowserFileAccess(context.project, filepath);
|
|
781
|
+
return filepath;
|
|
770
782
|
}
|
|
771
783
|
return {
|
|
772
784
|
name: file.name,
|
|
@@ -835,11 +847,102 @@ function playwright(options = {}) {
|
|
|
835
847
|
name: "playwright",
|
|
836
848
|
supportedBrowser: playwrightBrowsers,
|
|
837
849
|
options,
|
|
850
|
+
prewarm(ctx) {
|
|
851
|
+
prewarmBrowser(ctx, options);
|
|
852
|
+
},
|
|
838
853
|
providerFactory(project) {
|
|
839
854
|
return new PlaywrightBrowserProvider(project, options);
|
|
840
855
|
}
|
|
841
856
|
});
|
|
842
857
|
}
|
|
858
|
+
// The resolved config object is passed unchanged to the eventual TestProject,
|
|
859
|
+
// so it identifies the browser this project can adopt.
|
|
860
|
+
const warmBrowsers = new WeakMap();
|
|
861
|
+
const pendingWarmBrowsers = new WeakMap();
|
|
862
|
+
// starts importing playwright and launching the browser while the node side
|
|
863
|
+
// is still creating the vite server, so the launch latency overlaps it. The
|
|
864
|
+
// launch options are resolved by the same code as the real launch — if they
|
|
865
|
+
// still differ by the time the provider opens the browser, the warm instance
|
|
866
|
+
// is discarded, so this is always safe
|
|
867
|
+
function prewarmBrowser(project, options) {
|
|
868
|
+
const browserName = project.config.browser.name;
|
|
869
|
+
if (options.connectOptions || options.persistentContext || project.vitest.config.inspector.enabled) {
|
|
870
|
+
return;
|
|
871
|
+
}
|
|
872
|
+
if (!browserName || !playwrightBrowsers.includes(browserName)) {
|
|
873
|
+
return;
|
|
874
|
+
}
|
|
875
|
+
if (warmBrowsers.has(project.config)) {
|
|
876
|
+
return;
|
|
877
|
+
}
|
|
878
|
+
let pending = pendingWarmBrowsers.get(project.vitest);
|
|
879
|
+
if (!pending) {
|
|
880
|
+
const pendingBrowsers = new Set();
|
|
881
|
+
pending = pendingBrowsers;
|
|
882
|
+
pendingWarmBrowsers.set(project.vitest, pendingBrowsers);
|
|
883
|
+
// Browsers whose projects never initialize a provider (they have no test
|
|
884
|
+
// files to run) are cleaned up when Vitest closes.
|
|
885
|
+
project.vitest.onClose(() => closeWarmBrowsers(pendingBrowsers));
|
|
886
|
+
}
|
|
887
|
+
const launchOptions = resolveLaunchOptions(project.config.browser, project.vitest.config.inspector, options, browserName);
|
|
888
|
+
const entry = {
|
|
889
|
+
launchOptionsJson: JSON.stringify(launchOptions),
|
|
890
|
+
pending,
|
|
891
|
+
promise: (async () => {
|
|
892
|
+
debug?.("[%s] prewarming the browser", browserName);
|
|
893
|
+
const playwright = await import('playwright');
|
|
894
|
+
return playwright[browserName].launch(launchOptions);
|
|
895
|
+
})()
|
|
896
|
+
};
|
|
897
|
+
// if the warm launch fails, drop it so the real launch retries
|
|
898
|
+
// and surfaces the error through the normal path
|
|
899
|
+
entry.promise.catch(() => {
|
|
900
|
+
if (warmBrowsers.get(project.config) === entry) {
|
|
901
|
+
warmBrowsers.delete(project.config);
|
|
902
|
+
entry.pending.delete(entry);
|
|
903
|
+
}
|
|
904
|
+
});
|
|
905
|
+
pending.add(entry);
|
|
906
|
+
warmBrowsers.set(project.config, entry);
|
|
907
|
+
}
|
|
908
|
+
function takeWarmBrowser(config) {
|
|
909
|
+
const warm = warmBrowsers.get(config);
|
|
910
|
+
if (warm) {
|
|
911
|
+
warmBrowsers.delete(config);
|
|
912
|
+
warm.pending.delete(warm);
|
|
913
|
+
}
|
|
914
|
+
return warm;
|
|
915
|
+
}
|
|
916
|
+
async function closeWarmBrowsers(pending) {
|
|
917
|
+
const closing = Array.from(pending, (warm) => warm.promise.then((browser) => browser.close()).catch(() => {}));
|
|
918
|
+
pending.clear();
|
|
919
|
+
await Promise.all(closing);
|
|
920
|
+
}
|
|
921
|
+
function resolveLaunchOptions(browser, inspector, providerOptions, browserName) {
|
|
922
|
+
const launchOptions = {
|
|
923
|
+
...providerOptions.launchOptions,
|
|
924
|
+
headless: browser.headless
|
|
925
|
+
};
|
|
926
|
+
if (typeof browser.trace === "object" && browser.trace.tracesDir) {
|
|
927
|
+
launchOptions.tracesDir = browser.trace.tracesDir;
|
|
928
|
+
}
|
|
929
|
+
if (inspector.enabled) {
|
|
930
|
+
// NodeJS equivalent defaults: https://nodejs.org/en/learn/getting-started/debugging#enable-inspector
|
|
931
|
+
const port = inspector.port || 9229;
|
|
932
|
+
launchOptions.args ||= [];
|
|
933
|
+
launchOptions.args.push(`--remote-debugging-port=${port}`);
|
|
934
|
+
}
|
|
935
|
+
// start Vitest UI maximized only on supported browsers
|
|
936
|
+
if (browser.ui && browserName === "chromium") {
|
|
937
|
+
if (!launchOptions.args) {
|
|
938
|
+
launchOptions.args = [];
|
|
939
|
+
}
|
|
940
|
+
if (!launchOptions.args.includes("--start-maximized") && !launchOptions.args.includes("--start-fullscreen")) {
|
|
941
|
+
launchOptions.args.push("--start-maximized");
|
|
942
|
+
}
|
|
943
|
+
}
|
|
944
|
+
return launchOptions;
|
|
945
|
+
}
|
|
843
946
|
class PlaywrightBrowserProvider {
|
|
844
947
|
name = "playwright";
|
|
845
948
|
supportsParallelism = true;
|
|
@@ -889,36 +992,17 @@ class PlaywrightBrowserProvider {
|
|
|
889
992
|
return this.browser;
|
|
890
993
|
}
|
|
891
994
|
this.browserPromise = (async () => {
|
|
892
|
-
const options = this.project.config.browser;
|
|
893
995
|
const playwright = await import('playwright');
|
|
894
|
-
const launchOptions =
|
|
895
|
-
...this.options.launchOptions,
|
|
896
|
-
headless: options.headless
|
|
897
|
-
};
|
|
898
|
-
if (typeof options.trace === "object" && options.trace.tracesDir) {
|
|
899
|
-
launchOptions.tracesDir = options.trace?.tracesDir;
|
|
900
|
-
}
|
|
996
|
+
const launchOptions = resolveLaunchOptions(this.project.config.browser, this.project.vitest.config.inspector, this.options, this.browserName);
|
|
901
997
|
const inspector = this.project.vitest.config.inspector;
|
|
902
998
|
if (inspector.enabled) {
|
|
903
|
-
// NodeJS equivalent defaults: https://nodejs.org/en/learn/getting-started/debugging#enable-inspector
|
|
904
999
|
const port = inspector.port || 9229;
|
|
905
1000
|
const host = inspector.host || "127.0.0.1";
|
|
906
|
-
launchOptions.args ||= [];
|
|
907
|
-
launchOptions.args.push(`--remote-debugging-port=${port}`);
|
|
908
1001
|
if (host !== "localhost" && host !== "127.0.0.1" && host !== "::1") {
|
|
909
1002
|
this.project.vitest.logger.warn(`Custom inspector host "${host}" will be ignored. Chromium only allows remote debugging on localhost.`);
|
|
910
1003
|
}
|
|
911
1004
|
this.project.vitest.logger.log(`Debugger listening on ws://127.0.0.1:${port}`);
|
|
912
1005
|
}
|
|
913
|
-
// start Vitest UI maximized only on supported browsers
|
|
914
|
-
if (this.project.config.browser.ui && this.browserName === "chromium") {
|
|
915
|
-
if (!launchOptions.args) {
|
|
916
|
-
launchOptions.args = [];
|
|
917
|
-
}
|
|
918
|
-
if (!launchOptions.args.includes("--start-maximized") && !launchOptions.args.includes("--start-fullscreen")) {
|
|
919
|
-
launchOptions.args.push("--start-maximized");
|
|
920
|
-
}
|
|
921
|
-
}
|
|
922
1006
|
debug?.("[%s] initializing the browser with launch options: %O", this.browserName, launchOptions);
|
|
923
1007
|
if (this.options.connectOptions) {
|
|
924
1008
|
let { wsEndpoint, headers = {}, ...connectOptions } = this.options.connectOptions;
|
|
@@ -951,6 +1035,19 @@ class PlaywrightBrowserProvider {
|
|
|
951
1035
|
});
|
|
952
1036
|
this.browser = this.persistentContext.browser();
|
|
953
1037
|
} else {
|
|
1038
|
+
const warm = takeWarmBrowser(this.project.config);
|
|
1039
|
+
if (warm && warm.launchOptionsJson === JSON.stringify(launchOptions)) {
|
|
1040
|
+
const browser = await warm.promise.catch(() => null);
|
|
1041
|
+
if (browser?.isConnected()) {
|
|
1042
|
+
debug?.("[%s] adopting the prewarmed browser", this.browserName);
|
|
1043
|
+
this.browser = browser;
|
|
1044
|
+
this.browserPromise = null;
|
|
1045
|
+
return this.browser;
|
|
1046
|
+
}
|
|
1047
|
+
} else if (warm) {
|
|
1048
|
+
debug?.("[%s] discarding the prewarmed browser, launch options changed", this.browserName);
|
|
1049
|
+
void warm.promise.then((browser) => browser.close()).catch(() => {});
|
|
1050
|
+
}
|
|
954
1051
|
this.browser = await playwright[this.browserName].launch(launchOptions);
|
|
955
1052
|
}
|
|
956
1053
|
this.browserPromise = null;
|
|
@@ -1069,7 +1166,7 @@ class PlaywrightBrowserProvider {
|
|
|
1069
1166
|
clear: async (sessionId) => {
|
|
1070
1167
|
const page = this.getPage(sessionId);
|
|
1071
1168
|
const ids = sessionIds.get(sessionId) ?? new Set();
|
|
1072
|
-
const promises =
|
|
1169
|
+
const promises = Array.from(ids, (id) => {
|
|
1073
1170
|
const key = predicateKey(sessionId, id);
|
|
1074
1171
|
const predicate = idPredicates.get(key);
|
|
1075
1172
|
if (predicate) {
|
|
@@ -1111,7 +1208,11 @@ class PlaywrightBrowserProvider {
|
|
|
1111
1208
|
...contextOptions,
|
|
1112
1209
|
ignoreHTTPSErrors: true
|
|
1113
1210
|
};
|
|
1114
|
-
|
|
1211
|
+
// A `null` viewport lets the page adopt the real window size, which is only
|
|
1212
|
+
// meaningful for a headed UI. In headless mode there is no real window, so it
|
|
1213
|
+
// would inherit the host's device scale factor and produce screenshots that
|
|
1214
|
+
// differ from non-UI runs on the same machine.
|
|
1215
|
+
if (this.project.config.browser.ui && !this.project.config.browser.headless) {
|
|
1115
1216
|
options.viewport = null;
|
|
1116
1217
|
}
|
|
1117
1218
|
return options;
|
|
@@ -1201,18 +1302,23 @@ class PlaywrightBrowserProvider {
|
|
|
1201
1302
|
process.off("SIGTERM", this.onSIGTERM);
|
|
1202
1303
|
debug?.("[%s] closing provider", this.browserName);
|
|
1203
1304
|
this.closing = true;
|
|
1305
|
+
// a prewarmed browser that was never adopted must not outlive the provider
|
|
1306
|
+
const warm = takeWarmBrowser(this.project.config);
|
|
1307
|
+
if (warm) {
|
|
1308
|
+
void warm.promise.then((browser) => browser.close()).catch(() => {});
|
|
1309
|
+
}
|
|
1204
1310
|
if (this.browserPromise) {
|
|
1205
1311
|
await this.browserPromise;
|
|
1206
1312
|
this.browserPromise = null;
|
|
1207
1313
|
}
|
|
1208
1314
|
const browser = this.browser;
|
|
1209
1315
|
this.browser = null;
|
|
1210
|
-
await Promise.all(
|
|
1316
|
+
await Promise.all(Array.from(this.pages.values(), (p) => p.close()));
|
|
1211
1317
|
this.pages.clear();
|
|
1212
1318
|
if (this.persistentContext) {
|
|
1213
1319
|
await this.persistentContext.close();
|
|
1214
1320
|
} else {
|
|
1215
|
-
await Promise.all(
|
|
1321
|
+
await Promise.all(Array.from(this.contexts.values(), (c) => c.close()));
|
|
1216
1322
|
}
|
|
1217
1323
|
this.contexts.clear();
|
|
1218
1324
|
await browser?.close();
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@vitest/browser-playwright",
|
|
3
3
|
"type": "module",
|
|
4
|
-
"version": "5.0.0-beta.
|
|
4
|
+
"version": "5.0.0-beta.7",
|
|
5
5
|
"description": "Browser running for Vitest using playwright",
|
|
6
6
|
"license": "MIT",
|
|
7
7
|
"funding": "https://opencollective.com/vitest",
|
|
@@ -42,7 +42,7 @@
|
|
|
42
42
|
],
|
|
43
43
|
"peerDependencies": {
|
|
44
44
|
"playwright": "*",
|
|
45
|
-
"vitest": "5.0.0-beta.
|
|
45
|
+
"vitest": "5.0.0-beta.7"
|
|
46
46
|
},
|
|
47
47
|
"peerDependenciesMeta": {
|
|
48
48
|
"playwright": {
|
|
@@ -51,12 +51,12 @@
|
|
|
51
51
|
},
|
|
52
52
|
"dependencies": {
|
|
53
53
|
"tinyrainbow": "^3.1.0",
|
|
54
|
-
"@vitest/browser": "5.0.0-beta.
|
|
55
|
-
"@vitest/mocker": "5.0.0-beta.
|
|
54
|
+
"@vitest/browser": "5.0.0-beta.7",
|
|
55
|
+
"@vitest/mocker": "5.0.0-beta.7"
|
|
56
56
|
},
|
|
57
57
|
"devDependencies": {
|
|
58
|
-
"playwright": "^1.
|
|
59
|
-
"vitest": "5.0.0-beta.
|
|
58
|
+
"playwright": "^1.61.0",
|
|
59
|
+
"vitest": "5.0.0-beta.7"
|
|
60
60
|
},
|
|
61
61
|
"scripts": {
|
|
62
62
|
"build": "premove dist && pnpm rollup -c",
|