@rstest/browser 0.11.3 → 0.11.5
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/506.b2f920fa67.js +194 -0
- package/dist/browser-container/container-static/js/index.d27fd63f5e.js +1 -0
- package/dist/browser-container/container-static/js/lib-react.e24a1d366b.js +2 -0
- package/dist/browser-container/index.html +1 -1
- package/dist/index.js +290 -245
- package/dist/protocol.d.ts +5 -0
- package/package.json +8 -7
- package/src/browserExecutor.ts +5 -3
- package/src/client/entry.ts +3 -0
- package/src/configValidation.ts +6 -0
- package/src/hostController.ts +556 -286
- package/src/protocol.ts +6 -0
- package/dist/browser-container/container-static/js/949.7d935e2ac9.js +0 -27406
- package/dist/browser-container/container-static/js/949.7d935e2ac9.js.LICENSE.txt +0 -1
- package/dist/browser-container/container-static/js/index.492a6c2f8b.js +0 -3053
- package/dist/browser-container/container-static/js/lib-react.62b27a21db.js +0 -8454
- package/dist/browser-container/container-static/js/lib-react.62b27a21db.js.LICENSE.txt +0 -1
- package/dist/watchCliShortcuts.d.ts +0 -6
- package/src/AGENTS.md +0 -150
- package/src/client/AGENTS.md +0 -89
- package/src/watchCliShortcuts.ts +0 -77
package/src/hostController.ts
CHANGED
|
@@ -6,10 +6,17 @@ import { fileURLToPath } from 'node:url';
|
|
|
6
6
|
import { isDeepStrictEqual } from 'node:util';
|
|
7
7
|
import type { Rspack } from '@rstest/core';
|
|
8
8
|
import {
|
|
9
|
+
applyWatchInvalidation,
|
|
9
10
|
applyWebMockRspackConfig,
|
|
10
11
|
type BrowserTestRunOptions,
|
|
11
12
|
type BrowserTestRunResult,
|
|
13
|
+
type BrowserWatchHandles,
|
|
14
|
+
buildBrowserCoverageMap,
|
|
12
15
|
type CoverageMapData,
|
|
16
|
+
type EntryHashSnapshot,
|
|
17
|
+
type ExecutorCycleOutcome,
|
|
18
|
+
FATAL_SIGNALS,
|
|
19
|
+
finalizeRunCycle,
|
|
13
20
|
type ListBrowserTestsOptions,
|
|
14
21
|
color,
|
|
15
22
|
createCoverageProvider,
|
|
@@ -25,9 +32,11 @@ import {
|
|
|
25
32
|
importMetaRstestDefine,
|
|
26
33
|
initModifyRstestConfigHooks,
|
|
27
34
|
isDebug,
|
|
35
|
+
isTTY,
|
|
28
36
|
type ListCommandResult,
|
|
29
37
|
loadCoverageProvider,
|
|
30
38
|
logger,
|
|
39
|
+
logWatchReadyMessage,
|
|
31
40
|
pluginMockRuntime,
|
|
32
41
|
prepareWatchRerunState,
|
|
33
42
|
projectRuntimeConfig,
|
|
@@ -46,6 +55,7 @@ import {
|
|
|
46
55
|
type TestFileResult,
|
|
47
56
|
type TestResult,
|
|
48
57
|
type UserConsoleLog,
|
|
58
|
+
type WatchInvalidationState,
|
|
49
59
|
} from '@rstest/core/internal/browser';
|
|
50
60
|
import { type BirpcReturn, createBirpc } from 'birpc';
|
|
51
61
|
import openEditor from 'open-editor';
|
|
@@ -101,11 +111,6 @@ import {
|
|
|
101
111
|
type SourceMapPayload,
|
|
102
112
|
} from './sourceMap/sourceMapLoader';
|
|
103
113
|
import { resolveBrowserViewportPreset } from './viewportPresets';
|
|
104
|
-
import {
|
|
105
|
-
isBrowserWatchCliShortcutsEnabled,
|
|
106
|
-
logBrowserWatchReadyMessage,
|
|
107
|
-
setupBrowserWatchCliShortcuts,
|
|
108
|
-
} from './watchCliShortcuts';
|
|
109
114
|
import { collectWatchTestFiles, planWatchRerun } from './watchRerunPlanner';
|
|
110
115
|
|
|
111
116
|
const { createRsbuild, rspack } = rsbuild;
|
|
@@ -255,6 +260,12 @@ type ContainerRpcMethods = {
|
|
|
255
260
|
testFile: string,
|
|
256
261
|
testNamePattern?: string,
|
|
257
262
|
) => Promise<ReloadTestFileAck>;
|
|
263
|
+
/**
|
|
264
|
+
* Replace the container's copy of the host config so runner iframes loaded
|
|
265
|
+
* from now on receive fresh values (e.g. the 'u' shortcut flipping
|
|
266
|
+
* `snapshot.updateSnapshot` between watch reruns).
|
|
267
|
+
*/
|
|
268
|
+
onHostConfigUpdate: (config: BrowserHostConfig) => Promise<void>;
|
|
258
269
|
};
|
|
259
270
|
|
|
260
271
|
type ContainerRpc = BirpcReturn<ContainerRpcMethods, HostRpcMethods>;
|
|
@@ -394,6 +405,11 @@ class ContainerRpcManager {
|
|
|
394
405
|
await this.rpc?.onTestFileUpdate(files);
|
|
395
406
|
}
|
|
396
407
|
|
|
408
|
+
/** Push a refreshed host config to the container (watch reruns) */
|
|
409
|
+
async updateHostConfig(config: BrowserHostConfig): Promise<void> {
|
|
410
|
+
await this.rpc?.onHostConfigUpdate(config);
|
|
411
|
+
}
|
|
412
|
+
|
|
397
413
|
/** Request container to reload a specific test file */
|
|
398
414
|
async reloadTestFile(
|
|
399
415
|
testFile: string,
|
|
@@ -428,6 +444,56 @@ type BrowserProjectServer = {
|
|
|
428
444
|
manifestPath: string;
|
|
429
445
|
};
|
|
430
446
|
|
|
447
|
+
// Watch diff/rerun state. Lives on the BrowserRuntime (one per set of
|
|
448
|
+
// per-project compilers, surviving controller re-entry that reuses the
|
|
449
|
+
// runtime) instead of module scope, so its lifetime always matches the
|
|
450
|
+
// compilers whose baselines it holds.
|
|
451
|
+
type BrowserWatchState = {
|
|
452
|
+
lastTestFiles: TestFileInfo[];
|
|
453
|
+
hooksEnabled: boolean;
|
|
454
|
+
// Diff baselines keyed per project: sibling projects have isolated
|
|
455
|
+
// compilers, so a shared flat baseline would let one project's compile
|
|
456
|
+
// clobber another's (missed reruns) and collide on compiler-local chunk
|
|
457
|
+
// keys.
|
|
458
|
+
invalidation: Map<string, WatchInvalidationState>;
|
|
459
|
+
// Affected files accumulated per project until a rerun drains them, so a
|
|
460
|
+
// compile finishing while another project's rerun is being planned cannot
|
|
461
|
+
// drop pending work.
|
|
462
|
+
pendingAffectedTestFiles: Map<string, Set<string>>;
|
|
463
|
+
// Per-project compile start times and the accumulated compile duration of
|
|
464
|
+
// the pending rerun, so the rerun's finalize reports the real buildTime.
|
|
465
|
+
compileStartTimes: Map<string, number>;
|
|
466
|
+
pendingBuildTimeMs: number;
|
|
467
|
+
};
|
|
468
|
+
|
|
469
|
+
const createBrowserWatchState = (): BrowserWatchState => ({
|
|
470
|
+
lastTestFiles: [],
|
|
471
|
+
hooksEnabled: false,
|
|
472
|
+
invalidation: new Map(),
|
|
473
|
+
pendingAffectedTestFiles: new Map(),
|
|
474
|
+
compileStartTimes: new Map(),
|
|
475
|
+
pendingBuildTimeMs: 0,
|
|
476
|
+
});
|
|
477
|
+
|
|
478
|
+
const drainPendingBuildTime = (watchState: BrowserWatchState): number => {
|
|
479
|
+
const buildTime = watchState.pendingBuildTimeMs;
|
|
480
|
+
watchState.pendingBuildTimeMs = 0;
|
|
481
|
+
return buildTime;
|
|
482
|
+
};
|
|
483
|
+
|
|
484
|
+
const drainPendingAffectedTestFiles = (
|
|
485
|
+
watchState: BrowserWatchState,
|
|
486
|
+
): string[] => {
|
|
487
|
+
const affected = new Set<string>();
|
|
488
|
+
for (const files of watchState.pendingAffectedTestFiles.values()) {
|
|
489
|
+
for (const file of files) {
|
|
490
|
+
affected.add(file);
|
|
491
|
+
}
|
|
492
|
+
}
|
|
493
|
+
watchState.pendingAffectedTestFiles.clear();
|
|
494
|
+
return Array.from(affected);
|
|
495
|
+
};
|
|
496
|
+
|
|
431
497
|
type BrowserRuntime = {
|
|
432
498
|
// Per-project servers, keyed by project name.
|
|
433
499
|
projectServers: Map<string, BrowserProjectServer>;
|
|
@@ -446,32 +512,27 @@ type BrowserRuntime = {
|
|
|
446
512
|
wss: WebSocketServer;
|
|
447
513
|
rpcManager?: ContainerRpcManager;
|
|
448
514
|
projectEntries: BrowserProjectEntries[];
|
|
515
|
+
watchState: BrowserWatchState;
|
|
449
516
|
};
|
|
450
517
|
|
|
451
518
|
// ============================================================================
|
|
452
|
-
// Watch Mode Context -
|
|
519
|
+
// Watch Mode Context - Process-lifecycle watch state
|
|
453
520
|
// ============================================================================
|
|
454
521
|
|
|
522
|
+
// Only process-wide concerns stay module-level: the runtime handle reused
|
|
523
|
+
// across controller re-entry (config-change restarts), and the signal/exit
|
|
524
|
+
// cleanup that must run once per process. Diff/rerun state lives on
|
|
525
|
+
// `BrowserRuntime.watchState`.
|
|
455
526
|
type WatchContext = {
|
|
456
527
|
runtime: BrowserRuntime | null;
|
|
457
|
-
lastTestFiles: TestFileInfo[];
|
|
458
|
-
hooksEnabled: boolean;
|
|
459
528
|
cleanupRegistered: boolean;
|
|
460
529
|
cleanupPromise: Promise<void> | null;
|
|
461
|
-
closeCliShortcuts: (() => void) | null;
|
|
462
|
-
chunkHashes: Map<string, string>;
|
|
463
|
-
affectedTestFiles: string[];
|
|
464
530
|
};
|
|
465
531
|
|
|
466
532
|
const watchContext: WatchContext = {
|
|
467
533
|
runtime: null,
|
|
468
|
-
lastTestFiles: [],
|
|
469
|
-
hooksEnabled: false,
|
|
470
534
|
cleanupRegistered: false,
|
|
471
535
|
cleanupPromise: null,
|
|
472
|
-
closeCliShortcuts: null,
|
|
473
|
-
chunkHashes: new Map(),
|
|
474
|
-
affectedTestFiles: [],
|
|
475
536
|
};
|
|
476
537
|
|
|
477
538
|
// ============================================================================
|
|
@@ -948,42 +1009,87 @@ const getChunkKey = (chunk: StatsChunk): string | null => {
|
|
|
948
1009
|
};
|
|
949
1010
|
|
|
950
1011
|
/**
|
|
951
|
-
*
|
|
952
|
-
*
|
|
1012
|
+
* Fold one project compile's chunks into per-entry hash snapshots and apply
|
|
1013
|
+
* the shared watch-invalidation policy against that project's baseline.
|
|
1014
|
+
* Chunks are attributed to a test/setup file by scanning their modules; the
|
|
1015
|
+
* chunk.id/names key is only the hash-record key, never a cross-project one.
|
|
953
1016
|
*/
|
|
954
|
-
const getAffectedTestFiles = (
|
|
955
|
-
chunks
|
|
956
|
-
entryTestFiles
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
1017
|
+
const getAffectedTestFiles = ({
|
|
1018
|
+
chunks,
|
|
1019
|
+
entryTestFiles,
|
|
1020
|
+
setupFiles,
|
|
1021
|
+
state,
|
|
1022
|
+
}: {
|
|
1023
|
+
chunks: StatsChunk[] | undefined;
|
|
1024
|
+
entryTestFiles: Set<string>;
|
|
1025
|
+
setupFiles: Set<string>;
|
|
1026
|
+
state: WatchInvalidationState;
|
|
1027
|
+
}): string[] => {
|
|
1028
|
+
const entryHashes: EntryHashSnapshot = new Map();
|
|
1029
|
+
const setupHashes: EntryHashSnapshot = new Map();
|
|
1030
|
+
|
|
1031
|
+
const recordChunk = (
|
|
1032
|
+
snapshot: EntryHashSnapshot,
|
|
1033
|
+
entryPath: string,
|
|
1034
|
+
chunkKey: string,
|
|
1035
|
+
hash: string,
|
|
1036
|
+
) => {
|
|
1037
|
+
const record = snapshot.get(entryPath) ?? {};
|
|
1038
|
+
record[chunkKey] = hash;
|
|
1039
|
+
snapshot.set(entryPath, record);
|
|
1040
|
+
};
|
|
962
1041
|
|
|
963
|
-
for (const chunk of chunks) {
|
|
1042
|
+
for (const chunk of chunks || []) {
|
|
964
1043
|
if (!chunk.hash) continue;
|
|
965
1044
|
|
|
966
|
-
// First check if this chunk contains a test entry file
|
|
967
|
-
const testFile = findTestFileInModules(chunk.modules, entryTestFiles);
|
|
968
|
-
if (!testFile) continue;
|
|
969
|
-
|
|
970
|
-
// Get a stable key for this chunk
|
|
971
1045
|
const chunkKey = getChunkKey(chunk);
|
|
972
1046
|
if (!chunkKey) continue;
|
|
973
1047
|
|
|
974
|
-
const
|
|
975
|
-
|
|
1048
|
+
const testFile = findTestFileInModules(chunk.modules, entryTestFiles);
|
|
1049
|
+
if (testFile) {
|
|
1050
|
+
recordChunk(entryHashes, testFile, chunkKey, chunk.hash);
|
|
1051
|
+
continue;
|
|
1052
|
+
}
|
|
976
1053
|
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
1054
|
+
const setupFile = findTestFileInModules(chunk.modules, setupFiles);
|
|
1055
|
+
if (setupFile) {
|
|
1056
|
+
recordChunk(setupHashes, setupFile, chunkKey, chunk.hash);
|
|
1057
|
+
}
|
|
1058
|
+
}
|
|
1059
|
+
|
|
1060
|
+
// Headed watch compiles chunks on demand (lazyCompilation), so an entry's
|
|
1061
|
+
// first appearance in stats means "just loaded", not "just added": its first
|
|
1062
|
+
// sighting establishes the baseline instead of marking a change. Genuinely
|
|
1063
|
+
// new and deleted test files are owned by the test-file-set diff in
|
|
1064
|
+
// `planWatchRerun` / `collectDeletedTestPaths`.
|
|
1065
|
+
const seedFirstSeen = (
|
|
1066
|
+
baseline: EntryHashSnapshot | undefined,
|
|
1067
|
+
current: EntryHashSnapshot,
|
|
1068
|
+
) => {
|
|
1069
|
+
if (!baseline) return;
|
|
1070
|
+
for (const [entryPath, record] of current) {
|
|
1071
|
+
if (!baseline.has(entryPath)) {
|
|
1072
|
+
baseline.set(entryPath, record);
|
|
1073
|
+
}
|
|
982
1074
|
}
|
|
1075
|
+
};
|
|
1076
|
+
seedFirstSeen(state.entryHashes, entryHashes);
|
|
1077
|
+
seedFirstSeen(state.setupHashes, setupHashes);
|
|
1078
|
+
|
|
1079
|
+
const outcome = applyWatchInvalidation(state, { entryHashes, setupHashes });
|
|
1080
|
+
|
|
1081
|
+
if (outcome.rerunAll) {
|
|
1082
|
+
logger.debug(
|
|
1083
|
+
'[Watch] Setup file changed, re-running all test files of the project',
|
|
1084
|
+
);
|
|
1085
|
+
return Array.from(entryTestFiles);
|
|
983
1086
|
}
|
|
984
1087
|
|
|
985
|
-
|
|
986
|
-
|
|
1088
|
+
for (const affected of outcome.affectedPaths) {
|
|
1089
|
+
logger.debug(`[Watch] Chunk hash changed for test: ${affected}`);
|
|
1090
|
+
}
|
|
1091
|
+
|
|
1092
|
+
return outcome.affectedPaths;
|
|
987
1093
|
};
|
|
988
1094
|
|
|
989
1095
|
const getBrowserProjects = (context: RstestContext): ProjectContext[] =>
|
|
@@ -1413,9 +1519,6 @@ const cleanupWatchRuntime = (): Promise<void> => {
|
|
|
1413
1519
|
}
|
|
1414
1520
|
|
|
1415
1521
|
watchContext.cleanupPromise = (async () => {
|
|
1416
|
-
watchContext.closeCliShortcuts?.();
|
|
1417
|
-
watchContext.closeCliShortcuts = null;
|
|
1418
|
-
|
|
1419
1522
|
if (!watchContext.runtime) {
|
|
1420
1523
|
return;
|
|
1421
1524
|
}
|
|
@@ -1427,12 +1530,22 @@ const cleanupWatchRuntime = (): Promise<void> => {
|
|
|
1427
1530
|
return watchContext.cleanupPromise;
|
|
1428
1531
|
};
|
|
1429
1532
|
|
|
1430
|
-
const registerWatchCleanup = (): void => {
|
|
1533
|
+
const registerWatchCleanup = (embedded: boolean): void => {
|
|
1431
1534
|
if (watchContext.cleanupRegistered) {
|
|
1432
1535
|
return;
|
|
1433
1536
|
}
|
|
1537
|
+
watchContext.cleanupRegistered = true;
|
|
1538
|
+
|
|
1539
|
+
// Embedded (programmatic) hosts own the process lifecycle; they tear the
|
|
1540
|
+
// session down through the watch handles' `close` instead of signals.
|
|
1541
|
+
if (embedded) {
|
|
1542
|
+
return;
|
|
1543
|
+
}
|
|
1434
1544
|
|
|
1435
|
-
|
|
1545
|
+
// Cleanup-only nets: core's watch loop owns the signal → exit-code path
|
|
1546
|
+
// (`registerBrowserWatchSignalExit` / the mixed watch handler) and awaits
|
|
1547
|
+
// the same idempotent `cleanupWatchRuntime` promise through `watch.close`.
|
|
1548
|
+
for (const signal of FATAL_SIGNALS) {
|
|
1436
1549
|
process.once(signal, () => {
|
|
1437
1550
|
void cleanupWatchRuntime();
|
|
1438
1551
|
});
|
|
@@ -1441,8 +1554,6 @@ const registerWatchCleanup = (): void => {
|
|
|
1441
1554
|
process.once('exit', () => {
|
|
1442
1555
|
void cleanupWatchRuntime();
|
|
1443
1556
|
});
|
|
1444
|
-
|
|
1445
|
-
watchContext.cleanupRegistered = true;
|
|
1446
1557
|
};
|
|
1447
1558
|
|
|
1448
1559
|
const createBrowserRuntime = async ({
|
|
@@ -1502,6 +1613,10 @@ const createBrowserRuntime = async ({
|
|
|
1502
1613
|
let browserLaunchOptions =
|
|
1503
1614
|
ensureConsistentBrowserLaunchOptions(browserProjects);
|
|
1504
1615
|
let projectEntries = initialProjectEntries;
|
|
1616
|
+
// Created with the runtime so the per-project watch plugins and the
|
|
1617
|
+
// controller's rerun closures share one state whose lifetime matches the
|
|
1618
|
+
// compilers holding the diffed chunks.
|
|
1619
|
+
const watchState = createBrowserWatchState();
|
|
1505
1620
|
const manifestModules: Array<{
|
|
1506
1621
|
manifestPath: string;
|
|
1507
1622
|
project: ProjectContext;
|
|
@@ -1530,6 +1645,7 @@ const createBrowserRuntime = async ({
|
|
|
1530
1645
|
dispatchHandlers,
|
|
1531
1646
|
wss: undefined as unknown as WebSocketServer,
|
|
1532
1647
|
projectEntries,
|
|
1648
|
+
watchState,
|
|
1533
1649
|
};
|
|
1534
1650
|
};
|
|
1535
1651
|
|
|
@@ -1673,6 +1789,55 @@ const createBrowserRuntime = async ({
|
|
|
1673
1789
|
}
|
|
1674
1790
|
};
|
|
1675
1791
|
|
|
1792
|
+
const serveContainerRoute = async (
|
|
1793
|
+
req: IncomingMessage,
|
|
1794
|
+
res: ServerResponse,
|
|
1795
|
+
next: () => void,
|
|
1796
|
+
): Promise<void> => {
|
|
1797
|
+
if (!req.url) {
|
|
1798
|
+
next();
|
|
1799
|
+
return;
|
|
1800
|
+
}
|
|
1801
|
+
|
|
1802
|
+
const url = new URL(req.url, 'http://localhost');
|
|
1803
|
+
if (url.pathname === '/') {
|
|
1804
|
+
if (await respondWithDevServerHtml(url, res)) {
|
|
1805
|
+
return;
|
|
1806
|
+
}
|
|
1807
|
+
|
|
1808
|
+
const html =
|
|
1809
|
+
injectedContainerHtml ||
|
|
1810
|
+
containerHtmlTemplate?.replace(OPTIONS_PLACEHOLDER, 'null');
|
|
1811
|
+
|
|
1812
|
+
if (html) {
|
|
1813
|
+
res.setHeader('Content-Type', 'text/html');
|
|
1814
|
+
res.end(html);
|
|
1815
|
+
return;
|
|
1816
|
+
}
|
|
1817
|
+
|
|
1818
|
+
res.statusCode = 502;
|
|
1819
|
+
res.end('Container UI is not available.');
|
|
1820
|
+
return;
|
|
1821
|
+
}
|
|
1822
|
+
|
|
1823
|
+
if (url.pathname.startsWith('/container-static/')) {
|
|
1824
|
+
if (await proxyDevServerAsset(req, res)) {
|
|
1825
|
+
return;
|
|
1826
|
+
}
|
|
1827
|
+
|
|
1828
|
+
if (serveContainer) {
|
|
1829
|
+
serveContainer(req, res, next);
|
|
1830
|
+
return;
|
|
1831
|
+
}
|
|
1832
|
+
|
|
1833
|
+
res.statusCode = 502;
|
|
1834
|
+
res.end('Container assets are not available.');
|
|
1835
|
+
return;
|
|
1836
|
+
}
|
|
1837
|
+
|
|
1838
|
+
next();
|
|
1839
|
+
};
|
|
1840
|
+
|
|
1676
1841
|
// ---- Build one isolated rsbuild instance + dev server per project ----
|
|
1677
1842
|
const buildProjectServer = async (
|
|
1678
1843
|
project: ProjectContext,
|
|
@@ -1730,6 +1895,13 @@ const createBrowserRuntime = async ({
|
|
|
1730
1895
|
project.normalizedConfig.browser.port ??
|
|
1731
1896
|
(isContainerServer ? 4000 : 0),
|
|
1732
1897
|
strictPort: project.normalizedConfig.browser.strictPort,
|
|
1898
|
+
// User plugins may emit index.html; register before Rsbuild's HTML
|
|
1899
|
+
// completion middleware so `/` remains owned by the Browser UI.
|
|
1900
|
+
setup: isContainerServer
|
|
1901
|
+
? ({ server }) => {
|
|
1902
|
+
server.middlewares.use(serveContainerRoute);
|
|
1903
|
+
}
|
|
1904
|
+
: undefined,
|
|
1733
1905
|
},
|
|
1734
1906
|
dev: createBrowserRsbuildDevConfig(enableHmr),
|
|
1735
1907
|
environments: {
|
|
@@ -1822,8 +1994,35 @@ const createBrowserRuntime = async ({
|
|
|
1822
1994
|
sourceMap: {
|
|
1823
1995
|
js: 'source-map',
|
|
1824
1996
|
},
|
|
1997
|
+
// Every project server compiles the same asset names
|
|
1998
|
+
// (`static/js/runner.js`, ...). With `dev.writeToDisk`
|
|
1999
|
+
// (debug mode) the middleware serves from disk, so a
|
|
2000
|
+
// shared dist dir would be last-writer-wins and one
|
|
2001
|
+
// project's server would deliver another project's
|
|
2002
|
+
// bundle — keep each project's output isolated, inside
|
|
2003
|
+
// the run's temp dir so teardown removes it.
|
|
2004
|
+
distPath: {
|
|
2005
|
+
root: join(
|
|
2006
|
+
tempDir,
|
|
2007
|
+
'server',
|
|
2008
|
+
toSafeVarName(project.environmentName),
|
|
2009
|
+
),
|
|
2010
|
+
},
|
|
1825
2011
|
},
|
|
1826
2012
|
tools: {
|
|
2013
|
+
swc: (swcConfig) => {
|
|
2014
|
+
// Fixture dependency discovery reads callback parameters
|
|
2015
|
+
// through Function#toString(). Playwright's supported
|
|
2016
|
+
// browsers all support parameter destructuring, so keep
|
|
2017
|
+
// that syntax intact in the browser test bundle.
|
|
2018
|
+
swcConfig.env ??= {};
|
|
2019
|
+
swcConfig.env.exclude = Array.from(
|
|
2020
|
+
new Set([
|
|
2021
|
+
...(swcConfig.env.exclude ?? []),
|
|
2022
|
+
'transform-parameters',
|
|
2023
|
+
]),
|
|
2024
|
+
);
|
|
2025
|
+
},
|
|
1827
2026
|
rspack: (rspackConfig) => {
|
|
1828
2027
|
rspackConfig.mode = 'development';
|
|
1829
2028
|
// Web parameterization of the node mock transform:
|
|
@@ -1893,37 +2092,80 @@ const createBrowserRuntime = async ({
|
|
|
1893
2092
|
name: 'rstest:browser-watch',
|
|
1894
2093
|
setup(api) {
|
|
1895
2094
|
api.onBeforeDevCompile(() => {
|
|
1896
|
-
|
|
2095
|
+
watchState.compileStartTimes.set(project.name, Date.now());
|
|
2096
|
+
if (!watchState.hooksEnabled) {
|
|
1897
2097
|
return;
|
|
1898
2098
|
}
|
|
1899
2099
|
logger.log(color.cyan('\nFile changed, re-running tests...\n'));
|
|
1900
2100
|
});
|
|
1901
2101
|
|
|
1902
2102
|
api.onAfterDevCompile(async ({ stats }) => {
|
|
2103
|
+
const compileStart = watchState.compileStartTimes.get(
|
|
2104
|
+
project.name,
|
|
2105
|
+
);
|
|
2106
|
+
if (compileStart !== undefined) {
|
|
2107
|
+
watchState.compileStartTimes.delete(project.name);
|
|
2108
|
+
// Only change-triggered compiles feed the pending rerun's
|
|
2109
|
+
// build phase (the initial build is the initial run's
|
|
2110
|
+
// buildTime). Parallel project compiles overlap; the longest
|
|
2111
|
+
// one bounds the rerun's build phase.
|
|
2112
|
+
if (watchState.hooksEnabled) {
|
|
2113
|
+
watchState.pendingBuildTimeMs = Math.max(
|
|
2114
|
+
watchState.pendingBuildTimeMs,
|
|
2115
|
+
Date.now() - compileStart,
|
|
2116
|
+
);
|
|
2117
|
+
}
|
|
2118
|
+
}
|
|
1903
2119
|
// Collect hashes even during initial build to establish baseline
|
|
1904
2120
|
if (stats) {
|
|
1905
|
-
|
|
2121
|
+
// This compiler only ever holds this project's entries; the
|
|
2122
|
+
// diff baseline is keyed per project accordingly.
|
|
2123
|
+
const [projectEntry] = await collectProjectEntries(context, [
|
|
2124
|
+
project,
|
|
2125
|
+
]);
|
|
1906
2126
|
const entryTestFiles = new Set<string>(
|
|
1907
|
-
collectWatchTestFiles(
|
|
2127
|
+
collectWatchTestFiles(projectEntry ? [projectEntry] : []).map(
|
|
1908
2128
|
(file) => file.testPath,
|
|
1909
2129
|
),
|
|
1910
2130
|
);
|
|
2131
|
+
const setupFiles = new Set<string>(
|
|
2132
|
+
(projectEntry?.setupFiles ?? []).map((file) =>
|
|
2133
|
+
normalize(file),
|
|
2134
|
+
),
|
|
2135
|
+
);
|
|
2136
|
+
|
|
2137
|
+
let state = watchState.invalidation.get(project.name);
|
|
2138
|
+
if (!state) {
|
|
2139
|
+
state = {};
|
|
2140
|
+
watchState.invalidation.set(project.name, state);
|
|
2141
|
+
}
|
|
1911
2142
|
|
|
1912
2143
|
const statsJson = stats.toJson({ all: true });
|
|
1913
|
-
const affected = getAffectedTestFiles(
|
|
1914
|
-
statsJson.chunks,
|
|
2144
|
+
const affected = getAffectedTestFiles({
|
|
2145
|
+
chunks: statsJson.chunks,
|
|
1915
2146
|
entryTestFiles,
|
|
1916
|
-
|
|
1917
|
-
|
|
2147
|
+
setupFiles,
|
|
2148
|
+
state,
|
|
2149
|
+
});
|
|
1918
2150
|
|
|
1919
2151
|
if (affected.length > 0) {
|
|
2152
|
+
const pending =
|
|
2153
|
+
watchState.pendingAffectedTestFiles.get(project.name) ??
|
|
2154
|
+
new Set<string>();
|
|
2155
|
+
for (const file of affected) {
|
|
2156
|
+
pending.add(file);
|
|
2157
|
+
}
|
|
2158
|
+
watchState.pendingAffectedTestFiles.set(
|
|
2159
|
+
project.name,
|
|
2160
|
+
pending,
|
|
2161
|
+
);
|
|
1920
2162
|
logger.debug(
|
|
1921
2163
|
`[Watch] Affected test files: ${affected.join(', ')}`,
|
|
1922
2164
|
);
|
|
1923
2165
|
}
|
|
1924
2166
|
}
|
|
1925
2167
|
|
|
1926
|
-
if (!
|
|
2168
|
+
if (!watchState.hooksEnabled) {
|
|
1927
2169
|
return;
|
|
1928
2170
|
}
|
|
1929
2171
|
|
|
@@ -1965,6 +2207,15 @@ const createBrowserRuntime = async ({
|
|
|
1965
2207
|
if (isDebug()) {
|
|
1966
2208
|
await rsbuildInstance.inspectConfig({
|
|
1967
2209
|
writeToDisk: true,
|
|
2210
|
+
// The server's own distPath is isolated per project inside the run's
|
|
2211
|
+
// temp dir (removed at teardown); keep the debug artifacts at the
|
|
2212
|
+
// project's stable dist root so they survive the run and stay where
|
|
2213
|
+
// the docs point users to.
|
|
2214
|
+
outputPath: resolve(
|
|
2215
|
+
context.rootPath,
|
|
2216
|
+
context.normalizedConfig.output.distPath.root,
|
|
2217
|
+
'.rsbuild',
|
|
2218
|
+
),
|
|
1968
2219
|
extraConfigs: {
|
|
1969
2220
|
rstest: {
|
|
1970
2221
|
...context.normalizedConfig,
|
|
@@ -2001,43 +2252,6 @@ const createBrowserRuntime = async ({
|
|
|
2001
2252
|
}
|
|
2002
2253
|
return;
|
|
2003
2254
|
}
|
|
2004
|
-
// Container UI HTML + static assets are served by the container origin
|
|
2005
|
-
// only. Per-project runner servers expose just /runner.html + assets.
|
|
2006
|
-
if (isContainerServer) {
|
|
2007
|
-
if (url.pathname === '/') {
|
|
2008
|
-
if (await respondWithDevServerHtml(url, res)) {
|
|
2009
|
-
return;
|
|
2010
|
-
}
|
|
2011
|
-
|
|
2012
|
-
const html =
|
|
2013
|
-
injectedContainerHtml ||
|
|
2014
|
-
containerHtmlTemplate?.replace(OPTIONS_PLACEHOLDER, 'null');
|
|
2015
|
-
|
|
2016
|
-
if (html) {
|
|
2017
|
-
res.setHeader('Content-Type', 'text/html');
|
|
2018
|
-
res.end(html);
|
|
2019
|
-
return;
|
|
2020
|
-
}
|
|
2021
|
-
|
|
2022
|
-
res.statusCode = 502;
|
|
2023
|
-
res.end('Container UI is not available.');
|
|
2024
|
-
return;
|
|
2025
|
-
}
|
|
2026
|
-
if (url.pathname.startsWith('/container-static/')) {
|
|
2027
|
-
if (await proxyDevServerAsset(req, res)) {
|
|
2028
|
-
return;
|
|
2029
|
-
}
|
|
2030
|
-
|
|
2031
|
-
if (serveContainer) {
|
|
2032
|
-
serveContainer(req, res, next);
|
|
2033
|
-
return;
|
|
2034
|
-
}
|
|
2035
|
-
|
|
2036
|
-
res.statusCode = 502;
|
|
2037
|
-
res.end('Container assets are not available.');
|
|
2038
|
-
return;
|
|
2039
|
-
}
|
|
2040
|
-
}
|
|
2041
2255
|
if (url.pathname === '/runner.html') {
|
|
2042
2256
|
res.setHeader('Content-Type', 'text/html');
|
|
2043
2257
|
res.end(htmlTemplate);
|
|
@@ -2114,6 +2328,7 @@ const createBrowserRuntime = async ({
|
|
|
2114
2328
|
dispatchHandlers,
|
|
2115
2329
|
wss,
|
|
2116
2330
|
projectEntries,
|
|
2331
|
+
watchState,
|
|
2117
2332
|
};
|
|
2118
2333
|
} catch (error) {
|
|
2119
2334
|
wss.close();
|
|
@@ -2157,7 +2372,6 @@ export const runBrowserController = async (
|
|
|
2157
2372
|
options?: BrowserTestRunOptions,
|
|
2158
2373
|
): Promise<BrowserTestRunResult | void> => {
|
|
2159
2374
|
const {
|
|
2160
|
-
allowEmptyWatchRun = false,
|
|
2161
2375
|
allowEmptyRun = false,
|
|
2162
2376
|
filesOnly = false,
|
|
2163
2377
|
onTraceEvents,
|
|
@@ -2175,10 +2389,13 @@ export const runBrowserController = async (
|
|
|
2175
2389
|
// passes `onTraceEvents`). The browser host shares one Node process across
|
|
2176
2390
|
// every test file, so each tracker is assigned a synthetic per-file pid
|
|
2177
2391
|
// (`nextBrowserFilePid`) that lets Perfetto render each file as its own
|
|
2178
|
-
// process track with the file path as the title.
|
|
2392
|
+
// process track with the file path as the title. Keyed by project + path so
|
|
2393
|
+
// concurrent projects running the same file keep separate trackers.
|
|
2179
2394
|
const phaseTrackers = onTraceEvents
|
|
2180
2395
|
? new Map<string, PhaseTracker>()
|
|
2181
2396
|
: undefined;
|
|
2397
|
+
const trackerKey = (project: string, testPath: string) =>
|
|
2398
|
+
`${project}\u0000${testPath}`;
|
|
2182
2399
|
// Explicit projects input (plan output) replaces re-deriving `browser.enabled`
|
|
2183
2400
|
// projects from `context`, whose `projects` array is mutated during planning.
|
|
2184
2401
|
// Falls back to re-derivation only when the caller passes no list at all —
|
|
@@ -2324,47 +2541,27 @@ export const runBrowserController = async (
|
|
|
2324
2541
|
|
|
2325
2542
|
const notifyTestRunEnd = async ({
|
|
2326
2543
|
duration,
|
|
2327
|
-
|
|
2328
|
-
filterRerunTestPaths,
|
|
2544
|
+
coverage,
|
|
2329
2545
|
}: {
|
|
2330
2546
|
duration: {
|
|
2331
2547
|
totalTime: number;
|
|
2332
2548
|
buildTime: number;
|
|
2333
2549
|
testTime: number;
|
|
2334
2550
|
};
|
|
2335
|
-
|
|
2336
|
-
filterRerunTestPaths?: string[];
|
|
2551
|
+
coverage?: CoverageMapData;
|
|
2337
2552
|
}): Promise<void> => {
|
|
2338
2553
|
if (!isWatchMode) {
|
|
2339
2554
|
return;
|
|
2340
2555
|
}
|
|
2341
2556
|
|
|
2342
|
-
// Merge per-file coverage into a single CoverageMapData for reporters
|
|
2343
|
-
let mergedCoverage: CoverageMapData | undefined;
|
|
2344
|
-
if (coverageProvider) {
|
|
2345
|
-
const coverageMap = coverageProvider.createCoverageMap();
|
|
2346
|
-
let hasCoverage = false;
|
|
2347
|
-
for (const result of context.reporterResults.results) {
|
|
2348
|
-
if (result.coverage) {
|
|
2349
|
-
coverageMap.merge(result.coverage);
|
|
2350
|
-
hasCoverage = true;
|
|
2351
|
-
}
|
|
2352
|
-
}
|
|
2353
|
-
if (hasCoverage) {
|
|
2354
|
-
mergedCoverage = coverageMap.toJSON();
|
|
2355
|
-
}
|
|
2356
|
-
}
|
|
2357
|
-
|
|
2358
2557
|
for (const reporter of context.reporters) {
|
|
2359
2558
|
await reporter.onTestRunEnd?.({
|
|
2360
2559
|
results: context.reporterResults.results,
|
|
2361
|
-
coverage
|
|
2560
|
+
coverage,
|
|
2362
2561
|
testResults: context.reporterResults.testResults,
|
|
2363
2562
|
duration,
|
|
2364
2563
|
snapshotSummary: context.snapshotManager.summary,
|
|
2365
2564
|
getSourcemap: getBrowserSourcemap,
|
|
2366
|
-
unhandledErrors,
|
|
2367
|
-
filterRerunTestPaths,
|
|
2368
2565
|
});
|
|
2369
2566
|
}
|
|
2370
2567
|
};
|
|
@@ -2405,7 +2602,6 @@ export const runBrowserController = async (
|
|
|
2405
2602
|
(total, item) => total + item.testFiles.length,
|
|
2406
2603
|
0,
|
|
2407
2604
|
);
|
|
2408
|
-
const shouldKeepWatchingWithEmptySet = isWatchMode && allowEmptyWatchRun;
|
|
2409
2605
|
const shouldInitializeEmptyBrowserHooks =
|
|
2410
2606
|
totalTests === 0 && hasUserRstestConfigPlugins(browserProjects);
|
|
2411
2607
|
|
|
@@ -2425,16 +2621,14 @@ export const runBrowserController = async (
|
|
|
2425
2621
|
};
|
|
2426
2622
|
};
|
|
2427
2623
|
|
|
2428
|
-
const reportEmptyTestSet = ():
|
|
2624
|
+
const reportEmptyTestSet = (): void => {
|
|
2429
2625
|
const code = context.normalizedConfig.passWithNoTests ? 0 : 1;
|
|
2430
2626
|
if (isWatchMode || !allowEmptyRun) {
|
|
2431
|
-
const message =
|
|
2432
|
-
|
|
2433
|
-
|
|
2434
|
-
|
|
2435
|
-
|
|
2436
|
-
defaultMessage: `No test files found, exiting with code ${code}.`,
|
|
2437
|
-
});
|
|
2627
|
+
const message = getNoTestFilesMessage({
|
|
2628
|
+
context,
|
|
2629
|
+
code,
|
|
2630
|
+
defaultMessage: `No test files found, exiting with code ${code}.`,
|
|
2631
|
+
});
|
|
2438
2632
|
if (code === 0) {
|
|
2439
2633
|
logger.log(color.yellow(message));
|
|
2440
2634
|
} else {
|
|
@@ -2457,29 +2651,21 @@ export const runBrowserController = async (
|
|
|
2457
2651
|
// In non-watch runs the host returns a void outcome and core's
|
|
2458
2652
|
// `reportNoTestFiles` owns the exit code and the no-test reporter lifecycle;
|
|
2459
2653
|
// the host must not set the code itself. Watch keeps its own exit code.
|
|
2460
|
-
if (
|
|
2461
|
-
isWatchMode &&
|
|
2462
|
-
code !== 0 &&
|
|
2463
|
-
!shouldKeepWatchingWithEmptySet &&
|
|
2464
|
-
!allowEmptyRun
|
|
2465
|
-
) {
|
|
2654
|
+
if (isWatchMode && code !== 0 && !allowEmptyRun) {
|
|
2466
2655
|
ensureProcessExitCode(code);
|
|
2467
2656
|
}
|
|
2468
|
-
|
|
2469
|
-
return !shouldKeepWatchingWithEmptySet;
|
|
2470
2657
|
};
|
|
2471
2658
|
|
|
2472
2659
|
if (totalTests === 0 && !shouldInitializeEmptyBrowserHooks) {
|
|
2473
|
-
|
|
2474
|
-
|
|
2475
|
-
}
|
|
2660
|
+
reportEmptyTestSet();
|
|
2661
|
+
return allowEmptyRun ? createEmptyRunResult() : undefined;
|
|
2476
2662
|
}
|
|
2477
2663
|
|
|
2478
2664
|
if (!filesOnly) {
|
|
2479
2665
|
await notifyTestRunStart();
|
|
2480
2666
|
}
|
|
2481
2667
|
|
|
2482
|
-
const enableCliShortcuts = isWatchMode &&
|
|
2668
|
+
const enableCliShortcuts = isWatchMode && isTTY('stdin');
|
|
2483
2669
|
const browserTempOutputRoot = context.normalizedConfig.output.distPath.root;
|
|
2484
2670
|
const tempDir =
|
|
2485
2671
|
isWatchMode && watchContext.runtime
|
|
@@ -2493,15 +2679,13 @@ export const runBrowserController = async (
|
|
|
2493
2679
|
Date.now().toString(),
|
|
2494
2680
|
);
|
|
2495
2681
|
|
|
2496
|
-
// Track initial test files for watch mode
|
|
2497
|
-
if (isWatchMode) {
|
|
2498
|
-
watchContext.lastTestFiles = collectWatchTestFiles(projectEntries);
|
|
2499
|
-
}
|
|
2500
|
-
|
|
2501
2682
|
let runtime = isWatchMode ? watchContext.runtime : null;
|
|
2502
2683
|
|
|
2503
2684
|
// Define rerun callback for watch mode (will be populated later)
|
|
2504
2685
|
let triggerRerun: (() => Promise<void>) | undefined;
|
|
2686
|
+
// Headless reruns complete asynchronously in the scheduler's drain loop;
|
|
2687
|
+
// the watch handles await this so callers observe rerun completion.
|
|
2688
|
+
let awaitHeadlessRerunIdle: (() => Promise<void>) | undefined;
|
|
2505
2689
|
|
|
2506
2690
|
if (!runtime) {
|
|
2507
2691
|
try {
|
|
@@ -2532,15 +2716,109 @@ export const runBrowserController = async (
|
|
|
2532
2716
|
|
|
2533
2717
|
if (isWatchMode) {
|
|
2534
2718
|
watchContext.runtime = runtime;
|
|
2535
|
-
registerWatchCleanup();
|
|
2719
|
+
registerWatchCleanup(context.embedded);
|
|
2720
|
+
}
|
|
2721
|
+
}
|
|
2536
2722
|
|
|
2537
|
-
|
|
2538
|
-
|
|
2539
|
-
|
|
2540
|
-
|
|
2723
|
+
const watchState = runtime.watchState;
|
|
2724
|
+
|
|
2725
|
+
// Track initial test files for watch mode (from this controller's freshly
|
|
2726
|
+
// collected entries, before adopting the runtime's entry snapshot below).
|
|
2727
|
+
if (isWatchMode) {
|
|
2728
|
+
watchState.lastTestFiles = collectWatchTestFiles(projectEntries);
|
|
2729
|
+
}
|
|
2730
|
+
|
|
2731
|
+
// Mark files as pending-affected so the next `triggerRerun` reruns them
|
|
2732
|
+
// through the normal plan/schedule/finalize pipeline (used by the watch
|
|
2733
|
+
// handles' explicit reruns; omitted paths = all current files). Returns the
|
|
2734
|
+
// number of seeded files so callers can skip the rerun when a path-scoped
|
|
2735
|
+
// request matches no browser test file (mixed watch 'u' with node-only
|
|
2736
|
+
// snapshot updates).
|
|
2737
|
+
const seedPendingRerun = (testPaths?: string[]): number => {
|
|
2738
|
+
const wanted = testPaths
|
|
2739
|
+
? new Set(testPaths.map((testPath) => normalize(testPath)))
|
|
2740
|
+
: null;
|
|
2741
|
+
let seeded = 0;
|
|
2742
|
+
for (const file of watchState.lastTestFiles) {
|
|
2743
|
+
if (wanted && !wanted.has(file.testPath)) {
|
|
2744
|
+
continue;
|
|
2541
2745
|
}
|
|
2746
|
+
const pending =
|
|
2747
|
+
watchState.pendingAffectedTestFiles.get(file.projectName) ??
|
|
2748
|
+
new Set<string>();
|
|
2749
|
+
pending.add(file.testPath);
|
|
2750
|
+
watchState.pendingAffectedTestFiles.set(file.projectName, pending);
|
|
2751
|
+
seeded += 1;
|
|
2542
2752
|
}
|
|
2543
|
-
|
|
2753
|
+
return seeded;
|
|
2754
|
+
};
|
|
2755
|
+
|
|
2756
|
+
const watchHandles: BrowserWatchHandles | undefined = isWatchMode
|
|
2757
|
+
? {
|
|
2758
|
+
rerun: async (testPaths) => {
|
|
2759
|
+
const seeded = seedPendingRerun(testPaths);
|
|
2760
|
+
if (testPaths && seeded === 0) {
|
|
2761
|
+
return;
|
|
2762
|
+
}
|
|
2763
|
+
await triggerRerun?.();
|
|
2764
|
+
await awaitHeadlessRerunIdle?.();
|
|
2765
|
+
},
|
|
2766
|
+
close: cleanupWatchRuntime,
|
|
2767
|
+
}
|
|
2768
|
+
: undefined;
|
|
2769
|
+
|
|
2770
|
+
/**
|
|
2771
|
+
* Per-rerun finalize for watch mode: fold the rerun into a synthetic
|
|
2772
|
+
* `ExecutorCycleOutcome` and hand it to core's `finalizeRunCycle`, so
|
|
2773
|
+
* reporter payloads, exit-code never-downgrade semantics, and coverage
|
|
2774
|
+
* reports match the node watch cycle. The trace buffer stays session-owned
|
|
2775
|
+
* (no `traceRun` here); `buildTime` is the drained duration of the
|
|
2776
|
+
* change-triggered compile(s), not a hardcoded zero.
|
|
2777
|
+
*/
|
|
2778
|
+
const finalizeWatchRerun = async ({
|
|
2779
|
+
rerunTestPaths,
|
|
2780
|
+
testTime,
|
|
2781
|
+
unhandledErrors,
|
|
2782
|
+
}: {
|
|
2783
|
+
rerunTestPaths: string[];
|
|
2784
|
+
testTime: number;
|
|
2785
|
+
unhandledErrors?: Error[];
|
|
2786
|
+
}): Promise<void> => {
|
|
2787
|
+
const rerunPathSet = new Set(rerunTestPaths);
|
|
2788
|
+
const rerunResults = context.reporterResults.results.filter((result) =>
|
|
2789
|
+
rerunPathSet.has(result.testPath),
|
|
2790
|
+
);
|
|
2791
|
+
// Watch coverage is per-cycle on both transports: only the files this
|
|
2792
|
+
// rerun executed are reported.
|
|
2793
|
+
let rerunCoverage: CoverageMapData | undefined;
|
|
2794
|
+
const coverageMap = buildBrowserCoverageMap(rerunResults, coverageProvider);
|
|
2795
|
+
if (coverageMap && coverageMap.files().length > 0) {
|
|
2796
|
+
rerunCoverage = coverageMap.toJSON();
|
|
2797
|
+
}
|
|
2798
|
+
|
|
2799
|
+
const outcome: ExecutorCycleOutcome = {
|
|
2800
|
+
results: rerunResults,
|
|
2801
|
+
testResults: context.reporterResults.testResults.filter((result) =>
|
|
2802
|
+
rerunPathSet.has(result.testPath),
|
|
2803
|
+
),
|
|
2804
|
+
errors: unhandledErrors ?? [],
|
|
2805
|
+
testPaths: rerunTestPaths,
|
|
2806
|
+
duration: {
|
|
2807
|
+
buildTime: drainPendingBuildTime(watchState),
|
|
2808
|
+
testTime,
|
|
2809
|
+
},
|
|
2810
|
+
coverage: rerunCoverage ? { map: rerunCoverage } : undefined,
|
|
2811
|
+
resolveSourcemap: resolveBrowserSourcemap,
|
|
2812
|
+
};
|
|
2813
|
+
|
|
2814
|
+
await finalizeRunCycle(context, {
|
|
2815
|
+
outcomes: [outcome],
|
|
2816
|
+
mode: 'on-demand',
|
|
2817
|
+
isWatchMode: true,
|
|
2818
|
+
coverageProvider,
|
|
2819
|
+
reportOnFailure: coverageConfig?.reportOnFailure ?? false,
|
|
2820
|
+
});
|
|
2821
|
+
};
|
|
2544
2822
|
|
|
2545
2823
|
projectEntries = runtime.projectEntries;
|
|
2546
2824
|
totalTests = projectEntries.reduce(
|
|
@@ -2566,7 +2844,8 @@ export const runBrowserController = async (
|
|
|
2566
2844
|
};
|
|
2567
2845
|
}
|
|
2568
2846
|
|
|
2569
|
-
if (totalTests === 0
|
|
2847
|
+
if (totalTests === 0) {
|
|
2848
|
+
reportEmptyTestSet();
|
|
2570
2849
|
await destroyBrowserRuntime(runtime);
|
|
2571
2850
|
return allowEmptyRun ? createEmptyRunResult() : undefined;
|
|
2572
2851
|
}
|
|
@@ -2612,7 +2891,9 @@ export const runBrowserController = async (
|
|
|
2612
2891
|
rootPath: normalize(context.rootPath),
|
|
2613
2892
|
projects: projectRuntimeConfigs,
|
|
2614
2893
|
snapshot: {
|
|
2615
|
-
updateSnapshot:
|
|
2894
|
+
updateSnapshot:
|
|
2895
|
+
options?.updateSnapshot ??
|
|
2896
|
+
context.snapshotManager.options.updateSnapshot,
|
|
2616
2897
|
},
|
|
2617
2898
|
// Container origin (fallback). Per-project runner origins below.
|
|
2618
2899
|
runnerUrl: `http://localhost:${runtime.containerServer.port}`,
|
|
@@ -2728,22 +3009,18 @@ export const runBrowserController = async (
|
|
|
2728
3009
|
createRunnerEventSink(context, project.normalizedConfig),
|
|
2729
3010
|
]),
|
|
2730
3011
|
);
|
|
2731
|
-
|
|
2732
|
-
|
|
2733
|
-
//
|
|
2734
|
-
//
|
|
2735
|
-
//
|
|
2736
|
-
//
|
|
2737
|
-
|
|
2738
|
-
|
|
2739
|
-
|
|
2740
|
-
|
|
2741
|
-
|
|
2742
|
-
|
|
2743
|
-
|
|
2744
|
-
const sinkForTestPath = (testPath: string): RunnerEventSink => {
|
|
2745
|
-
const projectName = projectNameByTestPath.get(testPath);
|
|
2746
|
-
return projectName ? sinkForProjectName(projectName) : firstBrowserSink;
|
|
3012
|
+
// Every per-file wire payload carries its owning project name, so routing
|
|
3013
|
+
// never derives a project from a test path — concurrent projects can run the
|
|
3014
|
+
// same file, and a path-keyed lookup would attribute events to the wrong one.
|
|
3015
|
+
// The client resolves its project from the host's own manifest, so a miss is
|
|
3016
|
+
// a protocol bug; fail loudly rather than route through another project's
|
|
3017
|
+
// config.
|
|
3018
|
+
const sinkForProjectName = (projectName: string): RunnerEventSink => {
|
|
3019
|
+
const sink = runnerSinks.get(projectName);
|
|
3020
|
+
if (!sink) {
|
|
3021
|
+
throw new Error(`No runner event sink for project "${projectName}"`);
|
|
3022
|
+
}
|
|
3023
|
+
return sink;
|
|
2747
3024
|
};
|
|
2748
3025
|
|
|
2749
3026
|
// Silent-console buffering runs through the shared controller — the same
|
|
@@ -2760,7 +3037,7 @@ export const runBrowserController = async (
|
|
|
2760
3037
|
disableConsoleIntercept: context.normalizedConfig.disableConsoleIntercept,
|
|
2761
3038
|
},
|
|
2762
3039
|
emitInterceptedLog: (log) =>
|
|
2763
|
-
|
|
3040
|
+
sinkForProjectName(log.project).onConsoleLog(log),
|
|
2764
3041
|
writeOriginalLog: () => {},
|
|
2765
3042
|
});
|
|
2766
3043
|
|
|
@@ -2795,7 +3072,6 @@ export const runBrowserController = async (
|
|
|
2795
3072
|
const handleTestFileStart = async (
|
|
2796
3073
|
payload: TestFileStartPayload,
|
|
2797
3074
|
): Promise<void> => {
|
|
2798
|
-
projectNameByTestPath.set(payload.testPath, payload.projectName);
|
|
2799
3075
|
if (phaseTrackers) {
|
|
2800
3076
|
const tracker = new PhaseTracker({
|
|
2801
3077
|
trace: {
|
|
@@ -2805,13 +3081,17 @@ export const runBrowserController = async (
|
|
|
2805
3081
|
pid: nextBrowserFilePid++,
|
|
2806
3082
|
});
|
|
2807
3083
|
tracker.transition('prepare');
|
|
2808
|
-
phaseTrackers.set(
|
|
3084
|
+
phaseTrackers.set(
|
|
3085
|
+
trackerKey(payload.projectName, payload.testPath),
|
|
3086
|
+
tracker,
|
|
3087
|
+
);
|
|
2809
3088
|
}
|
|
2810
3089
|
// The client sends `{ testPath, projectName }`; the sink adapter builds the
|
|
2811
3090
|
// `TestFileInfo` the reporters and stateManager expect.
|
|
2812
3091
|
await sinkForProjectName(payload.projectName).onTestFileStart({
|
|
2813
3092
|
testId: getFileTaskId(payload.testPath),
|
|
2814
3093
|
testPath: payload.testPath,
|
|
3094
|
+
project: payload.projectName,
|
|
2815
3095
|
tests: [],
|
|
2816
3096
|
});
|
|
2817
3097
|
};
|
|
@@ -2819,22 +3099,28 @@ export const runBrowserController = async (
|
|
|
2819
3099
|
const handleTestFileReady = async (
|
|
2820
3100
|
payload: TestFileReadyPayload,
|
|
2821
3101
|
): Promise<void> => {
|
|
2822
|
-
phaseTrackers
|
|
2823
|
-
|
|
3102
|
+
phaseTrackers
|
|
3103
|
+
?.get(trackerKey(payload.project, payload.testPath))
|
|
3104
|
+
?.transition('tests');
|
|
3105
|
+
await sinkForProjectName(payload.project).onTestFileReady(payload);
|
|
2824
3106
|
};
|
|
2825
3107
|
|
|
2826
3108
|
const handleTestSuiteStart = async (
|
|
2827
3109
|
payload: TestSuiteStartPayload,
|
|
2828
3110
|
): Promise<void> => {
|
|
2829
|
-
phaseTrackers
|
|
2830
|
-
|
|
3111
|
+
phaseTrackers
|
|
3112
|
+
?.get(trackerKey(payload.project, payload.testPath))
|
|
3113
|
+
?.recordSuiteStart(payload);
|
|
3114
|
+
await sinkForProjectName(payload.project).onTestSuiteStart(payload);
|
|
2831
3115
|
};
|
|
2832
3116
|
|
|
2833
3117
|
const handleTestSuiteResult = async (
|
|
2834
3118
|
payload: TestSuiteResultPayload,
|
|
2835
3119
|
): Promise<void> => {
|
|
2836
|
-
phaseTrackers
|
|
2837
|
-
|
|
3120
|
+
phaseTrackers
|
|
3121
|
+
?.get(trackerKey(payload.project, payload.testPath))
|
|
3122
|
+
?.recordSuiteResult(payload);
|
|
3123
|
+
await sinkForProjectName(payload.project).onTestSuiteResult(payload);
|
|
2838
3124
|
|
|
2839
3125
|
if (context.normalizedConfig.silent === 'passed-only') {
|
|
2840
3126
|
silentConsoleController.flushBufferedLogsForTask({
|
|
@@ -2850,15 +3136,19 @@ export const runBrowserController = async (
|
|
|
2850
3136
|
const handleTestCaseStart = async (
|
|
2851
3137
|
payload: TestCaseStartPayload,
|
|
2852
3138
|
): Promise<void> => {
|
|
2853
|
-
phaseTrackers
|
|
3139
|
+
phaseTrackers
|
|
3140
|
+
?.get(trackerKey(payload.project, payload.testPath))
|
|
3141
|
+
?.recordCaseStart(payload);
|
|
2854
3142
|
// Fire-and-forget on both transports (the sink does not await case-start).
|
|
2855
|
-
|
|
3143
|
+
sinkForProjectName(payload.project).onTestCaseStart(payload);
|
|
2856
3144
|
};
|
|
2857
3145
|
|
|
2858
3146
|
const handleTestCaseResult = async (payload: TestResult): Promise<void> => {
|
|
2859
3147
|
caseResults.push(payload);
|
|
2860
|
-
phaseTrackers
|
|
2861
|
-
|
|
3148
|
+
phaseTrackers
|
|
3149
|
+
?.get(trackerKey(payload.project, payload.testPath))
|
|
3150
|
+
?.recordCaseResult(payload);
|
|
3151
|
+
await sinkForProjectName(payload.project).onTestCaseResult(payload);
|
|
2862
3152
|
|
|
2863
3153
|
if (context.normalizedConfig.silent === 'passed-only') {
|
|
2864
3154
|
silentConsoleController.flushBufferedLogsForTask({
|
|
@@ -2878,12 +3168,13 @@ export const runBrowserController = async (
|
|
|
2878
3168
|
context.updateReporterResultState([payload], payload.results);
|
|
2879
3169
|
|
|
2880
3170
|
if (phaseTrackers) {
|
|
2881
|
-
const
|
|
3171
|
+
const key = trackerKey(payload.project, payload.testPath);
|
|
3172
|
+
const tracker = phaseTrackers.get(key);
|
|
2882
3173
|
if (tracker) {
|
|
2883
3174
|
tracker.end();
|
|
2884
3175
|
const events = tracker.getTraceEvents();
|
|
2885
3176
|
if (events) onTraceEvents?.(events);
|
|
2886
|
-
phaseTrackers.delete(
|
|
3177
|
+
phaseTrackers.delete(key);
|
|
2887
3178
|
}
|
|
2888
3179
|
}
|
|
2889
3180
|
|
|
@@ -2899,7 +3190,7 @@ export const runBrowserController = async (
|
|
|
2899
3190
|
|
|
2900
3191
|
// Feeds stateManager, fans out onTestFileResult to reporters, and ingests
|
|
2901
3192
|
// payload.snapshotResult (the snapshotManager.add moved into the sink).
|
|
2902
|
-
await
|
|
3193
|
+
await sinkForProjectName(payload.project).onTestFileResult(payload);
|
|
2903
3194
|
// In non-watch runs core owns the exit code via `finalizeRunCycle` (the
|
|
2904
3195
|
// failing file rides the returned outcome); watch reruns set it here.
|
|
2905
3196
|
if (isWatchMode && payload.status === 'fail') {
|
|
@@ -2917,6 +3208,7 @@ export const runBrowserController = async (
|
|
|
2917
3208
|
taskParentNames: payload.taskParentNames,
|
|
2918
3209
|
taskType: payload.taskType,
|
|
2919
3210
|
testPath: payload.testPath,
|
|
3211
|
+
project: payload.projectName,
|
|
2920
3212
|
type: payload.type,
|
|
2921
3213
|
trace: payload.trace,
|
|
2922
3214
|
};
|
|
@@ -3174,6 +3466,12 @@ export const runBrowserController = async (
|
|
|
3174
3466
|
|
|
3175
3467
|
const inlineOptions: BrowserHostConfig = {
|
|
3176
3468
|
...hostOptions,
|
|
3469
|
+
// Read live per page load, not from the construction-time
|
|
3470
|
+
// `hostOptions` value: the 'u' shortcut flips
|
|
3471
|
+
// `snapshotManager.options` between reruns.
|
|
3472
|
+
snapshot: {
|
|
3473
|
+
updateSnapshot: context.snapshotManager.options.updateSnapshot,
|
|
3474
|
+
},
|
|
3177
3475
|
testFile: file.testPath,
|
|
3178
3476
|
runId: `${run.token}:${session.id}`,
|
|
3179
3477
|
};
|
|
@@ -3343,20 +3641,16 @@ export const runBrowserController = async (
|
|
|
3343
3641
|
fatalError && fatalError !== fatalErrorBeforeRun
|
|
3344
3642
|
? fatalError
|
|
3345
3643
|
: undefined;
|
|
3346
|
-
await
|
|
3347
|
-
|
|
3348
|
-
|
|
3349
|
-
buildTime: 0,
|
|
3350
|
-
testTime,
|
|
3351
|
-
},
|
|
3352
|
-
filterRerunTestPaths: files.map((file) => file.testPath),
|
|
3644
|
+
await finalizeWatchRerun({
|
|
3645
|
+
rerunTestPaths: files.map((file) => file.testPath),
|
|
3646
|
+
testTime,
|
|
3353
3647
|
unhandledErrors: rerunError
|
|
3354
3648
|
? [rerunError]
|
|
3355
3649
|
: rerunFatalError
|
|
3356
3650
|
? [rerunFatalError]
|
|
3357
3651
|
: undefined,
|
|
3358
3652
|
});
|
|
3359
|
-
|
|
3653
|
+
logWatchReadyMessage(context, enableCliShortcuts);
|
|
3360
3654
|
}
|
|
3361
3655
|
},
|
|
3362
3656
|
onError: async (error) => {
|
|
@@ -3373,68 +3667,7 @@ export const runBrowserController = async (
|
|
|
3373
3667
|
},
|
|
3374
3668
|
});
|
|
3375
3669
|
|
|
3376
|
-
|
|
3377
|
-
const duration = {
|
|
3378
|
-
totalTime: buildTime,
|
|
3379
|
-
buildTime,
|
|
3380
|
-
testTime: 0,
|
|
3381
|
-
};
|
|
3382
|
-
const result = {
|
|
3383
|
-
results: reporterResults,
|
|
3384
|
-
testResults: caseResults,
|
|
3385
|
-
duration,
|
|
3386
|
-
hasFailure: false,
|
|
3387
|
-
getSourcemap: getBrowserSourcemap,
|
|
3388
|
-
resolveSourcemap: resolveBrowserSourcemap,
|
|
3389
|
-
close: !isWatchMode
|
|
3390
|
-
? async () => {
|
|
3391
|
-
sessionRegistry.clear();
|
|
3392
|
-
await destroyBrowserRuntime(runtime);
|
|
3393
|
-
}
|
|
3394
|
-
: undefined,
|
|
3395
|
-
};
|
|
3396
|
-
|
|
3397
|
-
if (isWatchMode) {
|
|
3398
|
-
await notifyTestRunEnd({ duration });
|
|
3399
|
-
}
|
|
3400
|
-
|
|
3401
|
-
if (isWatchMode) {
|
|
3402
|
-
triggerRerun = async () => {
|
|
3403
|
-
const newProjectEntries = await collectProjectEntries(context);
|
|
3404
|
-
const rerunPlan = planWatchRerun({
|
|
3405
|
-
projectEntries: newProjectEntries,
|
|
3406
|
-
previousTestFiles: watchContext.lastTestFiles,
|
|
3407
|
-
affectedTestFiles: watchContext.affectedTestFiles,
|
|
3408
|
-
});
|
|
3409
|
-
watchContext.affectedTestFiles = [];
|
|
3410
|
-
|
|
3411
|
-
if (rerunPlan.filesChanged) {
|
|
3412
|
-
watchContext.lastTestFiles = rerunPlan.currentTestFiles;
|
|
3413
|
-
if (rerunPlan.currentTestFiles.length === 0) {
|
|
3414
|
-
logger.log(
|
|
3415
|
-
color.cyan('No browser test files remain after update.\n'),
|
|
3416
|
-
);
|
|
3417
|
-
logBrowserWatchReadyMessage(enableCliShortcuts);
|
|
3418
|
-
return;
|
|
3419
|
-
}
|
|
3420
|
-
|
|
3421
|
-
logger.log(
|
|
3422
|
-
color.cyan(
|
|
3423
|
-
`Test file set changed, re-running ${rerunPlan.currentTestFiles.length} file(s)...\n`,
|
|
3424
|
-
),
|
|
3425
|
-
);
|
|
3426
|
-
void latestRerunScheduler.enqueueLatest(rerunPlan.currentTestFiles);
|
|
3427
|
-
return;
|
|
3428
|
-
}
|
|
3429
|
-
|
|
3430
|
-
logBrowserWatchReadyMessage(enableCliShortcuts);
|
|
3431
|
-
};
|
|
3432
|
-
watchContext.hooksEnabled = true;
|
|
3433
|
-
logBrowserWatchReadyMessage(enableCliShortcuts);
|
|
3434
|
-
}
|
|
3435
|
-
|
|
3436
|
-
return result;
|
|
3437
|
-
}
|
|
3670
|
+
awaitHeadlessRerunIdle = () => latestRerunScheduler.whenIdle();
|
|
3438
3671
|
|
|
3439
3672
|
const testStart = Date.now();
|
|
3440
3673
|
await runFilesWithPool(allTestFiles);
|
|
@@ -3445,26 +3678,25 @@ export const runBrowserController = async (
|
|
|
3445
3678
|
const newProjectEntries = await collectProjectEntries(context);
|
|
3446
3679
|
const rerunPlan = planWatchRerun({
|
|
3447
3680
|
projectEntries: newProjectEntries,
|
|
3448
|
-
previousTestFiles:
|
|
3449
|
-
affectedTestFiles:
|
|
3681
|
+
previousTestFiles: watchState.lastTestFiles,
|
|
3682
|
+
affectedTestFiles: drainPendingAffectedTestFiles(watchState),
|
|
3450
3683
|
});
|
|
3451
|
-
watchContext.affectedTestFiles = [];
|
|
3452
3684
|
|
|
3453
3685
|
if (rerunPlan.filesChanged) {
|
|
3454
3686
|
const deletedTestPaths = collectDeletedTestPaths(
|
|
3455
|
-
|
|
3687
|
+
watchState.lastTestFiles,
|
|
3456
3688
|
rerunPlan.currentTestFiles,
|
|
3457
3689
|
);
|
|
3458
3690
|
if (deletedTestPaths.length > 0) {
|
|
3459
3691
|
context.updateReporterResultState([], [], deletedTestPaths);
|
|
3460
3692
|
}
|
|
3461
|
-
|
|
3693
|
+
watchState.lastTestFiles = rerunPlan.currentTestFiles;
|
|
3462
3694
|
if (rerunPlan.currentTestFiles.length === 0) {
|
|
3463
3695
|
await latestRerunScheduler.enqueueLatest([]);
|
|
3464
3696
|
logger.log(
|
|
3465
3697
|
color.cyan('No browser test files remain after update.\n'),
|
|
3466
3698
|
);
|
|
3467
|
-
|
|
3699
|
+
logWatchReadyMessage(context, enableCliShortcuts);
|
|
3468
3700
|
return;
|
|
3469
3701
|
}
|
|
3470
3702
|
|
|
@@ -3473,7 +3705,7 @@ export const runBrowserController = async (
|
|
|
3473
3705
|
`Test file set changed, re-running ${rerunPlan.currentTestFiles.length} file(s)...\n`,
|
|
3474
3706
|
),
|
|
3475
3707
|
);
|
|
3476
|
-
|
|
3708
|
+
await latestRerunScheduler.enqueueLatest(rerunPlan.currentTestFiles);
|
|
3477
3709
|
return;
|
|
3478
3710
|
}
|
|
3479
3711
|
|
|
@@ -3483,7 +3715,7 @@ export const runBrowserController = async (
|
|
|
3483
3715
|
'No affected browser test files detected, skipping re-run.\n',
|
|
3484
3716
|
),
|
|
3485
3717
|
);
|
|
3486
|
-
|
|
3718
|
+
logWatchReadyMessage(context, enableCliShortcuts);
|
|
3487
3719
|
return;
|
|
3488
3720
|
}
|
|
3489
3721
|
|
|
@@ -3492,7 +3724,7 @@ export const runBrowserController = async (
|
|
|
3492
3724
|
`Re-running ${rerunPlan.affectedTestFiles.length} affected test file(s)...\n`,
|
|
3493
3725
|
),
|
|
3494
3726
|
);
|
|
3495
|
-
|
|
3727
|
+
await latestRerunScheduler.enqueueLatest(rerunPlan.affectedTestFiles);
|
|
3496
3728
|
};
|
|
3497
3729
|
}
|
|
3498
3730
|
|
|
@@ -3524,6 +3756,15 @@ export const runBrowserController = async (
|
|
|
3524
3756
|
ensureProcessExitCode(1);
|
|
3525
3757
|
}
|
|
3526
3758
|
|
|
3759
|
+
// Fold and strip the initial cycle's coverage (the same per-cycle fold
|
|
3760
|
+
// reruns get in `finalizeWatchRerun`); the map rides on the result so the
|
|
3761
|
+
// browser-only watch path can report it without re-merging. Non-watch runs
|
|
3762
|
+
// keep `result.coverage` intact for the executor's outcome fold.
|
|
3763
|
+
const cycleCoverageMap =
|
|
3764
|
+
isWatchMode && coverageProvider
|
|
3765
|
+
? buildBrowserCoverageMap(reporterResults, coverageProvider)
|
|
3766
|
+
: undefined;
|
|
3767
|
+
|
|
3527
3768
|
const result = {
|
|
3528
3769
|
results: reporterResults,
|
|
3529
3770
|
testResults: caseResults,
|
|
@@ -3534,19 +3775,26 @@ export const runBrowserController = async (
|
|
|
3534
3775
|
// `closeHeadlessRuntime` is already `undefined` in watch mode, so the
|
|
3535
3776
|
// non-watch caller (core) receives the deferred close and watch does not.
|
|
3536
3777
|
close: closeHeadlessRuntime,
|
|
3778
|
+
coverage: cycleCoverageMap,
|
|
3779
|
+
watch: watchHandles,
|
|
3537
3780
|
};
|
|
3538
3781
|
|
|
3539
3782
|
if (isWatchMode) {
|
|
3540
3783
|
try {
|
|
3541
|
-
await notifyTestRunEnd({
|
|
3784
|
+
await notifyTestRunEnd({
|
|
3785
|
+
duration,
|
|
3786
|
+
coverage: cycleCoverageMap?.files().length
|
|
3787
|
+
? cycleCoverageMap.toJSON()
|
|
3788
|
+
: undefined,
|
|
3789
|
+
});
|
|
3542
3790
|
} finally {
|
|
3543
3791
|
await closeHeadlessRuntime?.();
|
|
3544
3792
|
}
|
|
3545
3793
|
}
|
|
3546
3794
|
|
|
3547
3795
|
if (isWatchMode && triggerRerun) {
|
|
3548
|
-
|
|
3549
|
-
|
|
3796
|
+
watchState.hooksEnabled = true;
|
|
3797
|
+
logWatchReadyMessage(context, enableCliShortcuts);
|
|
3550
3798
|
}
|
|
3551
3799
|
|
|
3552
3800
|
return result;
|
|
@@ -3924,30 +4172,43 @@ export const runBrowserController = async (
|
|
|
3924
4172
|
// Define rerun logic for watch mode
|
|
3925
4173
|
if (isWatchMode) {
|
|
3926
4174
|
triggerRerun = async () => {
|
|
3927
|
-
|
|
4175
|
+
// Re-deliver the host config so runner iframes reloaded by this rerun
|
|
4176
|
+
// observe live per-rerun values ('u' flips updateSnapshot between
|
|
4177
|
+
// reruns); `setContainerOptions` keeps full container reloads in sync.
|
|
4178
|
+
const refreshedHostOptions: BrowserHostConfig = {
|
|
4179
|
+
...hostOptions,
|
|
4180
|
+
snapshot: {
|
|
4181
|
+
updateSnapshot: context.snapshotManager.options.updateSnapshot,
|
|
4182
|
+
},
|
|
4183
|
+
};
|
|
4184
|
+
runtime.setContainerOptions(refreshedHostOptions);
|
|
4185
|
+
// Independent: config push to the container vs. local entry collection.
|
|
4186
|
+
const [, newProjectEntries] = await Promise.all([
|
|
4187
|
+
rpcManager.updateHostConfig(refreshedHostOptions),
|
|
4188
|
+
collectProjectEntries(context),
|
|
4189
|
+
]);
|
|
3928
4190
|
const rerunPlan = planWatchRerun({
|
|
3929
4191
|
projectEntries: newProjectEntries,
|
|
3930
|
-
previousTestFiles:
|
|
3931
|
-
affectedTestFiles:
|
|
4192
|
+
previousTestFiles: watchState.lastTestFiles,
|
|
4193
|
+
affectedTestFiles: drainPendingAffectedTestFiles(watchState),
|
|
3932
4194
|
});
|
|
3933
|
-
watchContext.affectedTestFiles = [];
|
|
3934
4195
|
|
|
3935
4196
|
if (rerunPlan.filesChanged) {
|
|
3936
4197
|
const deletedTestPaths = collectDeletedTestPaths(
|
|
3937
|
-
|
|
4198
|
+
watchState.lastTestFiles,
|
|
3938
4199
|
rerunPlan.currentTestFiles,
|
|
3939
4200
|
);
|
|
3940
4201
|
if (deletedTestPaths.length > 0) {
|
|
3941
4202
|
context.updateReporterResultState([], [], deletedTestPaths);
|
|
3942
4203
|
}
|
|
3943
|
-
|
|
4204
|
+
watchState.lastTestFiles = rerunPlan.currentTestFiles;
|
|
3944
4205
|
currentTestFiles = rerunPlan.currentTestFiles;
|
|
3945
4206
|
await rpcManager.notifyTestFileUpdate(currentTestFiles);
|
|
3946
4207
|
if (currentTestFiles.length === 0) {
|
|
3947
4208
|
logger.log(
|
|
3948
4209
|
color.cyan('No browser test files remain after update.\n'),
|
|
3949
4210
|
);
|
|
3950
|
-
|
|
4211
|
+
logWatchReadyMessage(context, enableCliShortcuts);
|
|
3951
4212
|
return;
|
|
3952
4213
|
}
|
|
3953
4214
|
await waitForRunnerFramesReady(
|
|
@@ -3984,26 +4245,22 @@ export const runBrowserController = async (
|
|
|
3984
4245
|
fatalError && fatalError !== fatalErrorBeforeRun
|
|
3985
4246
|
? fatalError
|
|
3986
4247
|
: undefined;
|
|
3987
|
-
await
|
|
3988
|
-
|
|
3989
|
-
|
|
3990
|
-
buildTime: 0,
|
|
3991
|
-
testTime,
|
|
3992
|
-
},
|
|
3993
|
-
filterRerunTestPaths: rerunPlan.normalizedAffectedTestFiles,
|
|
4248
|
+
await finalizeWatchRerun({
|
|
4249
|
+
rerunTestPaths: rerunPlan.normalizedAffectedTestFiles,
|
|
4250
|
+
testTime,
|
|
3994
4251
|
unhandledErrors: rerunError
|
|
3995
4252
|
? [rerunError]
|
|
3996
4253
|
: rerunFatalError
|
|
3997
4254
|
? [rerunFatalError]
|
|
3998
4255
|
: undefined,
|
|
3999
4256
|
});
|
|
4000
|
-
|
|
4257
|
+
logWatchReadyMessage(context, enableCliShortcuts);
|
|
4001
4258
|
}
|
|
4002
4259
|
} else if (!rerunPlan.filesChanged) {
|
|
4003
4260
|
logger.log(color.cyan('Tests will be re-executed automatically\n'));
|
|
4004
|
-
|
|
4261
|
+
logWatchReadyMessage(context, enableCliShortcuts);
|
|
4005
4262
|
} else {
|
|
4006
|
-
|
|
4263
|
+
logWatchReadyMessage(context, enableCliShortcuts);
|
|
4007
4264
|
}
|
|
4008
4265
|
};
|
|
4009
4266
|
}
|
|
@@ -4045,6 +4302,12 @@ export const runBrowserController = async (
|
|
|
4045
4302
|
ensureProcessExitCode(1);
|
|
4046
4303
|
}
|
|
4047
4304
|
|
|
4305
|
+
// Same per-cycle fold-and-strip as the headless path above.
|
|
4306
|
+
const cycleCoverageMap =
|
|
4307
|
+
isWatchMode && coverageProvider
|
|
4308
|
+
? buildBrowserCoverageMap(reporterResults, coverageProvider)
|
|
4309
|
+
: undefined;
|
|
4310
|
+
|
|
4048
4311
|
const result = {
|
|
4049
4312
|
results: reporterResults,
|
|
4050
4313
|
testResults: caseResults,
|
|
@@ -4055,11 +4318,18 @@ export const runBrowserController = async (
|
|
|
4055
4318
|
// `closeContainerRuntime` is already `undefined` in watch mode, so the
|
|
4056
4319
|
// non-watch caller (core) receives the deferred close and watch does not.
|
|
4057
4320
|
close: closeContainerRuntime,
|
|
4321
|
+
coverage: cycleCoverageMap,
|
|
4322
|
+
watch: watchHandles,
|
|
4058
4323
|
};
|
|
4059
4324
|
|
|
4060
4325
|
if (isWatchMode) {
|
|
4061
4326
|
try {
|
|
4062
|
-
await notifyTestRunEnd({
|
|
4327
|
+
await notifyTestRunEnd({
|
|
4328
|
+
duration,
|
|
4329
|
+
coverage: cycleCoverageMap?.files().length
|
|
4330
|
+
? cycleCoverageMap.toJSON()
|
|
4331
|
+
: undefined,
|
|
4332
|
+
});
|
|
4063
4333
|
} finally {
|
|
4064
4334
|
await closeContainerRuntime?.();
|
|
4065
4335
|
}
|
|
@@ -4067,8 +4337,8 @@ export const runBrowserController = async (
|
|
|
4067
4337
|
|
|
4068
4338
|
// Enable watch hooks AFTER initial test run to avoid duplicate runs
|
|
4069
4339
|
if (isWatchMode && triggerRerun) {
|
|
4070
|
-
|
|
4071
|
-
|
|
4340
|
+
watchState.hooksEnabled = true;
|
|
4341
|
+
logWatchReadyMessage(context, enableCliShortcuts);
|
|
4072
4342
|
}
|
|
4073
4343
|
|
|
4074
4344
|
return result;
|