@rstest/browser 0.11.6 → 0.11.7

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 (88) hide show
  1. package/dist/browser-container/container-static/css/index.43daaf6c93.css +1 -0
  2. package/dist/browser-container/container-static/js/260.3739fe815c.js +194 -0
  3. package/dist/browser-container/container-static/js/index.c0f87bfb3e.js +1 -0
  4. package/dist/browser-container/index.html +1 -1
  5. package/dist/browserRsbuild.d.ts +24 -4
  6. package/dist/browserV8Coverage.d.ts +15 -0
  7. package/dist/client/240~1.js +150 -0
  8. package/dist/{augmentExpect.d.ts → client/client/augmentExpect.d.ts} +1 -1
  9. package/dist/client/{browserRpc.d.ts → client/browserRpc.d.ts} +1 -1
  10. package/dist/client/{dispatchTransport.d.ts → client/dispatchTransport.d.ts} +8 -4
  11. package/dist/client/client/index.d.ts +2 -0
  12. package/dist/client/client/runIdentity.d.ts +12 -0
  13. package/dist/client/client/runner.d.ts +4 -0
  14. package/dist/client/client/snapshot.d.ts +27 -0
  15. package/dist/client/client/sourceMapSupport.d.ts +36 -0
  16. package/dist/{browser.js → client/index.js} +3 -102
  17. package/dist/client/protocol.d.ts +225 -0
  18. package/dist/client/rpcProtocol.d.ts +138 -0
  19. package/dist/client/runner.js +675 -0
  20. package/dist/client/sourceMap/sourceMapLoader.d.ts +25 -0
  21. package/dist/containerRpc.d.ts +27 -19
  22. package/dist/headedRunRegistry.d.ts +51 -0
  23. package/dist/headedScheduler.d.ts +10 -10
  24. package/dist/headlessScheduler.d.ts +5 -4
  25. package/dist/headlessTransport.d.ts +4 -2
  26. package/dist/hostPayloads.d.ts +1 -7
  27. package/dist/index.js +840 -330
  28. package/dist/protocol.d.ts +48 -8
  29. package/dist/providers/index.d.ts +21 -0
  30. package/dist/providers/playwright/v8Coverage.d.ts +2 -0
  31. package/dist/rpcProtocol.d.ts +1 -2
  32. package/dist/schedulerSeam.d.ts +5 -0
  33. package/dist/sourceMap/sourceMapLoader.d.ts +12 -1
  34. package/dist/watchRerunPlanner.d.ts +20 -9
  35. package/dist/watchSignals.d.ts +8 -1
  36. package/package.json +10 -10
  37. package/dist/33.js +0 -7
  38. package/dist/browser-container/container-static/css/index.0521ff95b1.css +0 -1
  39. package/dist/browser-container/container-static/js/683.4f821ce8f1.js +0 -194
  40. package/dist/browser-container/container-static/js/index.568aa7cb35.js +0 -1
  41. package/dist/browser.d.ts +0 -2
  42. package/src/augmentExpect.ts +0 -62
  43. package/src/browser.ts +0 -3
  44. package/src/browserExecutor.ts +0 -222
  45. package/src/browserRpcRegistry.ts +0 -57
  46. package/src/browserRsbuild.ts +0 -1927
  47. package/src/client/api.ts +0 -213
  48. package/src/client/browserRpc.ts +0 -86
  49. package/src/client/dispatchTransport.ts +0 -240
  50. package/src/client/entry.ts +0 -807
  51. package/src/client/locator.ts +0 -448
  52. package/src/client/snapshot.ts +0 -113
  53. package/src/client/sourceMapSupport.ts +0 -159
  54. package/src/concurrency.ts +0 -65
  55. package/src/configValidation.ts +0 -263
  56. package/src/containerRpc.ts +0 -206
  57. package/src/dispatchCapabilities.ts +0 -177
  58. package/src/dispatchRouter.ts +0 -82
  59. package/src/env.d.ts +0 -54
  60. package/src/headedScheduler.ts +0 -664
  61. package/src/headedSerialTaskQueue.ts +0 -23
  62. package/src/headlessScheduler.ts +0 -566
  63. package/src/headlessTransport.ts +0 -28
  64. package/src/hostController.ts +0 -1365
  65. package/src/hostPayloads.ts +0 -62
  66. package/src/index.ts +0 -51
  67. package/src/manifest.d.ts +0 -41
  68. package/src/protocol.ts +0 -225
  69. package/src/providers/index.ts +0 -126
  70. package/src/providers/playwright/compileLocator.ts +0 -130
  71. package/src/providers/playwright/dispatchBrowserRpc.ts +0 -372
  72. package/src/providers/playwright/expectUtils.ts +0 -57
  73. package/src/providers/playwright/implementation.ts +0 -35
  74. package/src/providers/playwright/index.ts +0 -1
  75. package/src/providers/playwright/runtime.ts +0 -159
  76. package/src/providers/playwright/textMatcher.ts +0 -10
  77. package/src/rpcProtocol.ts +0 -218
  78. package/src/runSession.ts +0 -110
  79. package/src/schedulerSeam.ts +0 -49
  80. package/src/sessionRegistry.ts +0 -89
  81. package/src/sourceMap/sourceMapLoader.ts +0 -96
  82. package/src/viewportPresets.ts +0 -69
  83. package/src/watchRerunPlanner.ts +0 -88
  84. package/src/watchRuntime.ts +0 -83
  85. package/src/watchSignals.ts +0 -93
  86. /package/dist/{rslib-runtime.js → 612~0.js} +0 -0
  87. /package/dist/client/{api.d.ts → client/api.d.ts} +0 -0
  88. /package/dist/client/{locator.d.ts → client/locator.d.ts} +0 -0
