@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.
- package/LICENSE-APACHE-2.0 +202 -0
- package/NOTICE +11 -0
- package/dist/361.js +8 -0
- package/dist/augmentExpect.d.ts +73 -0
- package/dist/browser-container/container-static/css/index.5c72297783.css +1 -0
- package/dist/browser-container/container-static/js/{392.28f9a733.js → 101.36a8ccdf84.js} +4068 -3904
- package/dist/browser-container/container-static/js/101.36a8ccdf84.js.LICENSE.txt +1 -0
- package/dist/browser-container/container-static/js/{index.129eaf9c.js → index.0687a8142a.js} +742 -692
- package/dist/browser-container/container-static/js/{lib-react.97ee79b0.js → lib-react.dcf2a5e57a.js} +10 -10
- package/dist/browser-container/container-static/js/lib-react.dcf2a5e57a.js.LICENSE.txt +1 -0
- package/dist/browser-container/index.html +1 -1
- package/dist/browser.d.ts +2 -0
- package/dist/browser.js +583 -0
- package/dist/browserRpcRegistry.d.ts +18 -0
- package/dist/client/api.d.ts +3 -0
- package/dist/client/browserRpc.d.ts +2 -0
- package/dist/client/dispatchTransport.d.ts +11 -0
- package/dist/client/entry.d.ts +1 -5
- package/dist/client/locator.d.ts +125 -0
- package/dist/client/snapshot.d.ts +0 -6
- package/dist/concurrency.d.ts +12 -0
- package/dist/dispatchCapabilities.d.ts +34 -0
- package/dist/dispatchRouter.d.ts +20 -0
- package/dist/headlessLatestRerunScheduler.d.ts +19 -0
- package/dist/headlessTransport.d.ts +12 -0
- package/dist/index.js +1608 -296
- package/dist/protocol.d.ts +44 -33
- package/dist/providers/index.d.ts +79 -0
- package/dist/providers/playwright/compileLocator.d.ts +3 -0
- package/dist/providers/playwright/dispatchBrowserRpc.d.ts +13 -0
- package/dist/providers/playwright/expectUtils.d.ts +24 -0
- package/dist/providers/playwright/implementation.d.ts +2 -0
- package/dist/providers/playwright/index.d.ts +1 -0
- package/dist/providers/playwright/runtime.d.ts +5 -0
- package/dist/providers/playwright/textMatcher.d.ts +8 -0
- package/dist/rpcProtocol.d.ts +145 -0
- package/dist/runSession.d.ts +33 -0
- package/dist/sessionRegistry.d.ts +34 -0
- package/dist/sourceMap/sourceMapLoader.d.ts +14 -0
- package/dist/watchRerunPlanner.d.ts +21 -0
- package/package.json +16 -11
- package/src/AGENTS.md +128 -0
- package/src/augmentExpect.ts +62 -0
- package/src/browser.ts +3 -0
- package/src/browserRpcRegistry.ts +57 -0
- package/src/client/AGENTS.md +82 -0
- package/src/client/api.ts +213 -0
- package/src/client/browserRpc.ts +86 -0
- package/src/client/dispatchTransport.ts +178 -0
- package/src/client/entry.ts +109 -39
- package/src/client/locator.ts +452 -0
- package/src/client/snapshot.ts +32 -97
- package/src/client/sourceMapSupport.ts +26 -37
- package/src/concurrency.ts +62 -0
- package/src/dispatchCapabilities.ts +162 -0
- package/src/dispatchRouter.ts +82 -0
- package/src/env.d.ts +8 -1
- package/src/headlessLatestRerunScheduler.ts +76 -0
- package/src/headlessTransport.ts +28 -0
- package/src/hostController.ts +1292 -367
- package/src/protocol.ts +66 -31
- package/src/providers/index.ts +103 -0
- package/src/providers/playwright/compileLocator.ts +130 -0
- package/src/providers/playwright/dispatchBrowserRpc.ts +372 -0
- package/src/providers/playwright/expectUtils.ts +57 -0
- package/src/providers/playwright/implementation.ts +33 -0
- package/src/providers/playwright/index.ts +1 -0
- package/src/providers/playwright/runtime.ts +32 -0
- package/src/providers/playwright/textMatcher.ts +10 -0
- package/src/rpcProtocol.ts +220 -0
- package/src/runSession.ts +110 -0
- package/src/sessionRegistry.ts +89 -0
- package/src/sourceMap/sourceMapLoader.ts +96 -0
- package/src/watchRerunPlanner.ts +77 -0
- package/dist/browser-container/container-static/css/index.5a71c757.css +0 -1
- package/dist/browser-container/container-static/js/392.28f9a733.js.LICENSE.txt +0 -1
- package/dist/browser-container/container-static/js/lib-react.97ee79b0.js.LICENSE.txt +0 -1
- package/dist/browser-container/container-static/js/scheduler.6976de44.js +0 -411
- 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
|
+
};
|
package/src/client/snapshot.ts
CHANGED
|
@@ -1,68 +1,27 @@
|
|
|
1
|
-
import type {
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
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
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
return
|
|
24
|
-
|
|
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
|
-
|
|
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
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
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
|
|
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
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
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
|
-
|
|
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
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
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
|
-
|
|
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
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
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);
|