@rstest/browser 0.11.3 → 0.11.4
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/{949.7d935e2ac9.js → 243.a8eed2b9e7.js} +223 -223
- package/dist/browser-container/container-static/js/243.a8eed2b9e7.js.LICENSE.txt +1 -0
- package/dist/browser-container/container-static/js/{index.492a6c2f8b.js → index.84aaafaf21.js} +78 -73
- package/dist/browser-container/index.html +1 -1
- package/dist/index.js +232 -139
- package/package.json +7 -6
- package/src/browserExecutor.ts +5 -3
- package/src/configValidation.ts +6 -0
- package/src/hostController.ts +426 -116
- package/dist/browser-container/container-static/js/949.7d935e2ac9.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
|
-
`[Watch] Chunk hash changed for ${chunkKey}: ${prevHash} -> ${chunk.hash} (test: ${testFile})`,
|
|
981
|
-
);
|
|
1054
|
+
const setupFile = findTestFileInModules(chunk.modules, setupFiles);
|
|
1055
|
+
if (setupFile) {
|
|
1056
|
+
recordChunk(setupHashes, setupFile, chunkKey, chunk.hash);
|
|
982
1057
|
}
|
|
983
1058
|
}
|
|
984
1059
|
|
|
985
|
-
|
|
986
|
-
|
|
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
|
+
}
|
|
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);
|
|
1086
|
+
}
|
|
1087
|
+
|
|
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
|
|
|
@@ -1822,8 +1938,35 @@ const createBrowserRuntime = async ({
|
|
|
1822
1938
|
sourceMap: {
|
|
1823
1939
|
js: 'source-map',
|
|
1824
1940
|
},
|
|
1941
|
+
// Every project server compiles the same asset names
|
|
1942
|
+
// (`static/js/runner.js`, ...). With `dev.writeToDisk`
|
|
1943
|
+
// (debug mode) the middleware serves from disk, so a
|
|
1944
|
+
// shared dist dir would be last-writer-wins and one
|
|
1945
|
+
// project's server would deliver another project's
|
|
1946
|
+
// bundle — keep each project's output isolated, inside
|
|
1947
|
+
// the run's temp dir so teardown removes it.
|
|
1948
|
+
distPath: {
|
|
1949
|
+
root: join(
|
|
1950
|
+
tempDir,
|
|
1951
|
+
'server',
|
|
1952
|
+
toSafeVarName(project.environmentName),
|
|
1953
|
+
),
|
|
1954
|
+
},
|
|
1825
1955
|
},
|
|
1826
1956
|
tools: {
|
|
1957
|
+
swc: (swcConfig) => {
|
|
1958
|
+
// Fixture dependency discovery reads callback parameters
|
|
1959
|
+
// through Function#toString(). Playwright's supported
|
|
1960
|
+
// browsers all support parameter destructuring, so keep
|
|
1961
|
+
// that syntax intact in the browser test bundle.
|
|
1962
|
+
swcConfig.env ??= {};
|
|
1963
|
+
swcConfig.env.exclude = Array.from(
|
|
1964
|
+
new Set([
|
|
1965
|
+
...(swcConfig.env.exclude ?? []),
|
|
1966
|
+
'transform-parameters',
|
|
1967
|
+
]),
|
|
1968
|
+
);
|
|
1969
|
+
},
|
|
1827
1970
|
rspack: (rspackConfig) => {
|
|
1828
1971
|
rspackConfig.mode = 'development';
|
|
1829
1972
|
// Web parameterization of the node mock transform:
|
|
@@ -1893,37 +2036,80 @@ const createBrowserRuntime = async ({
|
|
|
1893
2036
|
name: 'rstest:browser-watch',
|
|
1894
2037
|
setup(api) {
|
|
1895
2038
|
api.onBeforeDevCompile(() => {
|
|
1896
|
-
|
|
2039
|
+
watchState.compileStartTimes.set(project.name, Date.now());
|
|
2040
|
+
if (!watchState.hooksEnabled) {
|
|
1897
2041
|
return;
|
|
1898
2042
|
}
|
|
1899
2043
|
logger.log(color.cyan('\nFile changed, re-running tests...\n'));
|
|
1900
2044
|
});
|
|
1901
2045
|
|
|
1902
2046
|
api.onAfterDevCompile(async ({ stats }) => {
|
|
2047
|
+
const compileStart = watchState.compileStartTimes.get(
|
|
2048
|
+
project.name,
|
|
2049
|
+
);
|
|
2050
|
+
if (compileStart !== undefined) {
|
|
2051
|
+
watchState.compileStartTimes.delete(project.name);
|
|
2052
|
+
// Only change-triggered compiles feed the pending rerun's
|
|
2053
|
+
// build phase (the initial build is the initial run's
|
|
2054
|
+
// buildTime). Parallel project compiles overlap; the longest
|
|
2055
|
+
// one bounds the rerun's build phase.
|
|
2056
|
+
if (watchState.hooksEnabled) {
|
|
2057
|
+
watchState.pendingBuildTimeMs = Math.max(
|
|
2058
|
+
watchState.pendingBuildTimeMs,
|
|
2059
|
+
Date.now() - compileStart,
|
|
2060
|
+
);
|
|
2061
|
+
}
|
|
2062
|
+
}
|
|
1903
2063
|
// Collect hashes even during initial build to establish baseline
|
|
1904
2064
|
if (stats) {
|
|
1905
|
-
|
|
2065
|
+
// This compiler only ever holds this project's entries; the
|
|
2066
|
+
// diff baseline is keyed per project accordingly.
|
|
2067
|
+
const [projectEntry] = await collectProjectEntries(context, [
|
|
2068
|
+
project,
|
|
2069
|
+
]);
|
|
1906
2070
|
const entryTestFiles = new Set<string>(
|
|
1907
|
-
collectWatchTestFiles(
|
|
2071
|
+
collectWatchTestFiles(projectEntry ? [projectEntry] : []).map(
|
|
1908
2072
|
(file) => file.testPath,
|
|
1909
2073
|
),
|
|
1910
2074
|
);
|
|
2075
|
+
const setupFiles = new Set<string>(
|
|
2076
|
+
(projectEntry?.setupFiles ?? []).map((file) =>
|
|
2077
|
+
normalize(file),
|
|
2078
|
+
),
|
|
2079
|
+
);
|
|
2080
|
+
|
|
2081
|
+
let state = watchState.invalidation.get(project.name);
|
|
2082
|
+
if (!state) {
|
|
2083
|
+
state = {};
|
|
2084
|
+
watchState.invalidation.set(project.name, state);
|
|
2085
|
+
}
|
|
1911
2086
|
|
|
1912
2087
|
const statsJson = stats.toJson({ all: true });
|
|
1913
|
-
const affected = getAffectedTestFiles(
|
|
1914
|
-
statsJson.chunks,
|
|
2088
|
+
const affected = getAffectedTestFiles({
|
|
2089
|
+
chunks: statsJson.chunks,
|
|
1915
2090
|
entryTestFiles,
|
|
1916
|
-
|
|
1917
|
-
|
|
2091
|
+
setupFiles,
|
|
2092
|
+
state,
|
|
2093
|
+
});
|
|
1918
2094
|
|
|
1919
2095
|
if (affected.length > 0) {
|
|
2096
|
+
const pending =
|
|
2097
|
+
watchState.pendingAffectedTestFiles.get(project.name) ??
|
|
2098
|
+
new Set<string>();
|
|
2099
|
+
for (const file of affected) {
|
|
2100
|
+
pending.add(file);
|
|
2101
|
+
}
|
|
2102
|
+
watchState.pendingAffectedTestFiles.set(
|
|
2103
|
+
project.name,
|
|
2104
|
+
pending,
|
|
2105
|
+
);
|
|
1920
2106
|
logger.debug(
|
|
1921
2107
|
`[Watch] Affected test files: ${affected.join(', ')}`,
|
|
1922
2108
|
);
|
|
1923
2109
|
}
|
|
1924
2110
|
}
|
|
1925
2111
|
|
|
1926
|
-
if (!
|
|
2112
|
+
if (!watchState.hooksEnabled) {
|
|
1927
2113
|
return;
|
|
1928
2114
|
}
|
|
1929
2115
|
|
|
@@ -1965,6 +2151,15 @@ const createBrowserRuntime = async ({
|
|
|
1965
2151
|
if (isDebug()) {
|
|
1966
2152
|
await rsbuildInstance.inspectConfig({
|
|
1967
2153
|
writeToDisk: true,
|
|
2154
|
+
// The server's own distPath is isolated per project inside the run's
|
|
2155
|
+
// temp dir (removed at teardown); keep the debug artifacts at the
|
|
2156
|
+
// project's stable dist root so they survive the run and stay where
|
|
2157
|
+
// the docs point users to.
|
|
2158
|
+
outputPath: resolve(
|
|
2159
|
+
context.rootPath,
|
|
2160
|
+
context.normalizedConfig.output.distPath.root,
|
|
2161
|
+
'.rsbuild',
|
|
2162
|
+
),
|
|
1968
2163
|
extraConfigs: {
|
|
1969
2164
|
rstest: {
|
|
1970
2165
|
...context.normalizedConfig,
|
|
@@ -2114,6 +2309,7 @@ const createBrowserRuntime = async ({
|
|
|
2114
2309
|
dispatchHandlers,
|
|
2115
2310
|
wss,
|
|
2116
2311
|
projectEntries,
|
|
2312
|
+
watchState,
|
|
2117
2313
|
};
|
|
2118
2314
|
} catch (error) {
|
|
2119
2315
|
wss.close();
|
|
@@ -2479,7 +2675,7 @@ export const runBrowserController = async (
|
|
|
2479
2675
|
await notifyTestRunStart();
|
|
2480
2676
|
}
|
|
2481
2677
|
|
|
2482
|
-
const enableCliShortcuts = isWatchMode &&
|
|
2678
|
+
const enableCliShortcuts = isWatchMode && isTTY('stdin');
|
|
2483
2679
|
const browserTempOutputRoot = context.normalizedConfig.output.distPath.root;
|
|
2484
2680
|
const tempDir =
|
|
2485
2681
|
isWatchMode && watchContext.runtime
|
|
@@ -2493,15 +2689,13 @@ export const runBrowserController = async (
|
|
|
2493
2689
|
Date.now().toString(),
|
|
2494
2690
|
);
|
|
2495
2691
|
|
|
2496
|
-
// Track initial test files for watch mode
|
|
2497
|
-
if (isWatchMode) {
|
|
2498
|
-
watchContext.lastTestFiles = collectWatchTestFiles(projectEntries);
|
|
2499
|
-
}
|
|
2500
|
-
|
|
2501
2692
|
let runtime = isWatchMode ? watchContext.runtime : null;
|
|
2502
2693
|
|
|
2503
2694
|
// Define rerun callback for watch mode (will be populated later)
|
|
2504
2695
|
let triggerRerun: (() => Promise<void>) | undefined;
|
|
2696
|
+
// Headless reruns complete asynchronously in the scheduler's drain loop;
|
|
2697
|
+
// the watch handles await this so callers observe rerun completion.
|
|
2698
|
+
let awaitHeadlessRerunIdle: (() => Promise<void>) | undefined;
|
|
2505
2699
|
|
|
2506
2700
|
if (!runtime) {
|
|
2507
2701
|
try {
|
|
@@ -2532,15 +2726,113 @@ export const runBrowserController = async (
|
|
|
2532
2726
|
|
|
2533
2727
|
if (isWatchMode) {
|
|
2534
2728
|
watchContext.runtime = runtime;
|
|
2535
|
-
registerWatchCleanup();
|
|
2729
|
+
registerWatchCleanup(context.embedded);
|
|
2730
|
+
}
|
|
2731
|
+
}
|
|
2536
2732
|
|
|
2537
|
-
|
|
2538
|
-
|
|
2539
|
-
|
|
2540
|
-
|
|
2733
|
+
const watchState = runtime.watchState;
|
|
2734
|
+
|
|
2735
|
+
// Track initial test files for watch mode (from this controller's freshly
|
|
2736
|
+
// collected entries, before adopting the runtime's entry snapshot below).
|
|
2737
|
+
if (isWatchMode) {
|
|
2738
|
+
watchState.lastTestFiles = collectWatchTestFiles(projectEntries);
|
|
2739
|
+
}
|
|
2740
|
+
|
|
2741
|
+
// Mark files as pending-affected so the next `triggerRerun` reruns them
|
|
2742
|
+
// through the normal plan/schedule/finalize pipeline (used by the watch
|
|
2743
|
+
// handles' explicit reruns; omitted paths = all current files). Returns the
|
|
2744
|
+
// number of seeded files so callers can skip the rerun when a path-scoped
|
|
2745
|
+
// request matches no browser test file (mixed watch 'u' with node-only
|
|
2746
|
+
// snapshot updates).
|
|
2747
|
+
const seedPendingRerun = (testPaths?: string[]): number => {
|
|
2748
|
+
const wanted = testPaths
|
|
2749
|
+
? new Set(testPaths.map((testPath) => normalize(testPath)))
|
|
2750
|
+
: null;
|
|
2751
|
+
let seeded = 0;
|
|
2752
|
+
for (const file of watchState.lastTestFiles) {
|
|
2753
|
+
if (wanted && !wanted.has(file.testPath)) {
|
|
2754
|
+
continue;
|
|
2541
2755
|
}
|
|
2756
|
+
const pending =
|
|
2757
|
+
watchState.pendingAffectedTestFiles.get(file.projectName) ??
|
|
2758
|
+
new Set<string>();
|
|
2759
|
+
pending.add(file.testPath);
|
|
2760
|
+
watchState.pendingAffectedTestFiles.set(file.projectName, pending);
|
|
2761
|
+
seeded += 1;
|
|
2542
2762
|
}
|
|
2543
|
-
|
|
2763
|
+
return seeded;
|
|
2764
|
+
};
|
|
2765
|
+
|
|
2766
|
+
const watchHandles: BrowserWatchHandles | undefined = isWatchMode
|
|
2767
|
+
? {
|
|
2768
|
+
rerun: async (testPaths) => {
|
|
2769
|
+
const seeded = seedPendingRerun(testPaths);
|
|
2770
|
+
if (testPaths && seeded === 0) {
|
|
2771
|
+
return;
|
|
2772
|
+
}
|
|
2773
|
+
await triggerRerun?.();
|
|
2774
|
+
await awaitHeadlessRerunIdle?.();
|
|
2775
|
+
},
|
|
2776
|
+
close: cleanupWatchRuntime,
|
|
2777
|
+
}
|
|
2778
|
+
: undefined;
|
|
2779
|
+
|
|
2780
|
+
/**
|
|
2781
|
+
* Per-rerun finalize for watch mode: fold the rerun into a synthetic
|
|
2782
|
+
* `ExecutorCycleOutcome` and hand it to core's `finalizeRunCycle`, so
|
|
2783
|
+
* reporter payloads, exit-code never-downgrade semantics, and coverage
|
|
2784
|
+
* reports match the node watch cycle. The trace buffer stays session-owned
|
|
2785
|
+
* (no `traceRun` here); `buildTime` is the drained duration of the
|
|
2786
|
+
* change-triggered compile(s), not a hardcoded zero.
|
|
2787
|
+
*/
|
|
2788
|
+
const finalizeWatchRerun = async ({
|
|
2789
|
+
rerunTestPaths,
|
|
2790
|
+
testTime,
|
|
2791
|
+
unhandledErrors,
|
|
2792
|
+
}: {
|
|
2793
|
+
rerunTestPaths: string[];
|
|
2794
|
+
testTime: number;
|
|
2795
|
+
unhandledErrors?: Error[];
|
|
2796
|
+
}): Promise<void> => {
|
|
2797
|
+
const rerunPathSet = new Set(rerunTestPaths);
|
|
2798
|
+
// Reporter coverage spans the whole session (unaffected files keep their
|
|
2799
|
+
// last coverage), matching the previous self-finalize payload. The merge
|
|
2800
|
+
// must not strip `result.coverage`, or later reruns would lose it.
|
|
2801
|
+
let sessionCoverage: CoverageMapData | undefined;
|
|
2802
|
+
const coverageMap = buildBrowserCoverageMap(
|
|
2803
|
+
context.reporterResults.results,
|
|
2804
|
+
coverageProvider,
|
|
2805
|
+
{ keepResultCoverage: true },
|
|
2806
|
+
);
|
|
2807
|
+
if (coverageMap && coverageMap.files().length > 0) {
|
|
2808
|
+
sessionCoverage = coverageMap.toJSON();
|
|
2809
|
+
}
|
|
2810
|
+
|
|
2811
|
+
const outcome: ExecutorCycleOutcome = {
|
|
2812
|
+
results: context.reporterResults.results.filter((result) =>
|
|
2813
|
+
rerunPathSet.has(result.testPath),
|
|
2814
|
+
),
|
|
2815
|
+
testResults: context.reporterResults.testResults.filter((result) =>
|
|
2816
|
+
rerunPathSet.has(result.testPath),
|
|
2817
|
+
),
|
|
2818
|
+
errors: unhandledErrors ?? [],
|
|
2819
|
+
testPaths: rerunTestPaths,
|
|
2820
|
+
duration: {
|
|
2821
|
+
buildTime: drainPendingBuildTime(watchState),
|
|
2822
|
+
testTime,
|
|
2823
|
+
},
|
|
2824
|
+
coverage: sessionCoverage ? { map: sessionCoverage } : undefined,
|
|
2825
|
+
resolveSourcemap: resolveBrowserSourcemap,
|
|
2826
|
+
};
|
|
2827
|
+
|
|
2828
|
+
await finalizeRunCycle(context, {
|
|
2829
|
+
outcomes: [outcome],
|
|
2830
|
+
mode: 'on-demand',
|
|
2831
|
+
isWatchMode: true,
|
|
2832
|
+
coverageProvider,
|
|
2833
|
+
reportOnFailure: coverageConfig?.reportOnFailure ?? false,
|
|
2834
|
+
});
|
|
2835
|
+
};
|
|
2544
2836
|
|
|
2545
2837
|
projectEntries = runtime.projectEntries;
|
|
2546
2838
|
totalTests = projectEntries.reduce(
|
|
@@ -2612,7 +2904,9 @@ export const runBrowserController = async (
|
|
|
2612
2904
|
rootPath: normalize(context.rootPath),
|
|
2613
2905
|
projects: projectRuntimeConfigs,
|
|
2614
2906
|
snapshot: {
|
|
2615
|
-
updateSnapshot:
|
|
2907
|
+
updateSnapshot:
|
|
2908
|
+
options?.updateSnapshot ??
|
|
2909
|
+
context.snapshotManager.options.updateSnapshot,
|
|
2616
2910
|
},
|
|
2617
2911
|
// Container origin (fallback). Per-project runner origins below.
|
|
2618
2912
|
runnerUrl: `http://localhost:${runtime.containerServer.port}`,
|
|
@@ -3174,6 +3468,12 @@ export const runBrowserController = async (
|
|
|
3174
3468
|
|
|
3175
3469
|
const inlineOptions: BrowserHostConfig = {
|
|
3176
3470
|
...hostOptions,
|
|
3471
|
+
// Read live per page load, not from the construction-time
|
|
3472
|
+
// `hostOptions` value: the 'u' shortcut flips
|
|
3473
|
+
// `snapshotManager.options` between reruns.
|
|
3474
|
+
snapshot: {
|
|
3475
|
+
updateSnapshot: context.snapshotManager.options.updateSnapshot,
|
|
3476
|
+
},
|
|
3177
3477
|
testFile: file.testPath,
|
|
3178
3478
|
runId: `${run.token}:${session.id}`,
|
|
3179
3479
|
};
|
|
@@ -3343,20 +3643,16 @@ export const runBrowserController = async (
|
|
|
3343
3643
|
fatalError && fatalError !== fatalErrorBeforeRun
|
|
3344
3644
|
? fatalError
|
|
3345
3645
|
: undefined;
|
|
3346
|
-
await
|
|
3347
|
-
|
|
3348
|
-
|
|
3349
|
-
buildTime: 0,
|
|
3350
|
-
testTime,
|
|
3351
|
-
},
|
|
3352
|
-
filterRerunTestPaths: files.map((file) => file.testPath),
|
|
3646
|
+
await finalizeWatchRerun({
|
|
3647
|
+
rerunTestPaths: files.map((file) => file.testPath),
|
|
3648
|
+
testTime,
|
|
3353
3649
|
unhandledErrors: rerunError
|
|
3354
3650
|
? [rerunError]
|
|
3355
3651
|
: rerunFatalError
|
|
3356
3652
|
? [rerunFatalError]
|
|
3357
3653
|
: undefined,
|
|
3358
3654
|
});
|
|
3359
|
-
|
|
3655
|
+
logWatchReadyMessage(context, enableCliShortcuts);
|
|
3360
3656
|
}
|
|
3361
3657
|
},
|
|
3362
3658
|
onError: async (error) => {
|
|
@@ -3373,6 +3669,8 @@ export const runBrowserController = async (
|
|
|
3373
3669
|
},
|
|
3374
3670
|
});
|
|
3375
3671
|
|
|
3672
|
+
awaitHeadlessRerunIdle = () => latestRerunScheduler.whenIdle();
|
|
3673
|
+
|
|
3376
3674
|
if (allTestFiles.length === 0) {
|
|
3377
3675
|
const duration = {
|
|
3378
3676
|
totalTime: buildTime,
|
|
@@ -3392,6 +3690,7 @@ export const runBrowserController = async (
|
|
|
3392
3690
|
await destroyBrowserRuntime(runtime);
|
|
3393
3691
|
}
|
|
3394
3692
|
: undefined,
|
|
3693
|
+
watch: watchHandles,
|
|
3395
3694
|
};
|
|
3396
3695
|
|
|
3397
3696
|
if (isWatchMode) {
|
|
@@ -3403,18 +3702,17 @@ export const runBrowserController = async (
|
|
|
3403
3702
|
const newProjectEntries = await collectProjectEntries(context);
|
|
3404
3703
|
const rerunPlan = planWatchRerun({
|
|
3405
3704
|
projectEntries: newProjectEntries,
|
|
3406
|
-
previousTestFiles:
|
|
3407
|
-
affectedTestFiles:
|
|
3705
|
+
previousTestFiles: watchState.lastTestFiles,
|
|
3706
|
+
affectedTestFiles: drainPendingAffectedTestFiles(watchState),
|
|
3408
3707
|
});
|
|
3409
|
-
watchContext.affectedTestFiles = [];
|
|
3410
3708
|
|
|
3411
3709
|
if (rerunPlan.filesChanged) {
|
|
3412
|
-
|
|
3710
|
+
watchState.lastTestFiles = rerunPlan.currentTestFiles;
|
|
3413
3711
|
if (rerunPlan.currentTestFiles.length === 0) {
|
|
3414
3712
|
logger.log(
|
|
3415
3713
|
color.cyan('No browser test files remain after update.\n'),
|
|
3416
3714
|
);
|
|
3417
|
-
|
|
3715
|
+
logWatchReadyMessage(context, enableCliShortcuts);
|
|
3418
3716
|
return;
|
|
3419
3717
|
}
|
|
3420
3718
|
|
|
@@ -3423,14 +3721,16 @@ export const runBrowserController = async (
|
|
|
3423
3721
|
`Test file set changed, re-running ${rerunPlan.currentTestFiles.length} file(s)...\n`,
|
|
3424
3722
|
),
|
|
3425
3723
|
);
|
|
3426
|
-
|
|
3724
|
+
await latestRerunScheduler.enqueueLatest(
|
|
3725
|
+
rerunPlan.currentTestFiles,
|
|
3726
|
+
);
|
|
3427
3727
|
return;
|
|
3428
3728
|
}
|
|
3429
3729
|
|
|
3430
|
-
|
|
3730
|
+
logWatchReadyMessage(context, enableCliShortcuts);
|
|
3431
3731
|
};
|
|
3432
|
-
|
|
3433
|
-
|
|
3732
|
+
watchState.hooksEnabled = true;
|
|
3733
|
+
logWatchReadyMessage(context, enableCliShortcuts);
|
|
3434
3734
|
}
|
|
3435
3735
|
|
|
3436
3736
|
return result;
|
|
@@ -3445,26 +3745,25 @@ export const runBrowserController = async (
|
|
|
3445
3745
|
const newProjectEntries = await collectProjectEntries(context);
|
|
3446
3746
|
const rerunPlan = planWatchRerun({
|
|
3447
3747
|
projectEntries: newProjectEntries,
|
|
3448
|
-
previousTestFiles:
|
|
3449
|
-
affectedTestFiles:
|
|
3748
|
+
previousTestFiles: watchState.lastTestFiles,
|
|
3749
|
+
affectedTestFiles: drainPendingAffectedTestFiles(watchState),
|
|
3450
3750
|
});
|
|
3451
|
-
watchContext.affectedTestFiles = [];
|
|
3452
3751
|
|
|
3453
3752
|
if (rerunPlan.filesChanged) {
|
|
3454
3753
|
const deletedTestPaths = collectDeletedTestPaths(
|
|
3455
|
-
|
|
3754
|
+
watchState.lastTestFiles,
|
|
3456
3755
|
rerunPlan.currentTestFiles,
|
|
3457
3756
|
);
|
|
3458
3757
|
if (deletedTestPaths.length > 0) {
|
|
3459
3758
|
context.updateReporterResultState([], [], deletedTestPaths);
|
|
3460
3759
|
}
|
|
3461
|
-
|
|
3760
|
+
watchState.lastTestFiles = rerunPlan.currentTestFiles;
|
|
3462
3761
|
if (rerunPlan.currentTestFiles.length === 0) {
|
|
3463
3762
|
await latestRerunScheduler.enqueueLatest([]);
|
|
3464
3763
|
logger.log(
|
|
3465
3764
|
color.cyan('No browser test files remain after update.\n'),
|
|
3466
3765
|
);
|
|
3467
|
-
|
|
3766
|
+
logWatchReadyMessage(context, enableCliShortcuts);
|
|
3468
3767
|
return;
|
|
3469
3768
|
}
|
|
3470
3769
|
|
|
@@ -3473,7 +3772,7 @@ export const runBrowserController = async (
|
|
|
3473
3772
|
`Test file set changed, re-running ${rerunPlan.currentTestFiles.length} file(s)...\n`,
|
|
3474
3773
|
),
|
|
3475
3774
|
);
|
|
3476
|
-
|
|
3775
|
+
await latestRerunScheduler.enqueueLatest(rerunPlan.currentTestFiles);
|
|
3477
3776
|
return;
|
|
3478
3777
|
}
|
|
3479
3778
|
|
|
@@ -3483,7 +3782,7 @@ export const runBrowserController = async (
|
|
|
3483
3782
|
'No affected browser test files detected, skipping re-run.\n',
|
|
3484
3783
|
),
|
|
3485
3784
|
);
|
|
3486
|
-
|
|
3785
|
+
logWatchReadyMessage(context, enableCliShortcuts);
|
|
3487
3786
|
return;
|
|
3488
3787
|
}
|
|
3489
3788
|
|
|
@@ -3492,7 +3791,7 @@ export const runBrowserController = async (
|
|
|
3492
3791
|
`Re-running ${rerunPlan.affectedTestFiles.length} affected test file(s)...\n`,
|
|
3493
3792
|
),
|
|
3494
3793
|
);
|
|
3495
|
-
|
|
3794
|
+
await latestRerunScheduler.enqueueLatest(rerunPlan.affectedTestFiles);
|
|
3496
3795
|
};
|
|
3497
3796
|
}
|
|
3498
3797
|
|
|
@@ -3534,6 +3833,7 @@ export const runBrowserController = async (
|
|
|
3534
3833
|
// `closeHeadlessRuntime` is already `undefined` in watch mode, so the
|
|
3535
3834
|
// non-watch caller (core) receives the deferred close and watch does not.
|
|
3536
3835
|
close: closeHeadlessRuntime,
|
|
3836
|
+
watch: watchHandles,
|
|
3537
3837
|
};
|
|
3538
3838
|
|
|
3539
3839
|
if (isWatchMode) {
|
|
@@ -3545,8 +3845,8 @@ export const runBrowserController = async (
|
|
|
3545
3845
|
}
|
|
3546
3846
|
|
|
3547
3847
|
if (isWatchMode && triggerRerun) {
|
|
3548
|
-
|
|
3549
|
-
|
|
3848
|
+
watchState.hooksEnabled = true;
|
|
3849
|
+
logWatchReadyMessage(context, enableCliShortcuts);
|
|
3550
3850
|
}
|
|
3551
3851
|
|
|
3552
3852
|
return result;
|
|
@@ -3924,30 +4224,43 @@ export const runBrowserController = async (
|
|
|
3924
4224
|
// Define rerun logic for watch mode
|
|
3925
4225
|
if (isWatchMode) {
|
|
3926
4226
|
triggerRerun = async () => {
|
|
3927
|
-
|
|
4227
|
+
// Re-deliver the host config so runner iframes reloaded by this rerun
|
|
4228
|
+
// observe live per-rerun values ('u' flips updateSnapshot between
|
|
4229
|
+
// reruns); `setContainerOptions` keeps full container reloads in sync.
|
|
4230
|
+
const refreshedHostOptions: BrowserHostConfig = {
|
|
4231
|
+
...hostOptions,
|
|
4232
|
+
snapshot: {
|
|
4233
|
+
updateSnapshot: context.snapshotManager.options.updateSnapshot,
|
|
4234
|
+
},
|
|
4235
|
+
};
|
|
4236
|
+
runtime.setContainerOptions(refreshedHostOptions);
|
|
4237
|
+
// Independent: config push to the container vs. local entry collection.
|
|
4238
|
+
const [, newProjectEntries] = await Promise.all([
|
|
4239
|
+
rpcManager.updateHostConfig(refreshedHostOptions),
|
|
4240
|
+
collectProjectEntries(context),
|
|
4241
|
+
]);
|
|
3928
4242
|
const rerunPlan = planWatchRerun({
|
|
3929
4243
|
projectEntries: newProjectEntries,
|
|
3930
|
-
previousTestFiles:
|
|
3931
|
-
affectedTestFiles:
|
|
4244
|
+
previousTestFiles: watchState.lastTestFiles,
|
|
4245
|
+
affectedTestFiles: drainPendingAffectedTestFiles(watchState),
|
|
3932
4246
|
});
|
|
3933
|
-
watchContext.affectedTestFiles = [];
|
|
3934
4247
|
|
|
3935
4248
|
if (rerunPlan.filesChanged) {
|
|
3936
4249
|
const deletedTestPaths = collectDeletedTestPaths(
|
|
3937
|
-
|
|
4250
|
+
watchState.lastTestFiles,
|
|
3938
4251
|
rerunPlan.currentTestFiles,
|
|
3939
4252
|
);
|
|
3940
4253
|
if (deletedTestPaths.length > 0) {
|
|
3941
4254
|
context.updateReporterResultState([], [], deletedTestPaths);
|
|
3942
4255
|
}
|
|
3943
|
-
|
|
4256
|
+
watchState.lastTestFiles = rerunPlan.currentTestFiles;
|
|
3944
4257
|
currentTestFiles = rerunPlan.currentTestFiles;
|
|
3945
4258
|
await rpcManager.notifyTestFileUpdate(currentTestFiles);
|
|
3946
4259
|
if (currentTestFiles.length === 0) {
|
|
3947
4260
|
logger.log(
|
|
3948
4261
|
color.cyan('No browser test files remain after update.\n'),
|
|
3949
4262
|
);
|
|
3950
|
-
|
|
4263
|
+
logWatchReadyMessage(context, enableCliShortcuts);
|
|
3951
4264
|
return;
|
|
3952
4265
|
}
|
|
3953
4266
|
await waitForRunnerFramesReady(
|
|
@@ -3984,26 +4297,22 @@ export const runBrowserController = async (
|
|
|
3984
4297
|
fatalError && fatalError !== fatalErrorBeforeRun
|
|
3985
4298
|
? fatalError
|
|
3986
4299
|
: undefined;
|
|
3987
|
-
await
|
|
3988
|
-
|
|
3989
|
-
|
|
3990
|
-
buildTime: 0,
|
|
3991
|
-
testTime,
|
|
3992
|
-
},
|
|
3993
|
-
filterRerunTestPaths: rerunPlan.normalizedAffectedTestFiles,
|
|
4300
|
+
await finalizeWatchRerun({
|
|
4301
|
+
rerunTestPaths: rerunPlan.normalizedAffectedTestFiles,
|
|
4302
|
+
testTime,
|
|
3994
4303
|
unhandledErrors: rerunError
|
|
3995
4304
|
? [rerunError]
|
|
3996
4305
|
: rerunFatalError
|
|
3997
4306
|
? [rerunFatalError]
|
|
3998
4307
|
: undefined,
|
|
3999
4308
|
});
|
|
4000
|
-
|
|
4309
|
+
logWatchReadyMessage(context, enableCliShortcuts);
|
|
4001
4310
|
}
|
|
4002
4311
|
} else if (!rerunPlan.filesChanged) {
|
|
4003
4312
|
logger.log(color.cyan('Tests will be re-executed automatically\n'));
|
|
4004
|
-
|
|
4313
|
+
logWatchReadyMessage(context, enableCliShortcuts);
|
|
4005
4314
|
} else {
|
|
4006
|
-
|
|
4315
|
+
logWatchReadyMessage(context, enableCliShortcuts);
|
|
4007
4316
|
}
|
|
4008
4317
|
};
|
|
4009
4318
|
}
|
|
@@ -4055,6 +4364,7 @@ export const runBrowserController = async (
|
|
|
4055
4364
|
// `closeContainerRuntime` is already `undefined` in watch mode, so the
|
|
4056
4365
|
// non-watch caller (core) receives the deferred close and watch does not.
|
|
4057
4366
|
close: closeContainerRuntime,
|
|
4367
|
+
watch: watchHandles,
|
|
4058
4368
|
};
|
|
4059
4369
|
|
|
4060
4370
|
if (isWatchMode) {
|
|
@@ -4067,8 +4377,8 @@ export const runBrowserController = async (
|
|
|
4067
4377
|
|
|
4068
4378
|
// Enable watch hooks AFTER initial test run to avoid duplicate runs
|
|
4069
4379
|
if (isWatchMode && triggerRerun) {
|
|
4070
|
-
|
|
4071
|
-
|
|
4380
|
+
watchState.hooksEnabled = true;
|
|
4381
|
+
logWatchReadyMessage(context, enableCliShortcuts);
|
|
4072
4382
|
}
|
|
4073
4383
|
|
|
4074
4384
|
return result;
|