@rstest/browser 0.8.4 → 0.9.0

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 (79) hide show
  1. package/LICENSE-APACHE-2.0 +202 -0
  2. package/NOTICE +11 -0
  3. package/dist/361.js +8 -0
  4. package/dist/augmentExpect.d.ts +73 -0
  5. package/dist/browser-container/container-static/css/index.5c72297783.css +1 -0
  6. package/dist/browser-container/container-static/js/{392.28f9a733.js → 101.36a8ccdf84.js} +4068 -3904
  7. package/dist/browser-container/container-static/js/101.36a8ccdf84.js.LICENSE.txt +1 -0
  8. package/dist/browser-container/container-static/js/{index.129eaf9c.js → index.0687a8142a.js} +742 -692
  9. package/dist/browser-container/container-static/js/{lib-react.97ee79b0.js → lib-react.dcf2a5e57a.js} +10 -10
  10. package/dist/browser-container/container-static/js/lib-react.dcf2a5e57a.js.LICENSE.txt +1 -0
  11. package/dist/browser-container/index.html +1 -1
  12. package/dist/browser.d.ts +2 -0
  13. package/dist/browser.js +583 -0
  14. package/dist/browserRpcRegistry.d.ts +18 -0
  15. package/dist/client/api.d.ts +3 -0
  16. package/dist/client/browserRpc.d.ts +2 -0
  17. package/dist/client/dispatchTransport.d.ts +11 -0
  18. package/dist/client/entry.d.ts +1 -5
  19. package/dist/client/locator.d.ts +125 -0
  20. package/dist/client/snapshot.d.ts +0 -6
  21. package/dist/concurrency.d.ts +12 -0
  22. package/dist/dispatchCapabilities.d.ts +34 -0
  23. package/dist/dispatchRouter.d.ts +20 -0
  24. package/dist/headlessLatestRerunScheduler.d.ts +19 -0
  25. package/dist/headlessTransport.d.ts +12 -0
  26. package/dist/index.js +1608 -296
  27. package/dist/protocol.d.ts +44 -33
  28. package/dist/providers/index.d.ts +79 -0
  29. package/dist/providers/playwright/compileLocator.d.ts +3 -0
  30. package/dist/providers/playwright/dispatchBrowserRpc.d.ts +13 -0
  31. package/dist/providers/playwright/expectUtils.d.ts +24 -0
  32. package/dist/providers/playwright/implementation.d.ts +2 -0
  33. package/dist/providers/playwright/index.d.ts +1 -0
  34. package/dist/providers/playwright/runtime.d.ts +5 -0
  35. package/dist/providers/playwright/textMatcher.d.ts +8 -0
  36. package/dist/rpcProtocol.d.ts +145 -0
  37. package/dist/runSession.d.ts +33 -0
  38. package/dist/sessionRegistry.d.ts +34 -0
  39. package/dist/sourceMap/sourceMapLoader.d.ts +14 -0
  40. package/dist/watchRerunPlanner.d.ts +21 -0
  41. package/package.json +16 -11
  42. package/src/AGENTS.md +128 -0
  43. package/src/augmentExpect.ts +62 -0
  44. package/src/browser.ts +3 -0
  45. package/src/browserRpcRegistry.ts +57 -0
  46. package/src/client/AGENTS.md +82 -0
  47. package/src/client/api.ts +213 -0
  48. package/src/client/browserRpc.ts +86 -0
  49. package/src/client/dispatchTransport.ts +178 -0
  50. package/src/client/entry.ts +109 -39
  51. package/src/client/locator.ts +452 -0
  52. package/src/client/snapshot.ts +32 -97
  53. package/src/client/sourceMapSupport.ts +26 -37
  54. package/src/concurrency.ts +62 -0
  55. package/src/dispatchCapabilities.ts +162 -0
  56. package/src/dispatchRouter.ts +82 -0
  57. package/src/env.d.ts +8 -1
  58. package/src/headlessLatestRerunScheduler.ts +76 -0
  59. package/src/headlessTransport.ts +28 -0
  60. package/src/hostController.ts +1292 -367
  61. package/src/protocol.ts +66 -31
  62. package/src/providers/index.ts +103 -0
  63. package/src/providers/playwright/compileLocator.ts +130 -0
  64. package/src/providers/playwright/dispatchBrowserRpc.ts +372 -0
  65. package/src/providers/playwright/expectUtils.ts +57 -0
  66. package/src/providers/playwright/implementation.ts +33 -0
  67. package/src/providers/playwright/index.ts +1 -0
  68. package/src/providers/playwright/runtime.ts +32 -0
  69. package/src/providers/playwright/textMatcher.ts +10 -0
  70. package/src/rpcProtocol.ts +220 -0
  71. package/src/runSession.ts +110 -0
  72. package/src/sessionRegistry.ts +89 -0
  73. package/src/sourceMap/sourceMapLoader.ts +96 -0
  74. package/src/watchRerunPlanner.ts +77 -0
  75. package/dist/browser-container/container-static/css/index.5a71c757.css +0 -1
  76. package/dist/browser-container/container-static/js/392.28f9a733.js.LICENSE.txt +0 -1
  77. package/dist/browser-container/container-static/js/lib-react.97ee79b0.js.LICENSE.txt +0 -1
  78. package/dist/browser-container/container-static/js/scheduler.6976de44.js +0 -411
  79. package/dist/browser-container/scheduler.html +0 -19
