@rstest/browser 0.10.6 → 0.11.1

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.
@@ -65,9 +65,15 @@ export type BrowserHostConfig = {
65
65
  * Used by browser RPC calls to prevent stale requests from previous reruns.
66
66
  */ runId?: string;
67
67
  /**
68
- * Base URL for runner (iframe) pages.
68
+ * Base URL for runner (iframe) pages. Container origin; used as a fallback
69
+ * when a project has no entry in `projectRunnerUrls`.
69
70
  */ runnerUrl?: string;
70
71
  /**
72
+ * Per-project runner origin base URLs, keyed by project name. Each browser
73
+ * project runs on its own dev server, so the container must load each test
74
+ * file's iframe from that project's own origin.
75
+ */ projectRunnerUrls?: Record<string, string>;
76
+ /**
71
77
  * WebSocket port for container RPC.
72
78
  */ wsPort?: number;
73
79
  /**
@@ -41,6 +41,9 @@ import type { BrowserRpcRequest } from '../rpcProtocol.js';
41
41
  on: {
42
42
  (event: 'popup', listener: (page: BrowserProviderPage) => void) : void;
43
43
  (event: 'console', listener: (message: BrowserConsoleMessage) => void) : void;
44
+ // Page died: renderer crash or unexpected close. Headless scheduling wires
45
+ // these to fail the file immediately. Listeners take no payload.
46
+ (event: 'crash' | 'close', listener: () => void) : void;
44
47
  };
45
48
  close: () => Promise<void>;
46
49
  [Symbol.asyncDispose]: () => Promise<void>;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rstest/browser",
3
- "version": "0.10.6",
3
+ "version": "0.11.1",
4
4
  "description": "Browser mode support for Rstest testing framework.",
5
5
  "keywords": [
6
6
  "rstest",
@@ -48,21 +48,21 @@
48
48
  "ws": "^8.21.0"
49
49
  },
50
50
  "devDependencies": {
51
- "@rslib/core": "0.23.0",
51
+ "@rslib/core": "0.23.2",
52
52
  "@types/convert-source-map": "^2.0.3",
53
53
  "@types/picomatch": "^4.0.3",
54
54
  "@types/ws": "^8.18.1",
55
55
  "@vitest/snapshot": "^3.2.6",
56
56
  "birpc": "^4.0.0",
57
- "picomatch": "^4.0.4",
58
- "playwright": "^1.61.0",
59
- "@rstest/browser-ui": "0.0.0",
57
+ "picomatch": "^4.0.5",
58
+ "playwright": "^1.61.1",
59
+ "@rstest/core": "0.11.1",
60
60
  "@rstest/tsconfig": "0.0.1",
61
- "@rstest/core": "0.10.6"
61
+ "@rstest/browser-ui": "0.0.0"
62
62
  },
63
63
  "peerDependencies": {
64
64
  "playwright": "^1.49.1",
65
- "@rstest/core": "^0.10.6"
65
+ "@rstest/core": "^0.11.1"
66
66
  },
67
67
  "peerDependenciesMeta": {
68
68
  "playwright": {
@@ -32,6 +32,7 @@ import {
32
32
  createRunnerLifecycleRequest,
33
33
  sendRunnerLifecycle,
34
34
  } from './dispatchTransport';
35
+ import { formatConsoleArgs } from './formatConsole';
35
36
  import { BrowserSnapshotEnvironment } from './snapshot';
36
37
  import {
37
38
  findNewScriptUrl,
@@ -109,24 +110,6 @@ const ensureRuntimeEnv = (env: RuntimeConfig['env'] | undefined): void => {
109
110
  }
110
111
  };
111
112
 
112
- /**
113
- * Format an argument for console output.
114
- */
115
- const formatArg = (arg: unknown): string => {
116
- if (arg === null) return 'null';
117
- if (arg === undefined) return 'undefined';
118
- if (typeof arg === 'string') return arg;
119
- if (typeof arg === 'number' || typeof arg === 'boolean') return String(arg);
120
- if (arg instanceof Error) {
121
- return arg.stack || `${arg.name}: ${arg.message}`;
122
- }
123
- try {
124
- return JSON.stringify(arg, null, 2);
125
- } catch {
126
- return String(arg);
127
- }
128
- };
129
-
130
113
  const getFileTaskId = (testPath: string): string => {
131
114
  return `file:${testPath}`;
132
115
  };
@@ -161,8 +144,7 @@ const interceptConsole = (
161
144
  // Call original for browser DevTools
162
145
  originalConsole[level](...args);
163
146
 
164
- // Format message
165
- const content = args.map(formatArg).join(' ');
147
+ const content = formatConsoleArgs(args);
166
148
  const currentTask = getCurrentTask();
167
149
 
168
150
  // Send to host
@@ -284,10 +266,19 @@ const toContextKey = (absolutePath: string, projectRoot: string): string => {
284
266
  const normalizedAbsolute = normalize(absolutePath);
285
267
  const normalizedRoot = normalize(projectRoot);
286
268
 
287
- let relative = normalizedAbsolute;
288
- if (normalizedAbsolute.startsWith(normalizedRoot)) {
289
- relative = normalizedAbsolute.slice(normalizedRoot.length);
269
+ // Only strip the root at a path boundary: a bare `startsWith` would mangle a
270
+ // sibling like `/repo/pkg-extra/a.test.ts` under root `/repo/pkg`. Must stay
271
+ // in sync with the host `toContextKey` (hostController.ts) so the non-watch
272
+ // import-map keys resolve.
273
+ const withinRoot =
274
+ normalizedAbsolute === normalizedRoot ||
275
+ normalizedAbsolute.startsWith(`${normalizedRoot}/`);
276
+ if (!withinRoot) {
277
+ // Test file outside the project root: keep the absolute path as the key so
278
+ // `toAbsolutePath` round-trips it instead of re-rooting under projectRoot.
279
+ return normalizedAbsolute;
290
280
  }
281
+ const relative = normalizedAbsolute.slice(normalizedRoot.length);
291
282
  return relative.startsWith('/') ? `.${relative}` : `./${relative}`;
292
283
  };
293
284
 
@@ -296,6 +287,11 @@ const toContextKey = (absolutePath: string, projectRoot: string): string => {
296
287
  * e.g., './src/foo.test.ts' -> '/project/src/foo.test.ts'
297
288
  */
298
289
  const toAbsolutePath = (key: string, projectRoot: string): string => {
290
+ // An absolute key (test file outside the project root, see `toContextKey`)
291
+ // round-trips as-is; only `./`-prefixed relative keys are re-rooted.
292
+ if (!key.startsWith('.')) {
293
+ return key;
294
+ }
299
295
  // key format: ./src/foo.test.ts
300
296
  // Ensure no double slashes by removing trailing slash from projectRoot
301
297
  const normalizedRoot = normalize(projectRoot).replace(/\/$/, '');
@@ -482,6 +478,10 @@ const run = async () => {
482
478
  rootPath: options.rootPath,
483
479
  runtimeConfig,
484
480
  taskId: 0,
481
+ // The kept-module-cache flush keyed on `buildId` is node worker-pool
482
+ // only (#1373); browser runners never reuse a node worker, so a constant
483
+ // inert id is correct here.
484
+ buildId: 0,
485
485
  outputModule: false,
486
486
  environment: 'browser',
487
487
  testPath,
@@ -578,6 +578,8 @@ const run = async () => {
578
578
  rootPath: options.rootPath,
579
579
  runtimeConfig,
580
580
  taskId: 0,
581
+ // See the `buildId` note above: inert in browser mode.
582
+ buildId: 0,
581
583
  outputModule: false,
582
584
  environment: 'browser',
583
585
  currentTask: taskStack[0],
@@ -0,0 +1,77 @@
1
+ /** Stringify a single console argument for terminal forwarding. */
2
+ export const formatArg = (arg: unknown): string => {
3
+ if (arg === null) return 'null';
4
+ if (arg === undefined) return 'undefined';
5
+ if (typeof arg === 'string') return arg;
6
+ if (typeof arg === 'number' || typeof arg === 'boolean') return String(arg);
7
+ // `JSON.stringify(symbol)` is `undefined`, so a symbol must be handled before
8
+ // the JSON path or it would forward as an empty/`undefined` log.
9
+ if (typeof arg === 'symbol') return arg.toString();
10
+ if (arg instanceof Error) {
11
+ return arg.stack || `${arg.name}: ${arg.message}`;
12
+ }
13
+ try {
14
+ return JSON.stringify(arg, null, 2);
15
+ } catch {
16
+ return String(arg);
17
+ }
18
+ };
19
+
20
+ // Format specifiers understood by the browser console. `%c` applies CSS in a
21
+ // real console; forwarded to a terminal there is no styling, so it is consumed
22
+ // and dropped (matching how Node's `util.format` handles it). `%%` is a literal
23
+ // percent sign. Kept aligned with the browser console spec (no Node-only `%j`,
24
+ // since these logs originate in the browser).
25
+ // cspell:ignore sdifo WHATWG
26
+ const HAS_SPECIFIER = /%[sdifoOc%]/;
27
+ const SPECIFIER = /%[sdifoOc%]/g;
28
+
29
+ /**
30
+ * Join console arguments the way a console does. When the first argument is a
31
+ * string carrying `printf`-style specifiers, substitute the following arguments
32
+ * into it (consuming `%c` styles without emitting them), then append any
33
+ * leftover arguments. Otherwise fall back to a plain space-join.
34
+ *
35
+ * Without this, `console.info('%cText', 'font-weight:bold')` (e.g. React's
36
+ * DevTools notice) leaks the raw `%c` directive and its CSS argument into the
37
+ * forwarded terminal output.
38
+ */
39
+ export const formatConsoleArgs = (args: unknown[]): string => {
40
+ const first = args[0];
41
+ if (typeof first !== 'string' || !HAS_SPECIFIER.test(first)) {
42
+ return args.map(formatArg).join(' ');
43
+ }
44
+
45
+ let next = 1;
46
+ const substituted = first.replace(SPECIFIER, (spec) => {
47
+ if (spec === '%%') return '%';
48
+ if (next >= args.length) return spec;
49
+ const arg = args[next++];
50
+ switch (spec) {
51
+ case '%c':
52
+ return ''; // CSS directive: swallow the style argument
53
+ case '%s':
54
+ return formatArg(arg);
55
+ case '%d':
56
+ case '%i': {
57
+ // Match the browser console formatter (WHATWG): parseInt on the value,
58
+ // so unit-suffixed strings like '42px' format as 42 (not NaN).
59
+ // `String()` first keeps Symbol safe — a bare `parseInt(symbol)` throws
60
+ // via ToString, whereas the spec maps a Symbol to NaN.
61
+ const num = Number.parseInt(String(arg), 10);
62
+ return Number.isNaN(num) ? 'NaN' : String(num);
63
+ }
64
+ case '%f': {
65
+ const num = Number.parseFloat(String(arg));
66
+ return Number.isNaN(num) ? 'NaN' : String(num);
67
+ }
68
+ default: // %o, %O
69
+ return formatArg(arg);
70
+ }
71
+ });
72
+
73
+ if (next >= args.length) {
74
+ return substituted;
75
+ }
76
+ return [substituted, ...args.slice(next).map(formatArg)].join(' ');
77
+ };
@@ -29,11 +29,7 @@ export type LocatorTextOptions = {
29
29
  };
30
30
 
31
31
  export type LocatorKeyboardModifier =
32
- | 'Alt'
33
- | 'Control'
34
- | 'ControlOrMeta'
35
- | 'Meta'
36
- | 'Shift';
32
+ 'Alt' | 'Control' | 'ControlOrMeta' | 'Meta' | 'Shift';
37
33
 
38
34
  export type LocatorMouseButton = 'left' | 'right' | 'middle';
39
35