@@ -1,664 +0,0 @@
1
- import type {
2
- RstestContext,
3
- TestFileResult,
4
- TestResult,
5
- } from '@rstest/core/internal/browser';
6
- import { color, logger } from '@rstest/core/internal/browser';
7
- import { normalize, relative } from 'pathe';
8
- import {
9
- type BrowserRuntime,
10
- drainPendingAffectedTestFiles,
11
- } from './browserRsbuild';
12
- import { ContainerRpcManager, type HostRpcMethods } from './containerRpc';
13
- import type { HostDispatchRouter } from './dispatchRouter';
14
- import { createHeadedSerialTaskQueue } from './headedSerialTaskQueue';
15
- import {
16
- createDeferredPromise,
17
- type DeferredPromise,
18
- type FatalPayload,
19
- type HeadedTestFileCompletePayload,
20
- type LogPayload,
21
- type ReloadTestFileAck,
22
- type TestFileStartPayload,
23
- toError,
24
- } from './hostPayloads';
25
- import type {
26
- BrowserDispatchRequest,
27
- BrowserHostConfig,
28
- TestFileInfo,
29
- } from './protocol';
30
- import type { BrowserProviderContext, BrowserProviderPage } from './providers';
31
- import { collectDeletedTestPaths, planWatchRerun } from './watchRerunPlanner';
32
- import type {
33
- BrowserWatchSession,
34
- DispatchPageResolver,
35
- SchedulerRunResult,
36
- } from './schedulerSeam';
37
- import type { WatchSignals } from './watchSignals';
38
-
39
- type HeadedSchedulerContext = Pick<
40
- RstestContext,
41
- 'rootPath' | 'snapshotManager' | 'updateReporterResultState'
42
- > & {
43
- normalizedConfig: Pick<RstestContext['normalizedConfig'], 'name'>;
44
- };
45
-
46
- type HeadedSchedulerDeps = {
47
- context: HeadedSchedulerContext;
48
- runtime: BrowserRuntime;
49
- allTestFiles: TestFileInfo[];
50
- hostOptions: BrowserHostConfig;
51
- isWatchMode: boolean;
52
- createDispatchRouter: () => HostDispatchRouter;
53
- handlers: {
54
- handleTestFileStart: (payload: TestFileStartPayload) => Promise<void>;
55
- handleTestCaseResult: (payload: TestResult) => Promise<void>;
56
- handleTestFileComplete: (payload: TestFileResult) => Promise<void>;
57
- handleLog: (payload: LogPayload) => Promise<void>;
58
- handleFatal: (payload: FatalPayload) => Promise<void>;
59
- };
60
- fatalErrorRef: { current: Error | null };
61
- watchSignals: Pick<
62
- WatchSignals,
63
- 'setDispatchRerun' | 'signalInvalidation' | 'awaitSignalledCycle'
64
- >;
65
- setDispatchPageResolver: (resolver: DispatchPageResolver) => void;
66
- createWatchSession: (
67
- execute: (testPaths: string[]) => Promise<void>,
68
- ) => BrowserWatchSession;
69
- collectProjectEntries: () => Promise<
70
- Parameters<typeof planWatchRerun>[0]['projectEntries']
71
- >;
72
- logWatchReady: () => void;
73
- destroyRuntime: () => Promise<void>;
74
- };
75
-
76
- /**
77
- * A headed cycle's work list: the files of its scope that still exist, each
78
- * paired with the test-name pattern whichever trigger put it in scope asked for.
79
- *
80
- * Both halves are resolved in one pass, synchronously, at the top of the cycle.
81
- * A queued scope can go stale before its cycle is dequeued — a later trigger may
82
- * have rebuilt the file set without one of these files — and it is skipped the
83
- * way the headless twin skips it; throwing would abandon the still-valid files
84
- * beside it and fail the run. The patterns are claimed here, and only for the
85
- * paths in this scope, so a click landing once the cycle is under way keeps its
86
- * pattern for the cycle it signalled instead of losing it to this one mid-loop.
87
- *
88
- * A skipped path keeps its pattern too, for the same reason: consuming it on the
89
- * way past would leave the next cycle that does run the file — the file set can
90
- * be rebuilt back — running it unfiltered, so the user's click would silently
91
- * become a full-file rerun. The cost is one map entry per path that never comes
92
- * back, which the next launch drops with the map.
93
- */
94
- export const claimHeadedCycleScope = (
95
- testPaths: string[],
96
- currentTestFiles: TestFileInfo[],
97
- pendingTestNamePatterns: Map<string, string>,
98
- ): { file: TestFileInfo; testNamePattern?: string }[] => {
99
- const scope: { file: TestFileInfo; testNamePattern?: string }[] = [];
100
- const filesByPath = new Map(
101
- currentTestFiles.map((file) => [file.testPath, file]),
102
- );
103
- for (const testPath of testPaths) {
104
- const normalizedTestPath = normalize(testPath);
105
- const file = filesByPath.get(normalizedTestPath);
106
- if (file) {
107
- scope.push({
108
- file,
109
- testNamePattern: pendingTestNamePatterns.get(normalizedTestPath),
110
- });
111
- pendingTestNamePatterns.delete(normalizedTestPath);
112
- }
113
- }
114
- return scope;
115
- };
116
-
117
- export const createHeadedScheduler = async ({
118
- context,
119
- runtime,
120
- allTestFiles,
121
- hostOptions,
122
- isWatchMode,
123
- createDispatchRouter,
124
- handlers: {
125
- handleTestFileStart,
126
- handleTestCaseResult,
127
- handleTestFileComplete,
128
- handleLog,
129
- handleFatal,
130
- },
131
- fatalErrorRef,
132
- watchSignals,
133
- setDispatchPageResolver,
134
- createWatchSession,
135
- collectProjectEntries,
136
- logWatchReady,
137
- destroyRuntime,
138
- }: HeadedSchedulerDeps): Promise<SchedulerRunResult> => {
139
- const { browser, browserLaunchOptions, watchState, wss } = runtime;
140
- let currentTestFiles = allTestFiles;
141
- // Coincidentally equal to the runner-side CONFIG_WAIT_TIMEOUT_MS and
142
- // DEFAULT_RPC_TIMEOUT_MS (client/entry.ts, client/dispatchTransport.ts) but
143
- // semantically distinct and in a different runtime, so deliberately NOT shared
144
- // with them. Invariant worth preserving: a runner must be able to receive its
145
- // config (config-wait) before the host declares its frames un-ready, i.e.
146
- // CONFIG_WAIT_TIMEOUT_MS <= RUNNER_FRAMES_READY_TIMEOUT_MS.
147
- const RUNNER_FRAMES_READY_TIMEOUT_MS = 30_000;
148
- let currentRunnerFramesSignature: string | null = null;
149
- const runnerFramesWaiters = new Map<string, Set<() => void>>();
150
-
151
- const createTestFilesSignature = (testFiles: readonly string[]): string => {
152
- return JSON.stringify(testFiles.map((testFile) => normalize(testFile)));
153
- };
154
-
155
- const markRunnerFramesReady = (testFiles: string[]): void => {
156
- const signature = createTestFilesSignature(testFiles);
157
- currentRunnerFramesSignature = signature;
158
- const waiters = runnerFramesWaiters.get(signature);
159
- if (!waiters) {
160
- return;
161
- }
162
- runnerFramesWaiters.delete(signature);
163
- for (const waiter of waiters) {
164
- waiter();
165
- }
166
- };
167
-
168
- const waitForRunnerFramesReady = async (
169
- testFiles: readonly string[],
170
- ): Promise<void> => {
171
- const signature = createTestFilesSignature(testFiles);
172
- if (currentRunnerFramesSignature === signature) {
173
- return;
174
- }
175
-
176
- await new Promise<void>((resolve, reject) => {
177
- const waiters =
178
- runnerFramesWaiters.get(signature) ?? new Set<() => void>();
179
-
180
- const cleanup = () => {
181
- const currentWaiters = runnerFramesWaiters.get(signature);
182
- if (!currentWaiters) {
183
- return;
184
- }
185
- currentWaiters.delete(onReady);
186
- if (currentWaiters.size === 0) {
187
- runnerFramesWaiters.delete(signature);
188
- }
189
- };
190
-
191
- const onReady = () => {
192
- if (timeoutId) {
193
- clearTimeout(timeoutId);
194
- }
195
- cleanup();
196
- resolve();
197
- };
198
-
199
- const timeoutId = setTimeout(() => {
200
- cleanup();
201
- reject(
202
- new Error(
203
- `Timed out waiting for headed runner frames to be ready for ${testFiles.length} file(s).`,
204
- ),
205
- );
206
- }, RUNNER_FRAMES_READY_TIMEOUT_MS);
207
-
208
- waiters.add(onReady);
209
- runnerFramesWaiters.set(signature, waiters);
210
-
211
- if (currentRunnerFramesSignature === signature) {
212
- onReady();
213
- }
214
- });
215
- };
216
-
217
- const getTestFileInfo = (testFile: string): TestFileInfo => {
218
- const normalizedTestFile = normalize(testFile);
219
- const fileInfo = currentTestFiles.find(
220
- (file) => file.testPath === normalizedTestFile,
221
- );
222
- if (!fileInfo) {
223
- throw new Error(`Unknown browser test file: ${JSON.stringify(testFile)}`);
224
- }
225
- return fileInfo;
226
- };
227
-
228
- // Open a container page for user to view (reuse in watch mode)
229
- let containerContext: BrowserProviderContext;
230
- let containerPage: BrowserProviderPage;
231
- let isNewPage = false;
232
-
233
- if (isWatchMode && runtime.containerPage && runtime.containerContext) {
234
- containerContext = runtime.containerContext;
235
- containerPage = runtime.containerPage;
236
- logger.log(color.gray('\n[Watch] Reusing existing container page\n'));
237
- } else {
238
- isNewPage = true;
239
- containerContext = await browser.newContext({
240
- providerOptions: browserLaunchOptions.providerOptions,
241
- viewport: null,
242
- });
243
- containerPage = await containerContext.newPage();
244
-
245
- // Prevent popup windows from being created
246
- containerPage.on('popup', async (popup: BrowserProviderPage) => {
247
- await popup.close().catch(() => {});
248
- });
249
-
250
- containerContext.on('page', async (page: BrowserProviderPage) => {
251
- if (page !== containerPage) {
252
- await page.close().catch(() => {});
253
- }
254
- });
255
-
256
- if (isWatchMode) {
257
- runtime.containerPage = containerPage;
258
- runtime.containerContext = containerContext;
259
- }
260
-
261
- // Forward browser console to terminal
262
- containerPage.on('console', (msg) => {
263
- const text = msg.text();
264
- if (text.startsWith('[Container]') || text.startsWith('[Runner]')) {
265
- logger.log(color.gray(`[Browser Console] ${text}`));
266
- }
267
- });
268
- }
269
-
270
- setDispatchPageResolver(() => ({ containerPage }));
271
-
272
- const dispatchRouter = createDispatchRouter();
273
- const headedReloadQueue = createHeadedSerialTaskQueue();
274
- const pendingHeadedReloads = new Map<
275
- string,
276
- {
277
- runId: string;
278
- deferred: DeferredPromise<void>;
279
- }
280
- >();
281
- let enqueueHeadedReload = async (
282
- _file: TestFileInfo,
283
- _testNamePattern?: string,
284
- ): Promise<void> => {
285
- throw new Error('Headed reload queue is not initialized');
286
- };
287
-
288
- const rejectPendingHeadedReload = (
289
- testPath: string,
290
- error: Error,
291
- runId?: string,
292
- ): void => {
293
- const pending = pendingHeadedReloads.get(testPath);
294
- if (!pending) {
295
- return;
296
- }
297
- if (runId && pending.runId !== runId) {
298
- return;
299
- }
300
- pendingHeadedReloads.delete(testPath);
301
- pending.deferred.reject(error);
302
- };
303
-
304
- const rejectAllPendingHeadedReloads = (error: Error): void => {
305
- for (const [testPath, pending] of pendingHeadedReloads) {
306
- pendingHeadedReloads.delete(testPath);
307
- pending.deferred.reject(error);
308
- }
309
- };
310
-
311
- const registerPendingHeadedReload = (
312
- testPath: string,
313
- runId: string,
314
- ): Promise<void> => {
315
- const previousPending = pendingHeadedReloads.get(testPath);
316
- if (previousPending) {
317
- previousPending.deferred.reject(
318
- new Error(
319
- `Reload for "${testPath}" was superseded by a newer request.`,
320
- ),
321
- );
322
- pendingHeadedReloads.delete(testPath);
323
- }
324
-
325
- const deferred = createDeferredPromise<void>();
326
- pendingHeadedReloads.set(testPath, {
327
- runId,
328
- deferred,
329
- });
330
-
331
- return deferred.promise;
332
- };
333
-
334
- const resolvePendingHeadedReload = (
335
- testPath: string,
336
- runId?: string,
337
- ): void => {
338
- const pending = pendingHeadedReloads.get(testPath);
339
- if (!pending) {
340
- return;
341
- }
342
- if (runId && pending.runId !== runId) {
343
- logger.debug(
344
- `[Browser UI] Ignoring stale file-complete for ${testPath}. current=${pending.runId}, incoming=${runId}`,
345
- );
346
- return;
347
- }
348
- pendingHeadedReloads.delete(testPath);
349
- pending.deferred.resolve();
350
- };
351
-
352
- // No execution-duration watchdog: per-test/hook timeouts are enforced inside
353
- // the runner, and a dead container is caught event-driven by the WebSocket
354
- // `close` handler, which rejects every pending reload via `onDisconnect`.
355
- const reloadTestFileAndWait = async (
356
- file: TestFileInfo,
357
- testNamePattern?: string,
358
- ): Promise<void> => {
359
- let reloadAck: ReloadTestFileAck | undefined;
360
-
361
- try {
362
- reloadAck = await rpcManager.reloadTestFile(
363
- file.testPath,
364
- testNamePattern,
365
- );
366
- await registerPendingHeadedReload(file.testPath, reloadAck.runId);
367
- } catch (error) {
368
- if (reloadAck?.runId) {
369
- rejectPendingHeadedReload(
370
- file.testPath,
371
- toError(error),
372
- reloadAck.runId,
373
- );
374
- }
375
- throw error;
376
- }
377
- };
378
-
379
- // The in-page rerun button is a watch trigger like any other, so once the
380
- // watch session exists it routes through core's cycle instead of reloading
381
- // the frame behind core's back. Until then (during the initial cycle) the
382
- // direct reload is all there is.
383
- let runUiRequestedRerun = async (
384
- file: TestFileInfo,
385
- testNamePattern?: string,
386
- ): Promise<void> => {
387
- await enqueueHeadedReload(file, testNamePattern);
388
- };
389
-
390
- // Create RPC methods that can access test state variables
391
- const createRpcMethods = (): HostRpcMethods => ({
392
- async rerunTest(testFile: string, testNamePattern?: string) {
393
- const projectName = context.normalizedConfig.name || 'project';
394
- const relativePath = relative(context.rootPath, testFile);
395
- const displayPath = `<${projectName}>/${relativePath}`;
396
- logger.log(
397
- color.cyan(
398
- `\nRe-running test: ${displayPath}${testNamePattern ? ` (pattern: ${testNamePattern})` : ''}\n`,
399
- ),
400
- );
401
- await runUiRequestedRerun(getTestFileInfo(testFile), testNamePattern);
402
- },
403
- async getTestFiles() {
404
- return currentTestFiles;
405
- },
406
- async onRunnerFramesReady(testFiles: string[]) {
407
- markRunnerFramesReady(testFiles);
408
- },
409
- async onTestFileStart(payload: TestFileStartPayload) {
410
- await handleTestFileStart(payload);
411
- },
412
- async onTestCaseResult(payload: TestResult) {
413
- await handleTestCaseResult(payload);
414
- },
415
- async onTestFileComplete(payload: HeadedTestFileCompletePayload) {
416
- try {
417
- await handleTestFileComplete(payload);
418
- resolvePendingHeadedReload(payload.testPath, payload.runId);
419
- } catch (error) {
420
- rejectPendingHeadedReload(
421
- payload.testPath,
422
- toError(error),
423
- payload.runId,
424
- );
425
- throw error;
426
- }
427
- },
428
- async onLog(payload: LogPayload) {
429
- await handleLog(payload);
430
- },
431
- async onFatal(payload: FatalPayload) {
432
- const error = new Error(payload.message);
433
- error.stack = payload.stack;
434
- rejectAllPendingHeadedReloads(error);
435
- await handleFatal(payload);
436
- },
437
- async dispatch(request: BrowserDispatchRequest) {
438
- // Headed/container path now shares the same dispatch contract as headless.
439
- return dispatchRouter.dispatch(request);
440
- },
441
- });
442
-
443
- // Setup RPC manager
444
- let rpcManager: ContainerRpcManager;
445
-
446
- if (isWatchMode && runtime.rpcManager) {
447
- rpcManager = runtime.rpcManager;
448
- // Update methods with new test state (caseResults, completedTests, etc.)
449
- rpcManager.updateMethods(createRpcMethods(), rejectAllPendingHeadedReloads);
450
- // Reattach if we have an existing WebSocket
451
- const existingWs = rpcManager.currentWebSocket;
452
- if (existingWs) {
453
- rpcManager.reattach(existingWs);
454
- }
455
- } else {
456
- rpcManager = new ContainerRpcManager(
457
- wss,
458
- createRpcMethods(),
459
- rejectAllPendingHeadedReloads,
460
- );
461
-
462
- if (isWatchMode) {
463
- runtime.rpcManager = rpcManager;
464
- }
465
- }
466
-
467
- // Only navigate on first creation
468
- if (isNewPage) {
469
- const pagePath = '/';
470
- const containerPort = runtime.containerServer.port;
471
- await containerPage.goto(`http://localhost:${containerPort}${pagePath}`, {
472
- waitUntil: 'load',
473
- });
474
-
475
- logger.log(
476
- color.cyan(
477
- `\nBrowser mode opened at http://localhost:${containerPort}${pagePath}\n`,
478
- ),
479
- );
480
- }
481
-
482
- enqueueHeadedReload = async (
483
- file: TestFileInfo,
484
- testNamePattern?: string,
485
- ): Promise<void> => {
486
- return headedReloadQueue.enqueue(async () => {
487
- if (fatalErrorRef.current) {
488
- return;
489
- }
490
- await reloadTestFileAndWait(file, testNamePattern);
491
- });
492
- };
493
-
494
- let testTime = 0;
495
- if (currentTestFiles.length > 0) {
496
- const testStart = Date.now();
497
- try {
498
- await waitForRunnerFramesReady(
499
- currentTestFiles.map((file) => file.testPath),
500
- );
501
-
502
- for (const file of currentTestFiles) {
503
- await enqueueHeadedReload(file);
504
- if (fatalErrorRef.current) {
505
- break;
506
- }
507
- }
508
- } catch (error) {
509
- // The fatal error rides the returned result into the cycle outcome, and
510
- // core's `finalizeRunCycle` raises the exit code from it.
511
- fatalErrorRef.current = fatalErrorRef.current ?? toError(error);
512
- }
513
-
514
- testTime = Date.now() - testStart;
515
- }
516
-
517
- let watchSession: BrowserWatchSession | undefined;
518
- if (isWatchMode) {
519
- // Set by the in-page rerun trigger and consumed by the cycle that reloads
520
- // that file — the pattern is a headed-UI concept core's cycle options
521
- // cannot carry, so it travels beside the scope rather than inside it.
522
- // Keyed by test path rather than held in one slot: core queues cycles, so
523
- // an unrelated trigger can be dequeued between the click and its own cycle,
524
- // and a single slot would hand the pattern to whichever cycle ran first.
525
- // An entry is written with its own signal and read at a cycle's first
526
- // synchronous step, so the cycle that takes it is the one that signal
527
- // started — or, when the file was already in a queued scope, the one it
528
- // folded into, which is the cycle that runs the file. That holds as long as
529
- // nothing yields between the two: core closes the fold window and then
530
- // awaits `notifyReportersOnTestRunStart` before this cycle claims, so a user
531
- // reporter with an async `onTestRunStart` hook is the one thing that can
532
- // stretch the gap wide enough for another signal to land in it. A tracked
533
- // gap, not a choice: a second click on the same file inside that window
534
- // overwrites the entry, so the earlier cycle claims the newer pattern and
535
- // the later one finds it gone and reloads the file unfiltered. Closing it
536
- // means the pattern crossing the seam inside the queued cycle's own
537
- // options instead of traveling beside the scope.
538
- const pendingTestNamePatterns = new Map<string, string>();
539
-
540
- const runScope = async (testPaths: string[]): Promise<void> => {
541
- // Claimed in this synchronous prefix, before `runCycle` suspends, so
542
- // nothing this cycle does can change what it runs or which patterns it
543
- // takes.
544
- const cycleScope = claimHeadedCycleScope(
545
- testPaths,
546
- currentTestFiles,
547
- pendingTestNamePatterns,
548
- );
549
- for (const { file, testNamePattern } of cycleScope) {
550
- await enqueueHeadedReload(file, testNamePattern);
551
- }
552
- };
553
-
554
- /**
555
- * Re-deliver the host config so runner iframes reloaded by the next cycle
556
- * observe live per-rerun values ('u' flips updateSnapshot between reruns);
557
- * `setContainerOptions` keeps full container reloads in sync.
558
- */
559
- const refreshHostConfig = async (): Promise<void> => {
560
- const refreshedHostOptions: BrowserHostConfig = {
561
- ...hostOptions,
562
- snapshot: {
563
- updateSnapshot: context.snapshotManager.options.updateSnapshot,
564
- },
565
- };
566
- runtime.setContainerOptions(refreshedHostOptions);
567
- await rpcManager.updateHostConfig(refreshedHostOptions);
568
- };
569
-
570
- watchSignals.setDispatchRerun(async () => {
571
- // Independent: config push to the container vs. local entry collection.
572
- const [, newProjectEntries] = await Promise.all([
573
- refreshHostConfig(),
574
- collectProjectEntries(),
575
- ]);
576
- const rerunPlan = planWatchRerun({
577
- projectEntries: newProjectEntries,
578
- previousTestFiles: watchState.lastTestFiles,
579
- affectedTestFiles: drainPendingAffectedTestFiles(watchState),
580
- });
581
-
582
- if (rerunPlan.filesChanged) {
583
- const deletedTestPaths = collectDeletedTestPaths(
584
- watchState.lastTestFiles,
585
- rerunPlan.currentTestFiles,
586
- );
587
- if (deletedTestPaths.length > 0) {
588
- context.updateReporterResultState([], [], deletedTestPaths);
589
- }
590
- watchState.lastTestFiles = rerunPlan.currentTestFiles;
591
- currentTestFiles = rerunPlan.currentTestFiles;
592
- await rpcManager.notifyTestFileUpdate(currentTestFiles);
593
- if (currentTestFiles.length === 0) {
594
- logger.log(
595
- color.cyan('No browser test files remain after update.\n'),
596
- );
597
- logWatchReady();
598
- return;
599
- }
600
- await waitForRunnerFramesReady(
601
- currentTestFiles.map((file) => file.testPath),
602
- );
603
- }
604
-
605
- if (rerunPlan.normalizedAffectedTestFiles.length > 0) {
606
- logger.log(
607
- color.cyan(
608
- `Re-running ${rerunPlan.normalizedAffectedTestFiles.length} affected test file(s)...\n`,
609
- ),
610
- );
611
- await watchSignals.signalInvalidation(
612
- rerunPlan.normalizedAffectedTestFiles,
613
- );
614
- return;
615
- }
616
-
617
- if (!rerunPlan.filesChanged) {
618
- logger.log(color.cyan('Tests will be re-executed automatically\n'));
619
- }
620
- logWatchReady();
621
- });
622
-
623
- runUiRequestedRerun = async (file, testNamePattern) => {
624
- await refreshHostConfig();
625
- await watchSignals.signalInvalidation([file.testPath], () => {
626
- if (testNamePattern === undefined) {
627
- pendingTestNamePatterns.delete(normalize(file.testPath));
628
- } else {
629
- pendingTestNamePatterns.set(
630
- normalize(file.testPath),
631
- testNamePattern,
632
- );
633
- }
634
- });
635
- await watchSignals.awaitSignalledCycle();
636
- };
637
-
638
- watchSession = createWatchSession(runScope);
639
- }
640
-
641
- const closeContainerRuntime = !isWatchMode
642
- ? async () => {
643
- try {
644
- await containerPage.close();
645
- } catch {
646
- // ignore
647
- }
648
- try {
649
- await containerContext.close();
650
- } catch {
651
- // ignore
652
- }
653
- await destroyRuntime();
654
- }
655
- : undefined;
656
-
657
- return {
658
- testTime,
659
- watchSession,
660
- // `closeContainerRuntime` is already `undefined` in watch mode: the watch
661
- // runtime outlives the cycle and is torn down through `executor.close()`.
662
- close: closeContainerRuntime,
663
- };
664
- };
@@ -1,23 +0,0 @@
1
- type HeadedSerialTask = () => Promise<void>;
2
-
3
- type HeadedSerialTaskQueue = {
4
- enqueue: (task: HeadedSerialTask) => Promise<void>;
5
- };
6
-
7
- /**
8
- * Serializes headed browser file execution so only one task runs at a time.
9
- * The queue keeps draining even if an earlier task rejects.
10
- */
11
- export const createHeadedSerialTaskQueue = (): HeadedSerialTaskQueue => {
12
- let queue: Promise<void> = Promise.resolve();
13
-
14
- const enqueue = (task: HeadedSerialTask): Promise<void> => {
15
- const next = queue.then(task);
16
- queue = next.catch(() => {});
17
- return next;
18
- };
19
-
20
- return {
21
- enqueue,
22
- };
23
- };