@@ -0,0 +1,178 @@
1
+ import type {
2
+ BrowserDispatchRequest,
3
+ BrowserDispatchResponse,
4
+ } from '../protocol';
5
+ import {
6
+ DISPATCH_MESSAGE_TYPE,
7
+ DISPATCH_RESPONSE_TYPE,
8
+ DISPATCH_RPC_REQUEST_TYPE,
9
+ } from '../protocol';
10
+
11
+ export const DEFAULT_RPC_TIMEOUT_MS = 30_000;
12
+
13
+ export const getRpcTimeout = (): number => {
14
+ return (
15
+ window.__RSTEST_BROWSER_OPTIONS__?.rpcTimeout ?? DEFAULT_RPC_TIMEOUT_MS
16
+ );
17
+ };
18
+
19
+ const pendingRequests = new Map<
20
+ string,
21
+ {
22
+ resolve: (value: unknown) => void;
23
+ reject: (error: Error) => void;
24
+ staleMessage: string;
25
+ }
26
+ >();
27
+
28
+ let requestIdCounter = 0;
29
+ let messageListenerInitialized = false;
30
+
31
+ export const createRequestId = (prefix: string): string => {
32
+ if (typeof globalThis.crypto?.randomUUID === 'function') {
33
+ return globalThis.crypto.randomUUID();
34
+ }
35
+
36
+ requestIdCounter += 1;
37
+ return `${prefix}-${Date.now().toString(36)}-${requestIdCounter.toString(36)}-${Math.random().toString(36).slice(2, 10)}`;
38
+ };
39
+
40
+ const isDispatchResponse = (
41
+ value: unknown,
42
+ ): value is BrowserDispatchResponse => {
43
+ return (
44
+ typeof value === 'object' &&
45
+ value !== null &&
46
+ 'requestId' in value &&
47
+ typeof (value as { requestId: unknown }).requestId === 'string'
48
+ );
49
+ };
50
+
51
+ const settlePendingRequest = (response: BrowserDispatchResponse): void => {
52
+ const pending = pendingRequests.get(response.requestId);
53
+ if (!pending) {
54
+ return;
55
+ }
56
+
57
+ pendingRequests.delete(response.requestId);
58
+ if (response.stale) {
59
+ pending.reject(new Error(pending.staleMessage));
60
+ return;
61
+ }
62
+ if (response.error) {
63
+ pending.reject(new Error(response.error));
64
+ return;
65
+ }
66
+ pending.resolve(response.result);
67
+ };
68
+
69
+ const initMessageListener = (): void => {
70
+ if (messageListenerInitialized) {
71
+ return;
72
+ }
73
+ messageListenerInitialized = true;
74
+
75
+ window.addEventListener('message', (event: MessageEvent) => {
76
+ if (event.data?.type === DISPATCH_RESPONSE_TYPE) {
77
+ settlePendingRequest(event.data.payload as BrowserDispatchResponse);
78
+ }
79
+ });
80
+ };
81
+
82
+ const unwrapDispatchBridgeResult = <T>(
83
+ requestId: string,
84
+ result: unknown,
85
+ staleMessage: string,
86
+ ): T => {
87
+ if (!isDispatchResponse(result)) {
88
+ throw new Error('Invalid dispatch bridge response payload.');
89
+ }
90
+
91
+ if (result.requestId !== requestId) {
92
+ throw new Error(
93
+ `Mismatched dispatch response id: expected ${requestId}, got ${result.requestId}`,
94
+ );
95
+ }
96
+ if (result.stale) {
97
+ throw new Error(staleMessage);
98
+ }
99
+ if (result.error) {
100
+ throw new Error(result.error);
101
+ }
102
+ return result.result as T;
103
+ };
104
+
105
+ export const dispatchRpc = <T>({
106
+ requestId,
107
+ request,
108
+ timeoutMs,
109
+ timeoutMessage,
110
+ staleMessage,
111
+ }: {
112
+ requestId: string;
113
+ request: BrowserDispatchRequest;
114
+ timeoutMs: number;
115
+ timeoutMessage: string;
116
+ staleMessage: string;
117
+ }): Promise<T> => {
118
+ if (window.parent === window) {
119
+ const dispatchBridge = window.__rstest_dispatch_rpc__;
120
+ if (!dispatchBridge) {
121
+ throw new Error(
122
+ 'Dispatch RPC bridge is not available in top-level runner.',
123
+ );
124
+ }
125
+
126
+ return new Promise<T>((resolve, reject) => {
127
+ const timeoutId = setTimeout(() => {
128
+ reject(new Error(timeoutMessage));
129
+ }, timeoutMs);
130
+
131
+ const call = Promise.resolve(dispatchBridge(request)).then((result) =>
132
+ unwrapDispatchBridgeResult<T>(requestId, result, staleMessage),
133
+ );
134
+
135
+ call
136
+ .then((result) => {
137
+ clearTimeout(timeoutId);
138
+ resolve(result);
139
+ })
140
+ .catch((error) => {
141
+ clearTimeout(timeoutId);
142
+ reject(error instanceof Error ? error : new Error(String(error)));
143
+ });
144
+ });
145
+ }
146
+
147
+ initMessageListener();
148
+
149
+ return new Promise<T>((resolve, reject) => {
150
+ const timeoutId = setTimeout(() => {
151
+ pendingRequests.delete(requestId);
152
+ reject(new Error(timeoutMessage));
153
+ }, timeoutMs);
154
+
155
+ pendingRequests.set(requestId, {
156
+ staleMessage,
157
+ resolve: (value) => {
158
+ clearTimeout(timeoutId);
159
+ resolve(value as T);
160
+ },
161
+ reject: (error) => {
162
+ clearTimeout(timeoutId);
163
+ reject(error);
164
+ },
165
+ });
166
+
167
+ window.parent.postMessage(
168
+ {
169
+ type: DISPATCH_MESSAGE_TYPE,
170
+ payload: {
171
+ type: DISPATCH_RPC_REQUEST_TYPE,
172
+ payload: request,
173
+ },
174
+ },
175
+ '*',
176
+ );
177
+ });
178
+ };
@@ -20,9 +20,15 @@ import {
20
20
  import { normalize } from 'pathe';
21
21
  import type {
22
22
  BrowserClientMessage,
23
- BrowserHostConfig,
23
+ BrowserDispatchRequest,
24
24
  BrowserProjectRuntime,
25
25
  } from '../protocol';
26
+ import {
27
+ DISPATCH_MESSAGE_TYPE,
28
+ DISPATCH_NAMESPACE_RUNNER,
29
+ DISPATCH_RPC_REQUEST_TYPE,
30
+ RSTEST_CONFIG_MESSAGE_TYPE,
31
+ } from '../protocol';
26
32
  import { BrowserSnapshotEnvironment } from './snapshot';
27
33
  import {
28
34
  findNewScriptUrl,
@@ -32,14 +38,18 @@ import {
32
38
  } from './sourceMapSupport';
33
39
 
34
40
  declare global {
35
- interface Window {
36
- __RSTEST_BROWSER_OPTIONS__?: BrowserHostConfig;
37
- __rstest_dispatch__?: (message: BrowserClientMessage) => void;
38
- }
39
41
  // eslint-disable-next-line no-var
40
42
  var __coverage__: Record<string, unknown> | undefined;
41
43
  }
42
44
 
45
+ type RunnerLifecycleMethod =
46
+ | 'file-ready'
47
+ | 'suite-start'
48
+ | 'suite-result'
49
+ | 'case-start';
50
+
51
+ let runnerDispatchRequestId = 0;
52
+
43
53
  /**
44
54
  * Debug logger for browser client.
45
55
  * Only logs when debug mode is enabled (DEBUG=rstest on server side).
@@ -50,10 +60,13 @@ const debugLog = (...args: unknown[]): void => {
50
60
  }
51
61
  };
52
62
 
53
- type GlobalWithProcess = typeof globalThis & {
54
- global?: typeof globalThis;
55
- process?: NodeJS.Process;
56
- };
63
+ type RuntimeEnvStore = Record<string, string | undefined>;
64
+ const RSTEST_ENV_SYMBOL = Symbol.for('rstest.env');
65
+
66
+ type GlobalWithRuntimeEnv = typeof globalThis &
67
+ Record<symbol, unknown> & {
68
+ global?: typeof globalThis;
69
+ };
57
70
 
58
71
  const REGEXP_FLAG_PREFIX = 'RSTEST_REGEXP:';
59
72
 
@@ -82,36 +95,34 @@ const restoreRuntimeConfig = (
82
95
  };
83
96
  };
84
97
 
85
- const ensureProcessEnv = (env: RuntimeConfig['env'] | undefined): void => {
86
- const globalRef = globalThis as GlobalWithProcess;
98
+ const ensureRuntimeEnv = (env: RuntimeConfig['env'] | undefined): void => {
99
+ const globalRef = globalThis as GlobalWithRuntimeEnv;
87
100
  if (!globalRef.global) {
88
101
  globalRef.global = globalRef;
89
102
  }
90
103
 
91
- if (!globalRef.process) {
92
- const processShim: Partial<NodeJS.Process> & {
93
- env: Record<string, string | undefined>;
94
- } = {
95
- env: {},
96
- argv: [],
97
- version: 'browser',
98
- cwd: () => '/',
99
- platform: 'linux',
100
- nextTick: (cb: (...args: unknown[]) => void, ...args: unknown[]) =>
101
- queueMicrotask(() => cb(...args)),
102
- };
103
-
104
- globalRef.process = processShim as unknown as NodeJS.Process;
104
+ const existingEnv = globalRef[RSTEST_ENV_SYMBOL];
105
+ let runtimeEnv: RuntimeEnvStore;
106
+ if (existingEnv && typeof existingEnv === 'object') {
107
+ runtimeEnv = existingEnv as RuntimeEnvStore;
108
+ } else {
109
+ runtimeEnv = {};
110
+ globalRef[RSTEST_ENV_SYMBOL] = runtimeEnv;
105
111
  }
106
112
 
107
- globalRef.process.env ??= {};
108
-
109
113
  if (env) {
110
114
  for (const [key, value] of Object.entries(env)) {
111
- if (value === undefined) {
112
- delete globalRef.process.env[key];
115
+ const normalizedValue =
116
+ typeof value === 'string'
117
+ ? value
118
+ : value == null
119
+ ? undefined
120
+ : String(value);
121
+
122
+ if (normalizedValue === undefined) {
123
+ delete runtimeEnv[key];
113
124
  } else {
114
- globalRef.process.env[key] = value;
125
+ runtimeEnv[key] = normalizedValue;
115
126
  }
116
127
  }
117
128
  }
@@ -206,7 +217,7 @@ const send = (message: BrowserClientMessage): void => {
206
217
  // If in iframe, send to parent window (container) which will forward to host via RPC
207
218
  if (window.parent !== window) {
208
219
  window.parent.postMessage(
209
- { type: '__rstest_dispatch__', payload: message },
220
+ { type: DISPATCH_MESSAGE_TYPE, payload: message },
210
221
  '*',
211
222
  );
212
223
  return;
@@ -216,6 +227,38 @@ const send = (message: BrowserClientMessage): void => {
216
227
  window.__rstest_dispatch__?.(message);
217
228
  };
218
229
 
230
+ const dispatchRunnerLifecycle = (
231
+ method: RunnerLifecycleMethod,
232
+ payload: unknown,
233
+ ): void => {
234
+ const request: BrowserDispatchRequest = {
235
+ requestId: `runner-lifecycle-${++runnerDispatchRequestId}`,
236
+ namespace: DISPATCH_NAMESPACE_RUNNER,
237
+ method,
238
+ args: payload,
239
+ };
240
+
241
+ if (window.parent === window) {
242
+ const dispatchBridge = window.__rstest_dispatch_rpc__;
243
+ if (!dispatchBridge) {
244
+ debugLog(
245
+ '[Runner] Missing dispatch bridge for lifecycle method:',
246
+ method,
247
+ );
248
+ return;
249
+ }
250
+ void Promise.resolve(dispatchBridge(request)).catch((error: unknown) => {
251
+ debugLog('[Runner] Failed to dispatch lifecycle method:', method, error);
252
+ });
253
+ return;
254
+ }
255
+
256
+ send({
257
+ type: DISPATCH_RPC_REQUEST_TYPE,
258
+ payload: request,
259
+ });
260
+ };
261
+
219
262
  /** Timeout for waiting for browser config from container (30 seconds) */
220
263
  const CONFIG_WAIT_TIMEOUT_MS = 30_000;
221
264
 
@@ -231,7 +274,7 @@ const waitForConfig = (): Promise<void> => {
231
274
 
232
275
  return new Promise((resolve, reject) => {
233
276
  const handleMessage = (event: MessageEvent) => {
234
- if (event.data?.type === 'RSTEST_CONFIG') {
277
+ if (event.data?.type === RSTEST_CONFIG_MESSAGE_TYPE) {
235
278
  window.__RSTEST_BROWSER_OPTIONS__ = event.data.payload;
236
279
  debugLog(
237
280
  '[Runner] Received config from container:',
@@ -324,6 +367,7 @@ const run = async () => {
324
367
  // Support reading testFile and testNamePattern from URL parameters
325
368
  const urlParams = new URLSearchParams(window.location.search);
326
369
  const urlTestFile = urlParams.get('testFile');
370
+ const urlRunId = urlParams.get('runId');
327
371
  const urlTestNamePattern = urlParams.get('testNamePattern');
328
372
 
329
373
  if (urlTestFile && options) {
@@ -334,6 +378,13 @@ const run = async () => {
334
378
  };
335
379
  }
336
380
 
381
+ if (urlRunId && options) {
382
+ options = {
383
+ ...options,
384
+ runId: urlRunId,
385
+ };
386
+ }
387
+
337
388
  // Override testNamePattern from URL parameter if provided
338
389
  if (urlTestNamePattern && options) {
339
390
  options = {
@@ -404,7 +455,7 @@ const run = async () => {
404
455
  }
405
456
 
406
457
  const runtimeConfig = restoreRuntimeConfig(projectRuntime.runtimeConfig);
407
- ensureProcessEnv(runtimeConfig.env);
458
+ ensureRuntimeEnv(runtimeConfig.env);
408
459
 
409
460
  // Get this project's setup loaders and test context
410
461
  const currentSetupLoaders =
@@ -426,12 +477,13 @@ const run = async () => {
426
477
  return;
427
478
  }
428
479
 
429
- // 1. Load setup files for this project
430
- for (const loadSetup of currentSetupLoaders) {
431
- await loadSetup();
432
- }
480
+ const loadSetupFiles = async (): Promise<void> => {
481
+ for (const loadSetup of currentSetupLoaders) {
482
+ await loadSetup();
483
+ }
484
+ };
433
485
 
434
- // 2. Determine which test files to run
486
+ // 1. Determine which test files to run
435
487
  let testKeysToRun: string[];
436
488
 
437
489
  if (targetTestFile) {
@@ -478,6 +530,9 @@ const run = async () => {
478
530
  }
479
531
 
480
532
  try {
533
+ // Load setup files for this project after runtime is ready.
534
+ await loadSetupFiles();
535
+
481
536
  // Load the test file dynamically (registers tests without running)
482
537
  await currentTestContext.loadTest(key);
483
538
 
@@ -512,7 +567,7 @@ const run = async () => {
512
567
  return;
513
568
  }
514
569
 
515
- // 3. Run tests for each file
570
+ // 2. Run tests for each file
516
571
  for (const key of testKeysToRun) {
517
572
  const testPath = toAbsolutePath(key, currentProject.projectRoot);
518
573
 
@@ -552,6 +607,18 @@ const run = async () => {
552
607
  let failedTestsCount = 0;
553
608
 
554
609
  const runnerHooks: RunnerHooks = {
610
+ onTestFileReady: async (test) => {
611
+ dispatchRunnerLifecycle('file-ready', test);
612
+ },
613
+ onTestSuiteStart: async (test) => {
614
+ dispatchRunnerLifecycle('suite-start', test);
615
+ },
616
+ onTestSuiteResult: async (result) => {
617
+ dispatchRunnerLifecycle('suite-result', result);
618
+ },
619
+ onTestCaseStart: async (test) => {
620
+ dispatchRunnerLifecycle('case-start', test);
621
+ },
555
622
  onTestCaseResult: async (result) => {
556
623
  if (result.status === 'fail') {
557
624
  failedTestsCount++;
@@ -573,6 +640,9 @@ const run = async () => {
573
640
  });
574
641
 
575
642
  try {
643
+ // Load setup files for this project after runtime is ready.
644
+ await loadSetupFiles();
645
+
576
646
  // Record script URLs before loading the test file
577
647
  const beforeScripts = getScriptUrls();
578
648