@rstest/browser 0.8.5 → 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/{565.226c9ef5.js → 101.36a8ccdf84.js} +4024 -3856
  7. package/dist/browser-container/container-static/js/101.36a8ccdf84.js.LICENSE.txt +1 -0
  8. package/dist/browser-container/container-static/js/{index.c1d17467.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 +1580 -258
  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 +15 -10
  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 +96 -33
  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/565.226c9ef5.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.5accca0c.js +0 -407
  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 =
@@ -556,6 +607,18 @@ const run = async () => {
556
607
  let failedTestsCount = 0;
557
608
 
558
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
+ },
559
622
  onTestCaseResult: async (result) => {
560
623
  if (result.status === 'fail') {
561
624
  failedTestsCount++;