@vitest/browser 4.1.8 → 4.1.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.
@@ -4,7 +4,7 @@
4
4
  "name": "utils"
5
5
  },
6
6
  "orchestrator.html": {
7
- "file": "__vitest_browser__/orchestrator-B-wB8WWP.js",
7
+ "file": "__vitest_browser__/orchestrator-KVYrTcFi.js",
8
8
  "name": "orchestrator",
9
9
  "src": "orchestrator.html",
10
10
  "isEntry": true,
@@ -13,7 +13,7 @@
13
13
  ]
14
14
  },
15
15
  "tester/tester.html": {
16
- "file": "__vitest_browser__/tester-DQAp97S5.js",
16
+ "file": "__vitest_browser__/tester-BJtsJFpD.js",
17
17
  "name": "tester",
18
18
  "src": "tester/tester.html",
19
19
  "isEntry": true,
@@ -10,6 +10,8 @@ class IframeOrchestrator {
10
10
  cancelled = false;
11
11
  recreateNonIsolatedIframe = false;
12
12
  iframes = /* @__PURE__ */ new Map();
13
+ readyIframes = /* @__PURE__ */ new Set();
14
+ readyWaiters = /* @__PURE__ */ new Map();
13
15
  eventTarget = new EventTarget();
14
16
  traces;
15
17
  constructor() {
@@ -27,6 +29,9 @@ class IframeOrchestrator {
27
29
  "message",
28
30
  (e) => this.onGlobalChannelEvent(e)
29
31
  );
32
+ void client.waitForConnection().then(() => client.rpc.onOrchestratorReady()).catch((error) => {
33
+ debug("failed to notify orchestrator readiness", error);
34
+ });
30
35
  }
31
36
  async createTesters(options) {
32
37
  await this.traces.waitInit();
@@ -63,6 +68,8 @@ class IframeOrchestrator {
63
68
  }
64
69
  this.iframes.forEach((iframe) => iframe.remove());
65
70
  this.iframes.clear();
71
+ this.readyIframes.clear();
72
+ this.readyWaiters.clear();
66
73
  for (let i = 0; i < options.files.length; i++) {
67
74
  if (this.cancelled) {
68
75
  await endSpan();
@@ -104,8 +111,7 @@ class IframeOrchestrator {
104
111
  async runNonIsolatedTests(container, options, startTime, otelContext) {
105
112
  if (this.recreateNonIsolatedIframe) {
106
113
  this.recreateNonIsolatedIframe = false;
107
- this.iframes.get(ID_ALL).remove();
108
- this.iframes.delete(ID_ALL);
114
+ this.removeIframe(ID_ALL);
109
115
  debug("recreate non-isolated iframe");
110
116
  }
111
117
  if (!this.iframes.has(ID_ALL)) {
@@ -131,8 +137,7 @@ class IframeOrchestrator {
131
137
  const { width, height } = config.browser.viewport;
132
138
  const file = spec.filepath;
133
139
  if (this.iframes.has(file)) {
134
- this.iframes.get(file).remove();
135
- this.iframes.delete(file);
140
+ this.removeIframe(file);
136
141
  }
137
142
  const iframe = await this.prepareIframe(
138
143
  container,
@@ -181,12 +186,12 @@ Expected: ${iframe.src}`
181
186
  }
182
187
  } else {
183
188
  this.iframes.set(iframeId, iframe);
184
- this.sendEventToIframe({
189
+ this.waitForReady(iframeId).then(() => this.sendEventToIframe({
185
190
  event: "prepare",
186
191
  iframeId,
187
192
  startTime,
188
193
  otelCarrier: this.traces.getContextCarrier(otelContext)
189
- }).then(resolve, (error) => reject(this.dispatchIframeError(error)));
194
+ })).then(resolve, (error) => reject(this.dispatchIframeError(error)));
190
195
  }
191
196
  };
192
197
  iframe.onerror = (e) => {
@@ -201,6 +206,29 @@ Expected: ${iframe.src}`
201
206
  });
202
207
  return iframe;
203
208
  }
209
+ markReady(iframeId) {
210
+ this.readyIframes.add(iframeId);
211
+ const waiter = this.readyWaiters.get(iframeId);
212
+ if (waiter) {
213
+ this.readyWaiters.delete(iframeId);
214
+ waiter();
215
+ }
216
+ }
217
+ waitForReady(iframeId) {
218
+ if (this.readyIframes.has(iframeId)) {
219
+ return Promise.resolve();
220
+ }
221
+ return new Promise((resolve) => {
222
+ this.readyWaiters.set(iframeId, resolve);
223
+ });
224
+ }
225
+ removeIframe(iframeId) {
226
+ const iframe = this.iframes.get(iframeId);
227
+ this.iframes.delete(iframeId);
228
+ this.readyIframes.delete(iframeId);
229
+ this.readyWaiters.delete(iframeId);
230
+ iframe == null ? void 0 : iframe.remove();
231
+ }
204
232
  loggedIframe = /* @__PURE__ */ new WeakSet();
205
233
  createWarningMessage(iframeId, location) {
206
234
  return `The iframe${iframeId === ID_ALL ? "" : ` for "${iframeId}"`} was reloaded ${location}. This can lead to unexpected behavior during tests, duplicated test results or tests hanging.
@@ -257,6 +285,10 @@ If you are using a framework that manipulates browser history (like React Router
257
285
  async onIframeEvent(e) {
258
286
  debug("iframe event", JSON.stringify(e.data));
259
287
  switch (e.data.event) {
288
+ case "ready": {
289
+ this.markReady(e.data.iframeId);
290
+ break;
291
+ }
260
292
  case "viewport": {
261
293
  const { width, height, iframeId: id } = e.data;
262
294
  const iframe = this.iframes.get(id);
@@ -2151,6 +2151,10 @@ const commands = new CommandsManager();
2151
2151
  getBrowserState().commands = commands;
2152
2152
  getBrowserState().activeTraceTaskIds = /* @__PURE__ */ new Set();
2153
2153
  getBrowserState().iframeId = iframeId;
2154
+ channel.postMessage({
2155
+ event: "ready",
2156
+ iframeId
2157
+ });
2154
2158
  let contextSwitched = false;
2155
2159
  async function prepareTestEnvironment(options) {
2156
2160
  debug == null ? void 0 : debug("trying to resolve the runner");
@@ -26,7 +26,7 @@
26
26
  {__VITEST_INJECTOR__}
27
27
  {__VITEST_ERROR_CATCHER__}
28
28
  {__VITEST_SCRIPTS__}
29
- <script type="module" crossorigin src="/__vitest_browser__/orchestrator-B-wB8WWP.js"></script>
29
+ <script type="module" crossorigin src="/__vitest_browser__/orchestrator-KVYrTcFi.js"></script>
30
30
  <link rel="modulepreload" crossorigin href="/__vitest_browser__/utils-BsFW67Gk.js">
31
31
  </head>
32
32
  <body>
@@ -5,7 +5,7 @@
5
5
  <link rel="icon" href="{__VITEST_FAVICON__}" type="image/svg+xml">
6
6
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
7
7
  <title>Vitest Browser Tester</title>
8
- <script type="module" crossorigin src="/__vitest_browser__/tester-DQAp97S5.js"></script>
8
+ <script type="module" crossorigin src="/__vitest_browser__/tester-BJtsJFpD.js"></script>
9
9
  <link rel="modulepreload" crossorigin href="/__vitest_browser__/utils-BsFW67Gk.js">
10
10
  </head>
11
11
  <body>
package/dist/index.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { ResolvedConfig, BrowserServerFactory, BrowserCommand, BrowserProviderOption } from 'vitest/node';
1
+ import { TestProject, ResolvedConfig, BrowserServerFactory, BrowserCommand, BrowserProviderOption } from 'vitest/node';
2
2
  import { ScreenshotMatcherOptions, NonStandardScreenshotComparators, ScreenshotComparatorRegistry } from '@vitest/browser/context';
3
3
 
4
4
  interface BaseMetadata {
@@ -61,6 +61,8 @@ declare function parseKeyDef(text: string): {
61
61
  repeat: number;
62
62
  }[];
63
63
  declare function resolveScreenshotPath(testPath: string, name: string, config: ResolvedConfig, customPath: string | undefined): string;
64
+ declare function assertBrowserFileAccess(project: TestProject, path: string): void;
65
+ declare function assertBrowserApiWrite(project: TestProject, path: string): void;
64
66
 
65
67
  //#endregion
66
68
  //#region src/locatorGenerators.d.ts
@@ -89,5 +91,5 @@ declare function defineBrowserProvider<T extends object = object>(options: Omit<
89
91
  options?: T;
90
92
  }): BrowserProviderOption;
91
93
 
92
- export { asLocator, createBrowserServer, defineBrowserCommand, defineBrowserProvider, parseKeyDef, resolveScreenshotPath };
94
+ export { asLocator, assertBrowserApiWrite, assertBrowserFileAccess, createBrowserServer, defineBrowserCommand, defineBrowserProvider, parseKeyDef, resolveScreenshotPath };
93
95
  export type { CustomComparatorsRegistry };
package/dist/index.js CHANGED
@@ -1,7 +1,7 @@
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 { isValidApiRequest, isFileServingAllowed, distDir, rolldownVersion, resolveApiServerConfig, resolveFsAllow, isFileLoadingAllowed, createDebugger, createViteLogger, createViteServer } from 'vitest/node';
4
+ import { isFileLoadingAllowed, isValidApiRequest, isFileServingAllowed, distDir, rolldownVersion, resolveApiServerConfig, resolveFsAllow, createDebugger, createViteLogger, createViteServer } from 'vitest/node';
5
5
  import { fileURLToPath } from 'node:url';
6
6
  import fs, { readFileSync, createReadStream, promises, existsSync } from 'node:fs';
7
7
  import { createRequire } from 'node:module';
@@ -18,7 +18,7 @@ import { PNG } from 'pngjs';
18
18
  import { diff } from '@blazediff/core';
19
19
  import { WebSocketServer } from 'ws';
20
20
 
21
- var version = "4.1.8";
21
+ var version = "4.1.10";
22
22
 
23
23
  const _DRIVE_LETTER_START_RE = /^[A-Za-z]:\//;
24
24
  function normalizeWindowsPath(input = "") {
@@ -674,6 +674,17 @@ async function getBrowserProvider(options, project) {
674
674
  function slash(path) {
675
675
  return path.replace(/\\/g, "/").replace(/\/+/g, "/");
676
676
  }
677
+ function assertBrowserFileAccess(project, path) {
678
+ const normalized = slash(path);
679
+ if (!isFileLoadingAllowed(project.vite.config, normalized) && !isFileLoadingAllowed(project.vitest.vite.config, normalized)) {
680
+ throw new Error(`Access denied to "${path}". See Vite config documentation for "server.fs": https://vitejs.dev/config/server-options.html#server-fs-strict.`);
681
+ }
682
+ }
683
+ function assertBrowserApiWrite(project, path) {
684
+ if (!project.config.browser.api.allowWrite || !project.vitest.config.api.allowWrite) {
685
+ throw new Error(`Cannot modify file "${path}". File writing is disabled because the server is exposed to the internet, see https://vitest.dev/config/browser/api.`);
686
+ }
687
+ }
677
688
 
678
689
  async function resolveOrchestrator(globalServer, url, res) {
679
690
  let sessionId = url.searchParams.get("sessionId");
@@ -1896,19 +1907,9 @@ _Mime_extensionToType = new WeakMap(), _Mime_typeToExtension = new WeakMap(), _M
1896
1907
 
1897
1908
  var mime = new Mime(types)._freeze();
1898
1909
 
1899
- function assertFileAccess(path, project) {
1900
- if (!isFileLoadingAllowed(project.vite.config, path) && !isFileLoadingAllowed(project.vitest.vite.config, path)) {
1901
- throw new Error(`Access denied to "${path}". See Vite config documentation for "server.fs": https://vitejs.dev/config/server-options.html#server-fs-strict.`);
1902
- }
1903
- }
1904
- function assertWrite(path, project) {
1905
- if (!project.config.browser.api.allowWrite || !project.vitest.config.api.allowWrite) {
1906
- throw new Error(`Cannot modify file "${path}". File writing is disabled because server is exposed to the internet, see https://vitest.dev/config/browser/api.`);
1907
- }
1908
- }
1909
1910
  const readFile = async ({ project }, path, options = {}) => {
1910
1911
  const filepath = resolve$1(project.config.root, path);
1911
- assertFileAccess(slash(filepath), project);
1912
+ assertBrowserFileAccess(project, filepath);
1912
1913
  // never return a Buffer
1913
1914
  if (typeof options === "object" && !options.encoding) {
1914
1915
  options.encoding = "utf-8";
@@ -1916,9 +1917,9 @@ const readFile = async ({ project }, path, options = {}) => {
1916
1917
  return promises.readFile(filepath, options);
1917
1918
  };
1918
1919
  const writeFile = async ({ project }, path, data, options) => {
1919
- assertWrite(path, project);
1920
+ assertBrowserApiWrite(project, path);
1920
1921
  const filepath = resolve$1(project.config.root, path);
1921
- assertFileAccess(slash(filepath), project);
1922
+ assertBrowserFileAccess(project, filepath);
1922
1923
  const dir = dirname$1(filepath);
1923
1924
  if (!fs.existsSync(dir)) {
1924
1925
  await promises.mkdir(dir, { recursive: true });
@@ -1926,14 +1927,14 @@ const writeFile = async ({ project }, path, data, options) => {
1926
1927
  await promises.writeFile(filepath, data, options);
1927
1928
  };
1928
1929
  const removeFile = async ({ project }, path) => {
1929
- assertWrite(path, project);
1930
+ assertBrowserApiWrite(project, path);
1930
1931
  const filepath = resolve$1(project.config.root, path);
1931
- assertFileAccess(slash(filepath), project);
1932
+ assertBrowserFileAccess(project, filepath);
1932
1933
  await promises.rm(filepath);
1933
1934
  };
1934
1935
  const _fileInfo = async ({ project }, path, encoding) => {
1935
1936
  const filepath = resolve$1(project.config.root, path);
1936
- assertFileAccess(slash(filepath), project);
1937
+ assertBrowserFileAccess(project, filepath);
1937
1938
  const content = await promises.readFile(filepath, encoding || "base64");
1938
1939
  return {
1939
1940
  content,
@@ -2249,7 +2250,7 @@ const screenshotMatcher = async (context, name, testName, options) => {
2249
2250
  comparator,
2250
2251
  comparatorOptions
2251
2252
  });
2252
- await performSideEffects(outcome, codec);
2253
+ await performSideEffects(outcome, codec, context.project);
2253
2254
  return buildOutput(outcome, timeout);
2254
2255
  };
2255
2256
  /**
@@ -2335,17 +2336,17 @@ async function determineOutcome({ comparator, comparatorOptions, paths, referenc
2335
2336
  *
2336
2337
  * Only `missing-reference`, `update-reference`, and `mismatch` write files. Successful matches produce no side effects.
2337
2338
  */
2338
- async function performSideEffects(outcome, codec) {
2339
+ async function performSideEffects(outcome, codec, project) {
2339
2340
  switch (outcome.type) {
2340
2341
  case "missing-reference":
2341
2342
  case "update-reference": {
2342
- await writeScreenshot(outcome.reference.path, await codec.encode(outcome.reference.image, {}));
2343
+ await writeScreenshot(outcome.reference.path, await codec.encode(outcome.reference.image, {}), project);
2343
2344
  break;
2344
2345
  }
2345
2346
  case "mismatch": {
2346
- await writeScreenshot(outcome.actual.path, await codec.encode(outcome.actual.image, {}));
2347
+ await writeScreenshot(outcome.actual.path, await codec.encode(outcome.actual.image, {}), project);
2347
2348
  if (outcome.diff) {
2348
- await writeScreenshot(outcome.diff.path, await codec.encode(outcome.diff.image, {}));
2349
+ await writeScreenshot(outcome.diff.path, await codec.encode(outcome.diff.image, {}), project);
2349
2350
  }
2350
2351
  break;
2351
2352
  }
@@ -2479,8 +2480,10 @@ async function getStableScreenshot({ codec, context, comparator, comparatorOptio
2479
2480
  };
2480
2481
  }
2481
2482
  /** Writes encoded images to disk, creating parent directories as needed. */
2482
- async function writeScreenshot(path, image) {
2483
+ async function writeScreenshot(path, image, project) {
2483
2484
  try {
2485
+ assertBrowserApiWrite(project, path);
2486
+ assertBrowserFileAccess(project, path);
2484
2487
  await mkdir(dirname(path), { recursive: true });
2485
2488
  await writeFile$1(path, image);
2486
2489
  } catch (cause) {
@@ -3024,7 +3027,8 @@ function setupBrowserRpc(globalServer, defaultMockerRegistry) {
3024
3027
  }
3025
3028
  if (type === "orchestrator") {
3026
3029
  const session = sessions.getSession(sessionId);
3027
- // it's possible the session was already resolved by the preview provider
3030
+ // it's possible the session was already resolved by the preview provider,
3031
+ // but we still mark the websocket connection when the page reconnects
3028
3032
  session?.connected();
3029
3033
  }
3030
3034
  const project = vitest.getProjectByName(projectName);
@@ -3033,7 +3037,7 @@ function setupBrowserRpc(globalServer, defaultMockerRegistry) {
3033
3037
  }
3034
3038
  wss.handleUpgrade(request, socket, head, (ws) => {
3035
3039
  wss.emit("connection", ws, request);
3036
- const { rpc, offCancel } = setupClient(project, rpcId, ws);
3040
+ const { rpc, offCancel } = setupClient(project, rpcId, ws, { sessionId });
3037
3041
  const state = project.browser.state;
3038
3042
  const clients = type === "tester" ? state.testers : state.orchestrators;
3039
3043
  clients.set(rpcId, rpc);
@@ -3072,10 +3076,14 @@ function setupBrowserRpc(globalServer, defaultMockerRegistry) {
3072
3076
  throw new Error(`Cannot use CDP because browser API write or exec operations are disabled. See https://vitest.dev/config/browser/api.`);
3073
3077
  }
3074
3078
  }
3075
- function setupClient(project, rpcId, ws) {
3079
+ function setupClient(project, rpcId, ws, options) {
3076
3080
  const mockResolver = new ServerMockResolver(globalServer.vite, { moduleDirectories: project.config?.deps?.moduleDirectories });
3077
3081
  const mocker = project.browser?.provider.mocker;
3078
3082
  const rpc = createBirpc({
3083
+ onOrchestratorReady() {
3084
+ const sessions = vitest._browserSessions;
3085
+ sessions.getSession(options.sessionId)?.ready();
3086
+ },
3079
3087
  async onUnhandledError(error, type) {
3080
3088
  if (error && typeof error === "object") {
3081
3089
  const _error = error;
@@ -3109,6 +3117,16 @@ function setupBrowserRpc(globalServer, defaultMockerRegistry) {
3109
3117
  artifact.attachments = [];
3110
3118
  vitest.logger.error(`[vitest] Cannot record attachments ("${attachments}") because file writing is disabled, removing attachments from artifact "${artifact.type}". See https://vitest.dev/config/browser/api.`);
3111
3119
  }
3120
+ } else {
3121
+ // attachment files are copied into `attachmentsDir`, so confine
3122
+ // client-supplied paths to Vite's `server.fs` boundary
3123
+ const attachments = artifact.type === "internal:annotation" ? artifact.annotation.attachment ? [artifact.annotation.attachment] : [] : artifact.attachments ?? [];
3124
+ for (const attachment of attachments) {
3125
+ const path = attachment.path;
3126
+ if (path && !path.startsWith("http://") && !path.startsWith("https://")) {
3127
+ checkFileAccess(resolve(project.config.root, path));
3128
+ }
3129
+ }
3112
3130
  }
3113
3131
  return vitest._testRun.recordArtifact(id, artifact);
3114
3132
  },
@@ -7945,4 +7963,4 @@ function defineBrowserProvider(options) {
7945
7963
  };
7946
7964
  }
7947
7965
 
7948
- export { asLocator, createBrowserServer, defineBrowserCommand, defineBrowserProvider, parseKeyDef, resolveScreenshotPath };
7966
+ export { asLocator, assertBrowserApiWrite, assertBrowserFileAccess, createBrowserServer, defineBrowserCommand, defineBrowserProvider, parseKeyDef, resolveScreenshotPath };
package/dist/types.d.ts CHANGED
@@ -11,6 +11,7 @@ export interface WebSocketBrowserHandlers {
11
11
  onTaskArtifactRecord: <Artifact extends TestArtifact>(testId: string, artifact: Artifact) => Promise<Artifact>;
12
12
  onTaskUpdate: (method: TestExecutionMethod, packs: TaskResultPack[], events: TaskEventPack[]) => void;
13
13
  onAfterSuiteRun: (meta: AfterSuiteRunMeta) => void;
14
+ onOrchestratorReady: () => void;
14
15
  cancelCurrentRun: (reason: CancelReason) => void;
15
16
  getCountOfFailedTests: () => number;
16
17
  readSnapshotFile: (id: string) => Promise<string | null>;
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@vitest/browser",
3
3
  "type": "module",
4
- "version": "4.1.8",
4
+ "version": "4.1.10",
5
5
  "description": "Browser running for Vitest",
6
6
  "license": "MIT",
7
7
  "funding": "https://opencollective.com/vitest",
@@ -57,7 +57,7 @@
57
57
  "providers"
58
58
  ],
59
59
  "peerDependencies": {
60
- "vitest": "4.1.8"
60
+ "vitest": "4.1.10"
61
61
  },
62
62
  "dependencies": {
63
63
  "@blazediff/core": "1.9.1",
@@ -66,8 +66,8 @@
66
66
  "sirv": "^3.0.2",
67
67
  "tinyrainbow": "^3.1.0",
68
68
  "ws": "^8.19.0",
69
- "@vitest/mocker": "4.1.8",
70
- "@vitest/utils": "4.1.8"
69
+ "@vitest/mocker": "4.1.10",
70
+ "@vitest/utils": "4.1.10"
71
71
  },
72
72
  "devDependencies": {
73
73
  "@opentelemetry/api": "^1.9.0",
@@ -79,8 +79,8 @@
79
79
  "ivya": "^1.8.0",
80
80
  "mime": "^4.1.0",
81
81
  "pathe": "^2.0.3",
82
- "vitest": "4.1.8",
83
- "@vitest/runner": "4.1.8"
82
+ "@vitest/runner": "4.1.10",
83
+ "vitest": "4.1.10"
84
84
  },
85
85
  "scripts": {
86
86
  "typecheck": "tsc -p ./src/client/tsconfig.json --noEmit",