@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,452 @@
1
+ import type {
2
+ BrowserLocatorIR,
3
+ BrowserLocatorText,
4
+ BrowserRpcRequest,
5
+ } from '../rpcProtocol';
6
+ import { callBrowserRpc } from './browserRpc';
7
+
8
+ export const serializeText = (value: string | RegExp): BrowserLocatorText => {
9
+ if (typeof value === 'string') {
10
+ return { type: 'string', value };
11
+ }
12
+ return { type: 'regexp', source: value.source, flags: value.flags };
13
+ };
14
+
15
+ export type LocatorGetByRoleOptions = {
16
+ name?: string | RegExp;
17
+ exact?: boolean;
18
+ checked?: boolean;
19
+ disabled?: boolean;
20
+ expanded?: boolean;
21
+ selected?: boolean;
22
+ pressed?: boolean;
23
+ includeHidden?: boolean;
24
+ level?: number;
25
+ };
26
+
27
+ export type LocatorTextOptions = {
28
+ exact?: boolean;
29
+ };
30
+
31
+ export type LocatorKeyboardModifier =
32
+ | 'Alt'
33
+ | 'Control'
34
+ | 'ControlOrMeta'
35
+ | 'Meta'
36
+ | 'Shift';
37
+
38
+ export type LocatorMouseButton = 'left' | 'right' | 'middle';
39
+
40
+ export type LocatorPosition = {
41
+ x: number;
42
+ y: number;
43
+ };
44
+
45
+ export type LocatorClickOptions = {
46
+ button?: LocatorMouseButton;
47
+ clickCount?: number;
48
+ delay?: number;
49
+ force?: boolean;
50
+ modifiers?: LocatorKeyboardModifier[];
51
+ position?: LocatorPosition;
52
+ timeout?: number;
53
+ trial?: boolean;
54
+ };
55
+
56
+ export type LocatorDblclickOptions = Omit<LocatorClickOptions, 'clickCount'>;
57
+
58
+ export type LocatorHoverOptions = Pick<
59
+ LocatorClickOptions,
60
+ 'force' | 'modifiers' | 'position' | 'timeout' | 'trial'
61
+ >;
62
+
63
+ export type LocatorPressOptions = {
64
+ delay?: number;
65
+ timeout?: number;
66
+ };
67
+
68
+ export type LocatorFillOptions = {
69
+ force?: boolean;
70
+ timeout?: number;
71
+ };
72
+
73
+ export type LocatorCheckOptions = {
74
+ force?: boolean;
75
+ position?: LocatorPosition;
76
+ timeout?: number;
77
+ trial?: boolean;
78
+ };
79
+
80
+ export type LocatorFocusOptions = {
81
+ timeout?: number;
82
+ };
83
+
84
+ export type LocatorBlurOptions = {
85
+ timeout?: number;
86
+ };
87
+
88
+ export type LocatorScrollIntoViewIfNeededOptions = {
89
+ timeout?: number;
90
+ };
91
+
92
+ export type LocatorWaitForOptions = {
93
+ state?: 'attached' | 'detached' | 'visible' | 'hidden';
94
+ timeout?: number;
95
+ };
96
+
97
+ export type BrowserSerializable =
98
+ | null
99
+ | boolean
100
+ | number
101
+ | string
102
+ | BrowserSerializable[]
103
+ | { [key: string]: BrowserSerializable };
104
+
105
+ export type LocatorDispatchEventInit = BrowserSerializable;
106
+
107
+ export type LocatorSelectOptionOptions = {
108
+ force?: boolean;
109
+ timeout?: number;
110
+ };
111
+
112
+ export type LocatorSetInputFilesOptions = {
113
+ timeout?: number;
114
+ };
115
+
116
+ export type LocatorFilterOptions = {
117
+ hasText?: string | RegExp;
118
+ hasNotText?: string | RegExp;
119
+ has?: Locator;
120
+ hasNot?: Locator;
121
+ };
122
+
123
+ export class Locator {
124
+ readonly ir: BrowserLocatorIR;
125
+
126
+ constructor(ir: BrowserLocatorIR) {
127
+ this.ir = ir;
128
+ }
129
+
130
+ getByRole(role: string, options?: LocatorGetByRoleOptions): Locator {
131
+ const next = {
132
+ steps: [
133
+ ...this.ir.steps,
134
+ {
135
+ type: 'getByRole',
136
+ role,
137
+ options: options
138
+ ? {
139
+ ...options,
140
+ name:
141
+ options.name === undefined
142
+ ? undefined
143
+ : serializeText(options.name),
144
+ }
145
+ : undefined,
146
+ },
147
+ ],
148
+ } satisfies BrowserLocatorIR;
149
+ return new Locator(next);
150
+ }
151
+
152
+ locator(selector: string): Locator {
153
+ return new Locator({
154
+ steps: [...this.ir.steps, { type: 'locator', selector }],
155
+ });
156
+ }
157
+
158
+ getByText(text: string | RegExp, options?: LocatorTextOptions): Locator {
159
+ return new Locator({
160
+ steps: [
161
+ ...this.ir.steps,
162
+ { type: 'getByText', text: serializeText(text), options },
163
+ ],
164
+ });
165
+ }
166
+
167
+ getByLabel(text: string | RegExp, options?: LocatorTextOptions): Locator {
168
+ return new Locator({
169
+ steps: [
170
+ ...this.ir.steps,
171
+ { type: 'getByLabel', text: serializeText(text), options },
172
+ ],
173
+ });
174
+ }
175
+
176
+ getByPlaceholder(
177
+ text: string | RegExp,
178
+ options?: LocatorTextOptions,
179
+ ): Locator {
180
+ return new Locator({
181
+ steps: [
182
+ ...this.ir.steps,
183
+ { type: 'getByPlaceholder', text: serializeText(text), options },
184
+ ],
185
+ });
186
+ }
187
+
188
+ getByAltText(text: string | RegExp, options?: LocatorTextOptions): Locator {
189
+ return new Locator({
190
+ steps: [
191
+ ...this.ir.steps,
192
+ { type: 'getByAltText', text: serializeText(text), options },
193
+ ],
194
+ });
195
+ }
196
+
197
+ getByTitle(text: string | RegExp, options?: LocatorTextOptions): Locator {
198
+ return new Locator({
199
+ steps: [
200
+ ...this.ir.steps,
201
+ { type: 'getByTitle', text: serializeText(text), options },
202
+ ],
203
+ });
204
+ }
205
+
206
+ getByTestId(text: string | RegExp): Locator {
207
+ return new Locator({
208
+ steps: [
209
+ ...this.ir.steps,
210
+ { type: 'getByTestId', text: serializeText(text) },
211
+ ],
212
+ });
213
+ }
214
+
215
+ filter(options: LocatorFilterOptions): Locator {
216
+ return new Locator({
217
+ steps: [
218
+ ...this.ir.steps,
219
+ {
220
+ type: 'filter',
221
+ options: {
222
+ hasText: options.hasText
223
+ ? serializeText(options.hasText)
224
+ : undefined,
225
+ hasNotText: options.hasNotText
226
+ ? serializeText(options.hasNotText)
227
+ : undefined,
228
+ has:
229
+ options.has === undefined
230
+ ? undefined
231
+ : isLocator(options.has)
232
+ ? options.has.ir
233
+ : (() => {
234
+ throw new TypeError(
235
+ 'Locator.filter({ has }) expects a Locator returned from @rstest/browser page.getBy* APIs.',
236
+ );
237
+ })(),
238
+ hasNot:
239
+ options.hasNot === undefined
240
+ ? undefined
241
+ : isLocator(options.hasNot)
242
+ ? options.hasNot.ir
243
+ : (() => {
244
+ throw new TypeError(
245
+ 'Locator.filter({ hasNot }) expects a Locator returned from @rstest/browser page.getBy* APIs.',
246
+ );
247
+ })(),
248
+ },
249
+ },
250
+ ],
251
+ });
252
+ }
253
+
254
+ and(other: Locator): Locator {
255
+ if (!isLocator(other)) {
256
+ throw new TypeError(
257
+ 'Locator.and() expects a Locator returned from @rstest/browser page.getBy* APIs.',
258
+ );
259
+ }
260
+ return new Locator({
261
+ steps: [...this.ir.steps, { type: 'and', locator: other.ir }],
262
+ });
263
+ }
264
+
265
+ or(other: Locator): Locator {
266
+ if (!isLocator(other)) {
267
+ throw new TypeError(
268
+ 'Locator.or() expects a Locator returned from @rstest/browser page.getBy* APIs.',
269
+ );
270
+ }
271
+ return new Locator({
272
+ steps: [...this.ir.steps, { type: 'or', locator: other.ir }],
273
+ });
274
+ }
275
+
276
+ nth(index: number): Locator {
277
+ return new Locator({ steps: [...this.ir.steps, { type: 'nth', index }] });
278
+ }
279
+
280
+ first(): Locator {
281
+ return new Locator({ steps: [...this.ir.steps, { type: 'first' }] });
282
+ }
283
+
284
+ last(): Locator {
285
+ return new Locator({ steps: [...this.ir.steps, { type: 'last' }] });
286
+ }
287
+
288
+ async click(options?: LocatorClickOptions): Promise<void> {
289
+ await this.callLocator('click', options === undefined ? [] : [options]);
290
+ }
291
+
292
+ async dblclick(options?: LocatorDblclickOptions): Promise<void> {
293
+ await this.callLocator('dblclick', options === undefined ? [] : [options]);
294
+ }
295
+
296
+ async fill(value: string, options?: LocatorFillOptions): Promise<void> {
297
+ await this.callLocator(
298
+ 'fill',
299
+ options === undefined ? [value] : [value, options],
300
+ );
301
+ }
302
+
303
+ async hover(options?: LocatorHoverOptions): Promise<void> {
304
+ await this.callLocator('hover', options === undefined ? [] : [options]);
305
+ }
306
+
307
+ async press(key: string, options?: LocatorPressOptions): Promise<void> {
308
+ await this.callLocator(
309
+ 'press',
310
+ options === undefined ? [key] : [key, options],
311
+ );
312
+ }
313
+
314
+ async clear(): Promise<void> {
315
+ await this.callLocator('clear', []);
316
+ }
317
+
318
+ async check(options?: LocatorCheckOptions): Promise<void> {
319
+ await this.callLocator('check', options === undefined ? [] : [options]);
320
+ }
321
+
322
+ async uncheck(options?: LocatorCheckOptions): Promise<void> {
323
+ await this.callLocator('uncheck', options === undefined ? [] : [options]);
324
+ }
325
+
326
+ async focus(options?: LocatorFocusOptions): Promise<void> {
327
+ await this.callLocator('focus', options === undefined ? [] : [options]);
328
+ }
329
+
330
+ async blur(options?: LocatorBlurOptions): Promise<void> {
331
+ await this.callLocator('blur', options === undefined ? [] : [options]);
332
+ }
333
+
334
+ async scrollIntoViewIfNeeded(
335
+ options?: LocatorScrollIntoViewIfNeededOptions,
336
+ ): Promise<void> {
337
+ await this.callLocator(
338
+ 'scrollIntoViewIfNeeded',
339
+ options === undefined ? [] : [options],
340
+ );
341
+ }
342
+
343
+ async waitFor(options?: LocatorWaitForOptions): Promise<void> {
344
+ await this.callLocator('waitFor', options === undefined ? [] : [options]);
345
+ }
346
+
347
+ async dispatchEvent(
348
+ type: string,
349
+ eventInit?: LocatorDispatchEventInit,
350
+ ): Promise<void> {
351
+ if (typeof type !== 'string' || !type) {
352
+ throw new TypeError(
353
+ 'Locator.dispatchEvent() expects a non-empty event type string.',
354
+ );
355
+ }
356
+ await this.callLocator(
357
+ 'dispatchEvent',
358
+ eventInit === undefined ? [type] : [type, eventInit],
359
+ );
360
+ }
361
+
362
+ async selectOption(
363
+ value: string | string[],
364
+ options?: LocatorSelectOptionOptions,
365
+ ): Promise<void> {
366
+ if (
367
+ typeof value !== 'string' &&
368
+ !(Array.isArray(value) && value.every((v) => typeof v === 'string'))
369
+ ) {
370
+ throw new TypeError(
371
+ 'Locator.selectOption() only supports string or string[] values in browser mode.',
372
+ );
373
+ }
374
+ await this.callLocator(
375
+ 'selectOption',
376
+ options === undefined ? [value] : [value, options],
377
+ );
378
+ }
379
+
380
+ async setInputFiles(
381
+ files: string | string[],
382
+ options?: LocatorSetInputFilesOptions,
383
+ ): Promise<void> {
384
+ if (
385
+ typeof files !== 'string' &&
386
+ !(Array.isArray(files) && files.every((v) => typeof v === 'string'))
387
+ ) {
388
+ throw new TypeError(
389
+ 'Locator.setInputFiles() only supports file path string or string[] in browser mode.',
390
+ );
391
+ }
392
+ await this.callLocator(
393
+ 'setInputFiles',
394
+ options === undefined ? [files] : [files, options],
395
+ );
396
+ }
397
+
398
+ private async callLocator(method: string, args: unknown[]): Promise<void> {
399
+ await callBrowserRpc<void>({
400
+ kind: 'locator',
401
+ locator: this.ir,
402
+ method,
403
+ args,
404
+ } satisfies Omit<BrowserRpcRequest, 'id' | 'testPath' | 'runId'>);
405
+ }
406
+ }
407
+
408
+ const browserPageQueryMethods = [
409
+ 'locator',
410
+ 'getByRole',
411
+ 'getByText',
412
+ 'getByLabel',
413
+ 'getByPlaceholder',
414
+ 'getByAltText',
415
+ 'getByTitle',
416
+ 'getByTestId',
417
+ ] as const;
418
+
419
+ type BrowserPageQueryMethod = (typeof browserPageQueryMethods)[number];
420
+
421
+ export type BrowserPage = Pick<Locator, BrowserPageQueryMethod>;
422
+
423
+ const rootLocator = new Locator({ steps: [] });
424
+
425
+ const createBrowserPage = (): BrowserPage => {
426
+ return Object.fromEntries(
427
+ browserPageQueryMethods.map((methodName) => {
428
+ return [methodName, rootLocator[methodName].bind(rootLocator)];
429
+ }),
430
+ ) as BrowserPage;
431
+ };
432
+
433
+ export const page: BrowserPage = createBrowserPage();
434
+
435
+ export const isLocator = (value: unknown): value is Locator => {
436
+ return value instanceof Locator;
437
+ };
438
+
439
+ /**
440
+ * Configure the attribute used by `getByTestId()` queries.
441
+ * Forwards to the host provider (e.g. Playwright `selectors.setTestIdAttribute()`).
442
+ *
443
+ * @default 'data-testid'
444
+ */
445
+ export const setTestIdAttribute = async (attribute: string): Promise<void> => {
446
+ await callBrowserRpc<void>({
447
+ kind: 'config',
448
+ locator: { steps: [] },
449
+ method: 'setTestIdAttribute',
450
+ args: [attribute],
451
+ } satisfies Omit<BrowserRpcRequest, 'id' | 'testPath' | 'runId'>);
452
+ };
@@ -1,68 +1,27 @@
1
- import type {
2
- BrowserHostConfig,
3
- SnapshotRpcRequest,
4
- SnapshotRpcResponse,
5
- } from '../protocol';
1
+ import type { BrowserDispatchRequest, SnapshotRpcRequest } from '../protocol';
2
+ import { DISPATCH_NAMESPACE_SNAPSHOT } from '../protocol';
3
+ import {
4
+ createRequestId,
5
+ dispatchRpc,
6
+ getRpcTimeout,
7
+ } from './dispatchTransport';
6
8
  import { mapStackFrame } from './sourceMapSupport';
