@rstest/browser 0.9.2 → 0.9.3
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/dist/browser-container/container-static/js/{101.82cdbbe145.js → 927.514b181bd2.js} +1710 -1645
- package/dist/browser-container/container-static/js/927.514b181bd2.js.LICENSE.txt +1 -0
- package/dist/browser-container/container-static/js/{index.602d6770fe.js → index.5acf502b10.js} +505 -434
- package/dist/browser-container/container-static/js/{lib-react.ce60b6aea5.js → lib-react.f905279759.js} +4 -4
- package/dist/browser-container/container-static/js/lib-react.f905279759.js.LICENSE.txt +1 -0
- package/dist/browser-container/index.html +1 -1
- package/dist/index.js +145 -30
- package/dist/providers/index.d.ts +10 -0
- package/dist/providers/playwright/runtime.d.ts +2 -1
- package/dist/rslib-runtime.js +20 -0
- package/package.json +4 -3
- package/src/AGENTS.md +1 -1
- package/src/client/AGENTS.md +8 -8
- package/src/configValidation.ts +4 -0
- package/src/hostController.ts +232 -28
- package/src/index.ts +0 -1
- package/src/providers/index.ts +10 -0
- package/src/providers/playwright/implementation.ts +2 -0
- package/src/providers/playwright/runtime.ts +37 -15
- package/dist/browser-container/container-static/js/101.82cdbbe145.js.LICENSE.txt +0 -1
- package/dist/browser-container/container-static/js/lib-react.ce60b6aea5.js.LICENSE.txt +0 -1
package/src/hostController.ts
CHANGED
|
@@ -3,6 +3,7 @@ import fs from 'node:fs/promises';
|
|
|
3
3
|
import type { IncomingMessage, ServerResponse } from 'node:http';
|
|
4
4
|
import type { AddressInfo } from 'node:net';
|
|
5
5
|
import { fileURLToPath } from 'node:url';
|
|
6
|
+
import { isDeepStrictEqual } from 'node:util';
|
|
6
7
|
import type { Rspack } from '@rstest/core';
|
|
7
8
|
import {
|
|
8
9
|
type BrowserTestRunOptions,
|
|
@@ -30,7 +31,7 @@ import {
|
|
|
30
31
|
import { type BirpcReturn, createBirpc } from 'birpc';
|
|
31
32
|
import openEditor from 'open-editor';
|
|
32
33
|
import { basename, dirname, join, normalize, relative, resolve } from 'pathe';
|
|
33
|
-
import
|
|
34
|
+
import picomatch from 'picomatch';
|
|
34
35
|
import sirv from 'sirv';
|
|
35
36
|
import { type WebSocket, WebSocketServer } from 'ws';
|
|
36
37
|
import { getHeadlessConcurrency } from './concurrency';
|
|
@@ -123,10 +124,24 @@ type BrowserProviderProject = {
|
|
|
123
124
|
provider: BrowserProvider;
|
|
124
125
|
};
|
|
125
126
|
|
|
126
|
-
type BrowserLaunchOptions =
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
127
|
+
type BrowserLaunchOptions = {
|
|
128
|
+
provider: BrowserProvider;
|
|
129
|
+
browser: ProjectContext['normalizedConfig']['browser']['browser'];
|
|
130
|
+
headless: ProjectContext['normalizedConfig']['browser']['headless'];
|
|
131
|
+
port: ProjectContext['normalizedConfig']['browser']['port'];
|
|
132
|
+
strictPort: ProjectContext['normalizedConfig']['browser']['strictPort'];
|
|
133
|
+
providerOptions: Record<string, unknown>;
|
|
134
|
+
};
|
|
135
|
+
|
|
136
|
+
const getBrowserProviderOptions = (
|
|
137
|
+
project: ProjectContext,
|
|
138
|
+
): Record<string, unknown> => {
|
|
139
|
+
const browserConfig = project.normalizedConfig.browser as {
|
|
140
|
+
providerOptions?: Record<string, unknown>;
|
|
141
|
+
};
|
|
142
|
+
|
|
143
|
+
return browserConfig.providerOptions ?? {};
|
|
144
|
+
};
|
|
130
145
|
|
|
131
146
|
/** Payload for test file start event */
|
|
132
147
|
type TestFileStartPayload = {
|
|
@@ -157,6 +172,33 @@ type TestFileReadyPayload = ReporterHookArg<'onTestFileReady'>;
|
|
|
157
172
|
type TestSuiteStartPayload = ReporterHookArg<'onTestSuiteStart'>;
|
|
158
173
|
type TestSuiteResultPayload = ReporterHookArg<'onTestSuiteResult'>;
|
|
159
174
|
type TestCaseStartPayload = ReporterHookArg<'onTestCaseStart'>;
|
|
175
|
+
type ReloadTestFileAck = {
|
|
176
|
+
runId: string;
|
|
177
|
+
};
|
|
178
|
+
type HeadedTestFileCompletePayload = TestFileResult & {
|
|
179
|
+
runId?: string;
|
|
180
|
+
};
|
|
181
|
+
|
|
182
|
+
type DeferredPromise<T> = {
|
|
183
|
+
promise: Promise<T>;
|
|
184
|
+
resolve: (value: T | PromiseLike<T>) => void;
|
|
185
|
+
reject: (reason?: unknown) => void;
|
|
186
|
+
};
|
|
187
|
+
|
|
188
|
+
const createDeferredPromise = <T>(): DeferredPromise<T> => {
|
|
189
|
+
let resolve!: DeferredPromise<T>['resolve'];
|
|
190
|
+
let reject!: DeferredPromise<T>['reject'];
|
|
191
|
+
const promise = new Promise<T>((res, rej) => {
|
|
192
|
+
resolve = res;
|
|
193
|
+
reject = rej;
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
return {
|
|
197
|
+
promise,
|
|
198
|
+
resolve,
|
|
199
|
+
reject,
|
|
200
|
+
};
|
|
201
|
+
};
|
|
160
202
|
|
|
161
203
|
/** RPC methods exposed by the host (server) to the container (client) */
|
|
162
204
|
type HostRpcMethods = {
|
|
@@ -166,7 +208,7 @@ type HostRpcMethods = {
|
|
|
166
208
|
// Test result callbacks from container
|
|
167
209
|
onTestFileStart: (payload: TestFileStartPayload) => Promise<void>;
|
|
168
210
|
onTestCaseResult: (payload: TestResult) => Promise<void>;
|
|
169
|
-
onTestFileComplete: (payload:
|
|
211
|
+
onTestFileComplete: (payload: HeadedTestFileCompletePayload) => Promise<void>;
|
|
170
212
|
onLog: (payload: LogPayload) => Promise<void>;
|
|
171
213
|
onFatal: (payload: FatalPayload) => Promise<void>;
|
|
172
214
|
// Generic dispatch endpoint used by runner RPC requests.
|
|
@@ -178,7 +220,10 @@ type HostRpcMethods = {
|
|
|
178
220
|
/** RPC methods exposed by the container (client) to the host (server) */
|
|
179
221
|
type ContainerRpcMethods = {
|
|
180
222
|
onTestFileUpdate: (testFiles: TestFileInfo[]) => Promise<void>;
|
|
181
|
-
reloadTestFile: (
|
|
223
|
+
reloadTestFile: (
|
|
224
|
+
testFile: string,
|
|
225
|
+
testNamePattern?: string,
|
|
226
|
+
) => Promise<ReloadTestFileAck>;
|
|
182
227
|
};
|
|
183
228
|
|
|
184
229
|
type ContainerRpc = BirpcReturn<ContainerRpcMethods, HostRpcMethods>;
|
|
@@ -196,16 +241,27 @@ class ContainerRpcManager {
|
|
|
196
241
|
private ws: WebSocket | null = null;
|
|
197
242
|
private rpc: ContainerRpc | null = null;
|
|
198
243
|
private methods: HostRpcMethods;
|
|
244
|
+
private onDisconnect?: (error: Error) => void;
|
|
245
|
+
private detachActiveSocketListeners: (() => void) | null = null;
|
|
199
246
|
|
|
200
|
-
constructor(
|
|
247
|
+
constructor(
|
|
248
|
+
wss: WebSocketServer,
|
|
249
|
+
methods: HostRpcMethods,
|
|
250
|
+
onDisconnect?: (error: Error) => void,
|
|
251
|
+
) {
|
|
201
252
|
this.wss = wss;
|
|
202
253
|
this.methods = methods;
|
|
254
|
+
this.onDisconnect = onDisconnect;
|
|
203
255
|
this.setupConnectionHandler();
|
|
204
256
|
}
|
|
205
257
|
|
|
206
258
|
/** Update the RPC methods (used when starting a new test run) */
|
|
207
|
-
updateMethods(
|
|
259
|
+
updateMethods(
|
|
260
|
+
methods: HostRpcMethods,
|
|
261
|
+
onDisconnect?: (error: Error) => void,
|
|
262
|
+
): void {
|
|
208
263
|
this.methods = methods;
|
|
264
|
+
this.onDisconnect = onDisconnect;
|
|
209
265
|
// Re-create birpc with new methods if already connected
|
|
210
266
|
if (this.ws && this.ws.readyState === this.ws.OPEN) {
|
|
211
267
|
this.attachWebSocket(this.ws);
|
|
@@ -223,35 +279,68 @@ class ContainerRpcManager {
|
|
|
223
279
|
}
|
|
224
280
|
|
|
225
281
|
private attachWebSocket(ws: WebSocket): void {
|
|
282
|
+
this.detachActiveSocketListeners?.();
|
|
283
|
+
if (this.rpc && !this.rpc.$closed) {
|
|
284
|
+
this.rpc.$close(new Error('Container RPC transport reattached'));
|
|
285
|
+
}
|
|
226
286
|
this.ws = ws;
|
|
287
|
+
const messageHandlers = new WeakMap<
|
|
288
|
+
(data: any) => void,
|
|
289
|
+
(message: any) => void
|
|
290
|
+
>();
|
|
227
291
|
|
|
228
292
|
this.rpc = createBirpc<ContainerRpcMethods, HostRpcMethods>(this.methods, {
|
|
293
|
+
timeout: -1,
|
|
229
294
|
post: (data) => {
|
|
230
295
|
if (ws.readyState === ws.OPEN) {
|
|
231
296
|
ws.send(JSON.stringify(data));
|
|
232
297
|
}
|
|
233
298
|
},
|
|
234
299
|
on: (fn) => {
|
|
235
|
-
|
|
300
|
+
const handler = (message: any) => {
|
|
236
301
|
try {
|
|
237
302
|
const data = JSON.parse(message.toString());
|
|
238
303
|
fn(data);
|
|
239
304
|
} catch {
|
|
240
305
|
// ignore invalid messages
|
|
241
306
|
}
|
|
242
|
-
}
|
|
307
|
+
};
|
|
308
|
+
messageHandlers.set(fn, handler);
|
|
309
|
+
ws.on('message', handler);
|
|
310
|
+
},
|
|
311
|
+
off: (fn) => {
|
|
312
|
+
const handler = messageHandlers.get(fn);
|
|
313
|
+
if (!handler) {
|
|
314
|
+
return;
|
|
315
|
+
}
|
|
316
|
+
ws.off('message', handler);
|
|
317
|
+
messageHandlers.delete(fn);
|
|
243
318
|
},
|
|
244
319
|
});
|
|
245
320
|
|
|
246
|
-
|
|
321
|
+
const handleClose = () => {
|
|
247
322
|
// Only clear if this is still the active connection
|
|
248
323
|
// This prevents a race condition when a new connection is established
|
|
249
324
|
// before the old one's close event fires
|
|
250
325
|
if (this.ws === ws) {
|
|
251
326
|
this.ws = null;
|
|
252
|
-
this.rpc = null;
|
|
253
327
|
}
|
|
254
|
-
|
|
328
|
+
this.detachActiveSocketListeners?.();
|
|
329
|
+
this.detachActiveSocketListeners = null;
|
|
330
|
+
if (this.rpc && !this.rpc.$closed) {
|
|
331
|
+
const disconnectError = new Error(
|
|
332
|
+
'Browser UI WebSocket disconnected before reload completed',
|
|
333
|
+
);
|
|
334
|
+
this.rpc.$close(disconnectError);
|
|
335
|
+
this.onDisconnect?.(disconnectError);
|
|
336
|
+
}
|
|
337
|
+
this.rpc = null;
|
|
338
|
+
};
|
|
339
|
+
|
|
340
|
+
ws.on('close', handleClose);
|
|
341
|
+
this.detachActiveSocketListeners = () => {
|
|
342
|
+
ws.off('close', handleClose);
|
|
343
|
+
};
|
|
255
344
|
}
|
|
256
345
|
|
|
257
346
|
/** Check if a container is currently connected */
|
|
@@ -278,16 +367,15 @@ class ContainerRpcManager {
|
|
|
278
367
|
async reloadTestFile(
|
|
279
368
|
testFile: string,
|
|
280
369
|
testNamePattern?: string,
|
|
281
|
-
): Promise<
|
|
370
|
+
): Promise<ReloadTestFileAck> {
|
|
282
371
|
logger.debug(
|
|
283
372
|
`[Browser UI] reloadTestFile called, rpc: ${this.rpc ? 'exists' : 'null'}, ws: ${this.ws ? 'exists' : 'null'}`,
|
|
284
373
|
);
|
|
285
374
|
if (!this.rpc) {
|
|
286
|
-
|
|
287
|
-
return;
|
|
375
|
+
throw new Error('Browser UI RPC not available for reloadTestFile');
|
|
288
376
|
}
|
|
289
377
|
logger.debug(`[Browser UI] Calling reloadTestFile: ${testFile}`);
|
|
290
|
-
|
|
378
|
+
return this.rpc.reloadTestFile(testFile, testNamePattern);
|
|
291
379
|
}
|
|
292
380
|
}
|
|
293
381
|
|
|
@@ -299,6 +387,7 @@ type BrowserRuntime = {
|
|
|
299
387
|
rsbuildInstance: RsbuildInstance;
|
|
300
388
|
devServer: RsbuildDevServer;
|
|
301
389
|
browser: BrowserProviderBrowser;
|
|
390
|
+
browserLaunchOptions: BrowserLaunchOptions;
|
|
302
391
|
port: number;
|
|
303
392
|
wsPort: number;
|
|
304
393
|
manifestPath: string;
|
|
@@ -724,6 +813,7 @@ const getBrowserLaunchOptions = (
|
|
|
724
813
|
headless: project.normalizedConfig.browser.headless,
|
|
725
814
|
port: project.normalizedConfig.browser.port,
|
|
726
815
|
strictPort: project.normalizedConfig.browser.strictPort,
|
|
816
|
+
providerOptions: getBrowserProviderOptions(project),
|
|
727
817
|
});
|
|
728
818
|
|
|
729
819
|
const ensureConsistentBrowserLaunchOptions = (
|
|
@@ -743,11 +833,12 @@ const ensureConsistentBrowserLaunchOptions = (
|
|
|
743
833
|
options.browser !== firstOptions.browser ||
|
|
744
834
|
options.headless !== firstOptions.headless ||
|
|
745
835
|
options.port !== firstOptions.port ||
|
|
746
|
-
options.strictPort !== firstOptions.strictPort
|
|
836
|
+
options.strictPort !== firstOptions.strictPort ||
|
|
837
|
+
!isDeepStrictEqual(options.providerOptions, firstOptions.providerOptions)
|
|
747
838
|
) {
|
|
748
839
|
throw new Error(
|
|
749
840
|
`Browser launch config mismatch between projects "${firstProject.name}" and "${project.name}". ` +
|
|
750
|
-
'All browser-enabled projects in one run must share provider/browser/headless/port/strictPort.',
|
|
841
|
+
'All browser-enabled projects in one run must share provider/browser/headless/port/strictPort/providerOptions.',
|
|
751
842
|
);
|
|
752
843
|
}
|
|
753
844
|
}
|
|
@@ -1469,11 +1560,13 @@ const createBrowserRuntime = async ({
|
|
|
1469
1560
|
const runtime = await providerImplementation.launchRuntime({
|
|
1470
1561
|
browserName,
|
|
1471
1562
|
headless: forceHeadless ?? browserLaunchOptions.headless,
|
|
1563
|
+
providerOptions: browserLaunchOptions.providerOptions,
|
|
1472
1564
|
});
|
|
1473
1565
|
return {
|
|
1474
1566
|
rsbuildInstance,
|
|
1475
1567
|
devServer,
|
|
1476
1568
|
browser: runtime.browser,
|
|
1569
|
+
browserLaunchOptions,
|
|
1477
1570
|
port,
|
|
1478
1571
|
wsPort,
|
|
1479
1572
|
manifestPath,
|
|
@@ -1802,7 +1895,7 @@ export const runBrowserController = async (
|
|
|
1802
1895
|
}
|
|
1803
1896
|
}
|
|
1804
1897
|
|
|
1805
|
-
const { browser, port, wsPort, wss } = runtime;
|
|
1898
|
+
const { browser, browserLaunchOptions, port, wsPort, wss } = runtime;
|
|
1806
1899
|
const buildTime = Date.now() - buildStart;
|
|
1807
1900
|
|
|
1808
1901
|
// Collect all test files from project entries with project info
|
|
@@ -2208,6 +2301,7 @@ export const runBrowserController = async (
|
|
|
2208
2301
|
|
|
2209
2302
|
const viewport = viewportByProject.get(file.projectName);
|
|
2210
2303
|
const browserContext = await browser.newContext({
|
|
2304
|
+
providerOptions: browserLaunchOptions.providerOptions,
|
|
2211
2305
|
viewport: viewport ?? null,
|
|
2212
2306
|
});
|
|
2213
2307
|
run.contexts.add(browserContext);
|
|
@@ -2673,6 +2767,7 @@ export const runBrowserController = async (
|
|
|
2673
2767
|
} else {
|
|
2674
2768
|
isNewPage = true;
|
|
2675
2769
|
containerContext = await browser.newContext({
|
|
2770
|
+
providerOptions: browserLaunchOptions.providerOptions,
|
|
2676
2771
|
viewport: null,
|
|
2677
2772
|
});
|
|
2678
2773
|
containerPage = await containerContext.newPage();
|
|
@@ -2706,6 +2801,13 @@ export const runBrowserController = async (
|
|
|
2706
2801
|
|
|
2707
2802
|
const dispatchRouter = createDispatchRouter();
|
|
2708
2803
|
const headedReloadQueue = createHeadedSerialTaskQueue();
|
|
2804
|
+
const pendingHeadedReloads = new Map<
|
|
2805
|
+
string,
|
|
2806
|
+
{
|
|
2807
|
+
runId: string;
|
|
2808
|
+
deferred: DeferredPromise<void>;
|
|
2809
|
+
}
|
|
2810
|
+
>();
|
|
2709
2811
|
let enqueueHeadedReload = async (
|
|
2710
2812
|
_file: TestFileInfo,
|
|
2711
2813
|
_testNamePattern?: string,
|
|
@@ -2713,16 +2815,89 @@ export const runBrowserController = async (
|
|
|
2713
2815
|
throw new Error('Headed reload queue is not initialized');
|
|
2714
2816
|
};
|
|
2715
2817
|
|
|
2818
|
+
const rejectPendingHeadedReload = (
|
|
2819
|
+
testPath: string,
|
|
2820
|
+
error: Error,
|
|
2821
|
+
runId?: string,
|
|
2822
|
+
): void => {
|
|
2823
|
+
const pending = pendingHeadedReloads.get(testPath);
|
|
2824
|
+
if (!pending) {
|
|
2825
|
+
return;
|
|
2826
|
+
}
|
|
2827
|
+
if (runId && pending.runId !== runId) {
|
|
2828
|
+
return;
|
|
2829
|
+
}
|
|
2830
|
+
pendingHeadedReloads.delete(testPath);
|
|
2831
|
+
pending.deferred.reject(error);
|
|
2832
|
+
};
|
|
2833
|
+
|
|
2834
|
+
const rejectAllPendingHeadedReloads = (error: Error): void => {
|
|
2835
|
+
for (const [testPath, pending] of pendingHeadedReloads) {
|
|
2836
|
+
pendingHeadedReloads.delete(testPath);
|
|
2837
|
+
pending.deferred.reject(error);
|
|
2838
|
+
}
|
|
2839
|
+
};
|
|
2840
|
+
|
|
2841
|
+
const registerPendingHeadedReload = (
|
|
2842
|
+
testPath: string,
|
|
2843
|
+
runId: string,
|
|
2844
|
+
): Promise<void> => {
|
|
2845
|
+
const previousPending = pendingHeadedReloads.get(testPath);
|
|
2846
|
+
if (previousPending) {
|
|
2847
|
+
previousPending.deferred.reject(
|
|
2848
|
+
new Error(
|
|
2849
|
+
`Reload for "${testPath}" was superseded by a newer request.`,
|
|
2850
|
+
),
|
|
2851
|
+
);
|
|
2852
|
+
pendingHeadedReloads.delete(testPath);
|
|
2853
|
+
}
|
|
2854
|
+
|
|
2855
|
+
const deferred = createDeferredPromise<void>();
|
|
2856
|
+
pendingHeadedReloads.set(testPath, {
|
|
2857
|
+
runId,
|
|
2858
|
+
deferred,
|
|
2859
|
+
});
|
|
2860
|
+
|
|
2861
|
+
return deferred.promise;
|
|
2862
|
+
};
|
|
2863
|
+
|
|
2864
|
+
const resolvePendingHeadedReload = (
|
|
2865
|
+
testPath: string,
|
|
2866
|
+
runId?: string,
|
|
2867
|
+
): void => {
|
|
2868
|
+
const pending = pendingHeadedReloads.get(testPath);
|
|
2869
|
+
if (!pending) {
|
|
2870
|
+
return;
|
|
2871
|
+
}
|
|
2872
|
+
if (runId && pending.runId !== runId) {
|
|
2873
|
+
logger.debug(
|
|
2874
|
+
`[Browser UI] Ignoring stale file-complete for ${testPath}. current=${pending.runId}, incoming=${runId}`,
|
|
2875
|
+
);
|
|
2876
|
+
return;
|
|
2877
|
+
}
|
|
2878
|
+
pendingHeadedReloads.delete(testPath);
|
|
2879
|
+
pending.deferred.resolve();
|
|
2880
|
+
};
|
|
2881
|
+
|
|
2716
2882
|
const reloadTestFileWithTimeout = async (
|
|
2717
2883
|
file: TestFileInfo,
|
|
2718
2884
|
testNamePattern?: string,
|
|
2719
2885
|
): Promise<void> => {
|
|
2720
2886
|
const timeoutMs = getHeadedPerFileTimeoutMs(file);
|
|
2721
2887
|
let timeoutId: ReturnType<typeof setTimeout> | undefined;
|
|
2888
|
+
let reloadAck: ReloadTestFileAck | undefined;
|
|
2722
2889
|
|
|
2723
2890
|
try {
|
|
2891
|
+
reloadAck = await rpcManager.reloadTestFile(
|
|
2892
|
+
file.testPath,
|
|
2893
|
+
testNamePattern,
|
|
2894
|
+
);
|
|
2895
|
+
const completionPromise = registerPendingHeadedReload(
|
|
2896
|
+
file.testPath,
|
|
2897
|
+
reloadAck.runId,
|
|
2898
|
+
);
|
|
2724
2899
|
await Promise.race([
|
|
2725
|
-
|
|
2900
|
+
completionPromise,
|
|
2726
2901
|
new Promise<never>((_, reject) => {
|
|
2727
2902
|
timeoutId = setTimeout(() => {
|
|
2728
2903
|
reject(
|
|
@@ -2733,6 +2908,15 @@ export const runBrowserController = async (
|
|
|
2733
2908
|
}, timeoutMs);
|
|
2734
2909
|
}),
|
|
2735
2910
|
]);
|
|
2911
|
+
} catch (error) {
|
|
2912
|
+
if (reloadAck?.runId) {
|
|
2913
|
+
rejectPendingHeadedReload(
|
|
2914
|
+
file.testPath,
|
|
2915
|
+
toError(error),
|
|
2916
|
+
reloadAck.runId,
|
|
2917
|
+
);
|
|
2918
|
+
}
|
|
2919
|
+
throw error;
|
|
2736
2920
|
} finally {
|
|
2737
2921
|
if (timeoutId) {
|
|
2738
2922
|
clearTimeout(timeoutId);
|
|
@@ -2765,13 +2949,26 @@ export const runBrowserController = async (
|
|
|
2765
2949
|
async onTestCaseResult(payload: TestResult) {
|
|
2766
2950
|
await handleTestCaseResult(payload);
|
|
2767
2951
|
},
|
|
2768
|
-
async onTestFileComplete(payload:
|
|
2769
|
-
|
|
2952
|
+
async onTestFileComplete(payload: HeadedTestFileCompletePayload) {
|
|
2953
|
+
try {
|
|
2954
|
+
await handleTestFileComplete(payload);
|
|
2955
|
+
resolvePendingHeadedReload(payload.testPath, payload.runId);
|
|
2956
|
+
} catch (error) {
|
|
2957
|
+
rejectPendingHeadedReload(
|
|
2958
|
+
payload.testPath,
|
|
2959
|
+
toError(error),
|
|
2960
|
+
payload.runId,
|
|
2961
|
+
);
|
|
2962
|
+
throw error;
|
|
2963
|
+
}
|
|
2770
2964
|
},
|
|
2771
2965
|
async onLog(payload: LogPayload) {
|
|
2772
2966
|
await handleLog(payload);
|
|
2773
2967
|
},
|
|
2774
2968
|
async onFatal(payload: FatalPayload) {
|
|
2969
|
+
const error = new Error(payload.message);
|
|
2970
|
+
error.stack = payload.stack;
|
|
2971
|
+
rejectAllPendingHeadedReloads(error);
|
|
2775
2972
|
await handleFatal(payload);
|
|
2776
2973
|
},
|
|
2777
2974
|
async dispatch(request: BrowserDispatchRequest) {
|
|
@@ -2786,14 +2983,18 @@ export const runBrowserController = async (
|
|
|
2786
2983
|
if (isWatchMode && runtime.rpcManager) {
|
|
2787
2984
|
rpcManager = runtime.rpcManager;
|
|
2788
2985
|
// Update methods with new test state (caseResults, completedTests, etc.)
|
|
2789
|
-
rpcManager.updateMethods(createRpcMethods());
|
|
2986
|
+
rpcManager.updateMethods(createRpcMethods(), rejectAllPendingHeadedReloads);
|
|
2790
2987
|
// Reattach if we have an existing WebSocket
|
|
2791
2988
|
const existingWs = rpcManager.currentWebSocket;
|
|
2792
2989
|
if (existingWs) {
|
|
2793
2990
|
rpcManager.reattach(existingWs);
|
|
2794
2991
|
}
|
|
2795
2992
|
} else {
|
|
2796
|
-
rpcManager = new ContainerRpcManager(
|
|
2993
|
+
rpcManager = new ContainerRpcManager(
|
|
2994
|
+
wss,
|
|
2995
|
+
createRpcMethods(),
|
|
2996
|
+
rejectAllPendingHeadedReloads,
|
|
2997
|
+
);
|
|
2797
2998
|
|
|
2798
2999
|
if (isWatchMode) {
|
|
2799
3000
|
runtime.rpcManager = rpcManager;
|
|
@@ -3066,7 +3267,7 @@ export const listBrowserTests = async (
|
|
|
3066
3267
|
throw error;
|
|
3067
3268
|
}
|
|
3068
3269
|
|
|
3069
|
-
const { browser, port } = runtime;
|
|
3270
|
+
const { browser, browserLaunchOptions, port } = runtime;
|
|
3070
3271
|
|
|
3071
3272
|
// Get browser projects for runtime config
|
|
3072
3273
|
// Normalize projectRoot to posix format for cross-platform compatibility
|
|
@@ -3110,7 +3311,10 @@ export const listBrowserTests = async (
|
|
|
3110
3311
|
});
|
|
3111
3312
|
|
|
3112
3313
|
// Create a headless page to run collection
|
|
3113
|
-
const browserContext = await browser.newContext({
|
|
3314
|
+
const browserContext = await browser.newContext({
|
|
3315
|
+
providerOptions: browserLaunchOptions.providerOptions,
|
|
3316
|
+
viewport: null,
|
|
3317
|
+
});
|
|
3114
3318
|
const page = await browserContext.newPage();
|
|
3115
3319
|
|
|
3116
3320
|
// Expose dispatch function for browser client to send messages
|
package/src/index.ts
CHANGED
package/src/providers/index.ts
CHANGED
|
@@ -6,6 +6,14 @@ import { playwrightProviderImplementation } from './playwright';
|
|
|
6
6
|
*
|
|
7
7
|
* When adding a new built-in provider, implement `BrowserProviderImplementation`
|
|
8
8
|
* and register it in `providerImplementations` below.
|
|
9
|
+
*
|
|
10
|
+
* Provider-agnostic policy:
|
|
11
|
+
* - keep shared contracts and behavior provider-neutral
|
|
12
|
+
* - `browser.providerOptions` stays opaque at the framework boundary
|
|
13
|
+
* - do not export provider-owned config types from `@rstest/browser`
|
|
14
|
+
* - do not reference optional peer provider types from public declarations
|
|
15
|
+
* - keep provider-specific behavior and config decoding inside provider implementations
|
|
16
|
+
* - prefer direct passthrough to provider APIs over provider-specific translation
|
|
9
17
|
*/
|
|
10
18
|
export type BrowserProvider = 'playwright';
|
|
11
19
|
|
|
@@ -50,6 +58,7 @@ export type BrowserProviderBrowser = {
|
|
|
50
58
|
close: () => Promise<void>;
|
|
51
59
|
newContext: (options: {
|
|
52
60
|
viewport: { width: number; height: number } | null;
|
|
61
|
+
providerOptions?: Record<string, unknown>;
|
|
53
62
|
}) => Promise<BrowserProviderContext>;
|
|
54
63
|
};
|
|
55
64
|
|
|
@@ -62,6 +71,7 @@ export type BrowserProviderRuntime = {
|
|
|
62
71
|
export type LaunchBrowserInput = {
|
|
63
72
|
browserName: 'chromium' | 'firefox' | 'webkit';
|
|
64
73
|
headless: boolean | undefined;
|
|
74
|
+
providerOptions: Record<string, unknown>;
|
|
65
75
|
};
|
|
66
76
|
|
|
67
77
|
/** Input contract for provider-side browser RPC dispatch. */
|
|
@@ -11,10 +11,12 @@ export const playwrightProviderImplementation: BrowserProviderImplementation = {
|
|
|
11
11
|
async launchRuntime({
|
|
12
12
|
browserName,
|
|
13
13
|
headless,
|
|
14
|
+
providerOptions,
|
|
14
15
|
}): Promise<BrowserProviderRuntime> {
|
|
15
16
|
return launchPlaywrightBrowser({
|
|
16
17
|
browserName,
|
|
17
18
|
headless,
|
|
19
|
+
providerOptions,
|
|
18
20
|
});
|
|
19
21
|
},
|
|
20
22
|
async dispatchRpc({
|
|
@@ -1,32 +1,54 @@
|
|
|
1
|
-
import type { BrowserProviderRuntime } from '../index';
|
|
2
|
-
|
|
3
|
-
type PlaywrightModule = typeof import('playwright');
|
|
4
|
-
type PlaywrightBrowserType = PlaywrightModule['chromium'];
|
|
1
|
+
import type { BrowserProviderContext, BrowserProviderRuntime } from '../index';
|
|
5
2
|
|
|
6
3
|
export async function launchPlaywrightBrowser({
|
|
7
4
|
browserName,
|
|
8
5
|
headless,
|
|
6
|
+
providerOptions,
|
|
9
7
|
}: {
|
|
10
8
|
browserName: 'chromium' | 'firefox' | 'webkit';
|
|
11
9
|
headless: boolean | undefined;
|
|
10
|
+
providerOptions: Record<string, unknown>;
|
|
12
11
|
}): Promise<BrowserProviderRuntime> {
|
|
13
12
|
const playwright = await import('playwright');
|
|
14
|
-
const browserType = playwright[browserName]
|
|
13
|
+
const browserType = playwright[browserName];
|
|
14
|
+
const launchOptions = providerOptions.launch as
|
|
15
|
+
| Record<string, unknown>
|
|
16
|
+
| undefined;
|
|
17
|
+
const launchArgs = Array.isArray(launchOptions?.args)
|
|
18
|
+
? launchOptions.args
|
|
19
|
+
: browserName === 'chromium'
|
|
20
|
+
? [
|
|
21
|
+
'--disable-popup-blocking',
|
|
22
|
+
'--no-first-run',
|
|
23
|
+
'--no-default-browser-check',
|
|
24
|
+
]
|
|
25
|
+
: undefined;
|
|
15
26
|
|
|
16
27
|
const browser = await browserType.launch({
|
|
28
|
+
...launchOptions,
|
|
17
29
|
headless,
|
|
18
|
-
|
|
19
|
-
args:
|
|
20
|
-
browserName === 'chromium'
|
|
21
|
-
? [
|
|
22
|
-
'--disable-popup-blocking',
|
|
23
|
-
'--no-first-run',
|
|
24
|
-
'--no-default-browser-check',
|
|
25
|
-
]
|
|
26
|
-
: undefined,
|
|
30
|
+
args: launchArgs,
|
|
27
31
|
});
|
|
28
32
|
|
|
33
|
+
const wrappedBrowser: BrowserProviderRuntime['browser'] = {
|
|
34
|
+
close: async () => browser.close(),
|
|
35
|
+
newContext: async ({
|
|
36
|
+
providerOptions: contextProviderOptions,
|
|
37
|
+
viewport,
|
|
38
|
+
}) => {
|
|
39
|
+
const contextOptions = contextProviderOptions?.context as
|
|
40
|
+
| Record<string, unknown>
|
|
41
|
+
| undefined;
|
|
42
|
+
const context = await browser.newContext({
|
|
43
|
+
...contextOptions,
|
|
44
|
+
viewport,
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
return context as unknown as BrowserProviderContext;
|
|
48
|
+
},
|
|
49
|
+
};
|
|
50
|
+
|
|
29
51
|
return {
|
|
30
|
-
browser:
|
|
52
|
+
browser: wrappedBrowser,
|
|
31
53
|
};
|
|
32
54
|
}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
/*! LICENSE: 101.82cdbbe145.js.LICENSE.txt */
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
/*! LICENSE: lib-react.ce60b6aea5.js.LICENSE.txt */
|