@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.
Files changed (2) hide show
  1. package/dist/index.js +132 -26
  2. 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
- return resolve(root, file);
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 = [...ids].map((id) => {
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
- if (this.project.config.browser.ui) {
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([...this.pages.values()].map((p) => p.close()));
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([...this.contexts.values()].map((c) => c.close()));
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.5",
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.5"
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.5",
55
- "@vitest/mocker": "5.0.0-beta.5"
54
+ "@vitest/browser": "5.0.0-beta.7",
55
+ "@vitest/mocker": "5.0.0-beta.7"
56
56
  },
57
57
  "devDependencies": {
58
- "playwright": "^1.60.0",
59
- "vitest": "5.0.0-beta.5"
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",