7
9
 
8
- declare global {
9
- interface Window {
10
- __RSTEST_BROWSER_OPTIONS__?: BrowserHostConfig;
11
- }
12
- }
13
-
14
10
  const SNAPSHOT_HEADER = '// Rstest Snapshot';
15
11
 
16
- /** Default RPC timeout if not specified in config (30 seconds) */
17
- const DEFAULT_RPC_TIMEOUT_MS = 30_000;
18
-
19
- /**
20
- * Get RPC timeout from browser options or use default.
21
- */
22
- const getRpcTimeout = (): number => {
23
- return (
24
- window.__RSTEST_BROWSER_OPTIONS__?.rpcTimeout ?? DEFAULT_RPC_TIMEOUT_MS
25
- );
26
- };
27
-
28
- /**
29
- * Pending RPC requests waiting for responses from the container.
30
- */
31
- const pendingRequests = new Map<
32
- string,
33
- {
34
- resolve: (value: unknown) => void;
35
- reject: (error: Error) => void;
36
- }
37
- >();
38
-
39
- let requestIdCounter = 0;
40
- let messageListenerInitialized = false;
41
-
42
- /**
43
- * Initialize the message listener for snapshot RPC responses.
44
- * This is called once when the first RPC request is made.
45
- */
46
- const initMessageListener = (): void => {
47
- if (messageListenerInitialized) {
48
- return;
49
- }
50
- messageListenerInitialized = true;
51
-
52
- window.addEventListener('message', (event: MessageEvent) => {
53
- if (event.data?.type === '__rstest_snapshot_response__') {
54
- const response = event.data.payload as SnapshotRpcResponse;
55
- const pending = pendingRequests.get(response.id);
56
- if (pending) {
57
- pendingRequests.delete(response.id);
58
- if (response.error) {
59
- pending.reject(new Error(response.error));
60
- } else {
61
- pending.resolve(response.result);
62
- }
63
- }
64
- }
65
- });
12
+ const createSnapshotDispatchRequest = (
13
+ requestId: string,
14
+ method: SnapshotRpcRequest['method'],
15
+ args: SnapshotRpcRequest['args'],
16
+ ): BrowserDispatchRequest => {
17
+ // Snapshot is just one namespace on the shared dispatch RPC channel.
18
+ // Keep this mapping explicit so new runner-side RPC clients can mirror it.
19
+ return {
20
+ requestId,
21
+ namespace: DISPATCH_NAMESPACE_SNAPSHOT,
22
+ method,
23
+ args,
24
+ };
66
25
  };
67
26
 
68
27
  /**
@@ -73,44 +32,20 @@ const sendRpcRequest = <T>(
73
32
  method: SnapshotRpcRequest['method'],
74
33
  args: SnapshotRpcRequest['args'],
75
34
  ): Promise<T> => {
76
- initMessageListener();
77
-
78
- const id = `snapshot-rpc-${++requestIdCounter}`;
35
+ const requestId = createRequestId('snapshot-rpc');
79
36
  const rpcTimeout = getRpcTimeout();
37
+ const dispatchRequest = createSnapshotDispatchRequest(
38
+ requestId,
39
+ method,
40
+ args,
41
+ );
80
42
 
81
- return new Promise<T>((resolve, reject) => {
82
- // Set a timeout for the RPC call
83
- const timeoutId = setTimeout(() => {
84
- pendingRequests.delete(id);
85
- reject(
86
- new Error(
87
- `Snapshot RPC timeout after ${rpcTimeout / 1000}s: ${method}`,
88
- ),
89
- );
90
- }, rpcTimeout);
91
-
92
- pendingRequests.set(id, {
93
- resolve: (value) => {
94
- clearTimeout(timeoutId);
95
- resolve(value as T);
96
- },
97
- reject: (error) => {
98
- clearTimeout(timeoutId);
99
- reject(error);
100
- },
101
- });
102
-
103
- // Send request to parent window (container)
104
- window.parent.postMessage(
105
- {
106
- type: '__rstest_dispatch__',
107
- payload: {
108
- type: 'snapshot-rpc-request',
109
- payload: { id, method, args },
110
- },
111
- },
112
- '*',
113
- );
43
+ return dispatchRpc<T>({
44
+ requestId,
45
+ request: dispatchRequest,
46
+ timeoutMs: rpcTimeout,
47
+ staleMessage: 'Stale snapshot RPC request ignored.',
48
+ timeoutMessage: `Snapshot RPC timeout after ${rpcTimeout / 1000}s: ${method}`,
114
49
  });
115
50
  };
116
51
 
@@ -1,8 +1,13 @@
1
1
  import { originalPositionFor, TraceMap } from '@jridgewell/trace-mapping';
2
- import convert from 'convert-source-map';
2
+ import {
3
+ loadSourceMapWithCache,
4
+ normalizeJavaScriptUrl,
5
+ type SourceMapPayload,
6
+ } from '../sourceMap/sourceMapLoader';
3
7
 
4
8
  // Source map cache: JS URL → TraceMap
5
9
  const sourceMapCache = new Map<string, TraceMap | null>();
10
+ const sourceMapPayloadCache = new Map<string, SourceMapPayload | null>();
6
11
 
7
12
  /**
8
13
  * Get TraceMap for specified URL (sync cache lookup)
@@ -23,40 +28,23 @@ const preloadSourceMap = async (
23
28
  jsUrl: string,
24
29
  force = false,
25
30
  ): Promise<void> => {
26
- if (!force && sourceMapCache.has(jsUrl)) return;
27
-
28
- try {
29
- // First, fetch JS file and try to extract inline source map
30
- const jsResponse = await fetch(jsUrl);
31
- if (!jsResponse.ok) {
32
- sourceMapCache.set(jsUrl, null);
33
- return;
34
- }
35
-
36
- const code = await jsResponse.text();
31
+ const normalizedUrl = normalizeJavaScriptUrl(jsUrl, {
32
+ origin: window.location.origin,
33
+ });
34
+ if (!normalizedUrl) {
35
+ return;
36
+ }
37
37
 
38
- // Try to extract inline source map using convert-source-map
39
- const inlineConverter = convert.fromSource(code);
40
- if (inlineConverter) {
41
- const mapObject = inlineConverter.toObject();
42
- sourceMapCache.set(jsUrl, new TraceMap(mapObject));
43
- return;
44
- }
38
+ if (!force && sourceMapCache.has(normalizedUrl)) return;
45
39
 
46
- // Fallback: try to fetch external .map file
47
- const mapUrl = `${jsUrl}.map`;
48
- const mapResponse = await fetch(mapUrl);
49
- if (mapResponse.ok) {
50
- const mapJson = await mapResponse.json();
51
- sourceMapCache.set(jsUrl, new TraceMap(mapJson));
52
- return;
53
- }
40
+ const sourceMap = await loadSourceMapWithCache({
41
+ jsUrl: normalizedUrl,
42
+ cache: sourceMapPayloadCache,
43
+ force,
44
+ origin: window.location.origin,
45
+ });
54
46
 
55
- // No source map found
56
- sourceMapCache.set(jsUrl, null);
57
- } catch {
58
- sourceMapCache.set(jsUrl, null);
59
- }
47
+ sourceMapCache.set(normalizedUrl, sourceMap ? new TraceMap(sourceMap) : null);
60
48
  };
61
49
 
62
50
  /**
@@ -128,6 +116,7 @@ export const preloadRunnerSourceMap = async (): Promise<void> => {
128
116
  */
129
117
  export const clearCache = (): void => {
130
118
  sourceMapCache.clear();
119
+ sourceMapPayloadCache.clear();
131
120
  };
132
121
 
133
122
  /**
@@ -147,11 +136,11 @@ export interface StackFrame {
147
136
  export const mapStackFrame = (frame: StackFrame): StackFrame => {
148
137
  const { file, line, column } = frame;
149
138
 
150
- // Normalize file path to full URL for cache lookup
151
- let fullUrl = file;
152
- if (!file.startsWith('http://') && !file.startsWith('https://')) {
153
- // Convert relative path to full URL
154
- fullUrl = `${window.location.origin}${file.startsWith('/') ? '' : '/'}${file}`;
139
+ const fullUrl = normalizeJavaScriptUrl(file, {
140
+ origin: window.location.origin,
141
+ });
142
+ if (!fullUrl) {
143
+ return frame;
155
144
  }
156
145
 
157
146
  const traceMap = getSourceMap(fullUrl);