@rstest/browser 0.11.2 → 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/{117.3da95caa39.js → 243.a8eed2b9e7.js} +936 -910
- package/dist/browser-container/container-static/js/243.a8eed2b9e7.js.LICENSE.txt +1 -0
- package/dist/browser-container/container-static/js/{index.6e4da8c9ab.js → index.84aaafaf21.js} +80 -75
- package/dist/browser-container/index.html +1 -1
- package/dist/index.js +263 -148
- package/package.json +10 -9
- package/src/browserExecutor.ts +5 -3
- package/src/client/entry.ts +21 -13
- package/src/configValidation.ts +6 -0
- package/src/hostController.ts +496 -129
- package/dist/browser-container/container-static/js/117.3da95caa39.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/client/formatConsole.ts +0 -77
- package/src/client/public.ts +0 -12
- package/src/watchCliShortcuts.ts +0 -77
package/src/hostController.ts
CHANGED
|
@@ -6,9 +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,
|
|
10
|
+
applyWebMockRspackConfig,
|
|
9
11
|
type BrowserTestRunOptions,
|
|
10
12
|
type BrowserTestRunResult,
|
|
13
|
+
type BrowserWatchHandles,
|
|
14
|
+
buildBrowserCoverageMap,
|
|
11
15
|
type CoverageMapData,
|
|
16
|
+
type EntryHashSnapshot,
|
|
17
|
+
type ExecutorCycleOutcome,
|
|
18
|
+
FATAL_SIGNALS,
|
|
19
|
+
finalizeRunCycle,
|
|
12
20
|
type ListBrowserTestsOptions,
|
|
13
21
|
color,
|
|
14
22
|
createCoverageProvider,
|
|
@@ -17,14 +25,19 @@ import {
|
|
|
17
25
|
DEFAULT_TEST_TIMEOUT,
|
|
18
26
|
type FormattedError,
|
|
19
27
|
getNoTestFilesMessage,
|
|
28
|
+
getPrettyConsoleName,
|
|
20
29
|
getSetupFiles,
|
|
21
30
|
getTestEntries,
|
|
22
31
|
hasUserRstestConfigPlugins,
|
|
32
|
+
importMetaRstestDefine,
|
|
23
33
|
initModifyRstestConfigHooks,
|
|
24
34
|
isDebug,
|
|
35
|
+
isTTY,
|
|
25
36
|
type ListCommandResult,
|
|
26
37
|
loadCoverageProvider,
|
|
27
38
|
logger,
|
|
39
|
+
logWatchReadyMessage,
|
|
40
|
+
pluginMockRuntime,
|
|
28
41
|
prepareWatchRerunState,
|
|
29
42
|
projectRuntimeConfig,
|
|
30
43
|
PhaseTracker,
|
|
@@ -42,6 +55,7 @@ import {
|
|
|
42
55
|
type TestFileResult,
|
|
43
56
|
type TestResult,
|
|
44
57
|
type UserConsoleLog,
|
|
58
|
+
type WatchInvalidationState,
|
|
45
59
|
} from '@rstest/core/internal/browser';
|
|
46
60
|
import { type BirpcReturn, createBirpc } from 'birpc';
|
|
47
61
|
import openEditor from 'open-editor';
|
|
@@ -97,11 +111,6 @@ import {
|
|
|
97
111
|
type SourceMapPayload,
|
|
98
112
|
} from './sourceMap/sourceMapLoader';
|
|
99
113
|
import { resolveBrowserViewportPreset } from './viewportPresets';
|
|
100
|
-
import {
|
|
101
|
-
isBrowserWatchCliShortcutsEnabled,
|
|
102
|
-
logBrowserWatchReadyMessage,
|
|
103
|
-
setupBrowserWatchCliShortcuts,
|
|
104
|
-
} from './watchCliShortcuts';
|
|
105
114
|
import { collectWatchTestFiles, planWatchRerun } from './watchRerunPlanner';
|
|
106
115
|
|
|
107
116
|
const { createRsbuild, rspack } = rsbuild;
|
|
@@ -251,6 +260,12 @@ type ContainerRpcMethods = {
|
|
|
251
260
|
testFile: string,
|
|
252
261
|
testNamePattern?: string,
|
|
253
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>;
|
|
254
269
|
};
|
|
255
270
|
|
|
256
271
|
type ContainerRpc = BirpcReturn<ContainerRpcMethods, HostRpcMethods>;
|
|
@@ -390,6 +405,11 @@ class ContainerRpcManager {
|
|
|
390
405
|
await this.rpc?.onTestFileUpdate(files);
|
|
391
406
|
}
|
|
392
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
|
+
|
|
393
413
|
/** Request container to reload a specific test file */
|
|
394
414
|
async reloadTestFile(
|
|
395
415
|
testFile: string,
|
|
@@ -424,6 +444,56 @@ type BrowserProjectServer = {
|
|
|
424
444
|
manifestPath: string;
|
|
425
445
|
};
|
|
426
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
|
+
|
|
427
497
|
type BrowserRuntime = {
|
|
428
498
|
// Per-project servers, keyed by project name.
|
|
429
499
|
projectServers: Map<string, BrowserProjectServer>;
|
|
@@ -442,32 +512,27 @@ type BrowserRuntime = {
|
|
|
442
512
|
wss: WebSocketServer;
|
|
443
513
|
rpcManager?: ContainerRpcManager;
|
|
444
514
|
projectEntries: BrowserProjectEntries[];
|
|
515
|
+
watchState: BrowserWatchState;
|
|
445
516
|
};
|
|
446
517
|
|
|
447
518
|
// ============================================================================
|
|
448
|
-
// Watch Mode Context -
|
|
519
|
+
// Watch Mode Context - Process-lifecycle watch state
|
|
449
520
|
// ============================================================================
|
|
450
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`.
|
|
451
526
|
type WatchContext = {
|
|
452
527
|
runtime: BrowserRuntime | null;
|
|
453
|
-
lastTestFiles: TestFileInfo[];
|
|
454
|
-
hooksEnabled: boolean;
|
|
455
528
|
cleanupRegistered: boolean;
|
|
456
529
|
cleanupPromise: Promise<void> | null;
|
|
457
|
-
closeCliShortcuts: (() => void) | null;
|
|
458
|
-
chunkHashes: Map<string, string>;
|
|
459
|
-
affectedTestFiles: string[];
|
|
460
530
|
};
|
|
461
531
|
|
|
462
532
|
const watchContext: WatchContext = {
|
|
463
533
|
runtime: null,
|
|
464
|
-
lastTestFiles: [],
|
|
465
|
-
hooksEnabled: false,
|
|
466
534
|
cleanupRegistered: false,
|
|
467
535
|
cleanupPromise: null,
|
|
468
|
-
closeCliShortcuts: null,
|
|
469
|
-
chunkHashes: new Map(),
|
|
470
|
-
affectedTestFiles: [],
|
|
471
536
|
};
|
|
472
537
|
|
|
473
538
|
// ============================================================================
|
|
@@ -944,42 +1009,87 @@ const getChunkKey = (chunk: StatsChunk): string | null => {
|
|
|
944
1009
|
};
|
|
945
1010
|
|
|
946
1011
|
/**
|
|
947
|
-
*
|
|
948
|
-
*
|
|
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.
|
|
949
1016
|
*/
|
|
950
|
-
const getAffectedTestFiles = (
|
|
951
|
-
chunks
|
|
952
|
-
entryTestFiles
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
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
|
+
};
|
|
958
1041
|
|
|
959
|
-
for (const chunk of chunks) {
|
|
1042
|
+
for (const chunk of chunks || []) {
|
|
960
1043
|
if (!chunk.hash) continue;
|
|
961
1044
|
|
|
962
|
-
// First check if this chunk contains a test entry file
|
|
963
|
-
const testFile = findTestFileInModules(chunk.modules, entryTestFiles);
|
|
964
|
-
if (!testFile) continue;
|
|
965
|
-
|
|
966
|
-
// Get a stable key for this chunk
|
|
967
1045
|
const chunkKey = getChunkKey(chunk);
|
|
968
1046
|
if (!chunkKey) continue;
|
|
969
1047
|
|
|
970
|
-
const
|
|
971
|
-
|
|
1048
|
+
const testFile = findTestFileInModules(chunk.modules, entryTestFiles);
|
|
1049
|
+
if (testFile) {
|
|
1050
|
+
recordChunk(entryHashes, testFile, chunkKey, chunk.hash);
|
|
1051
|
+
continue;
|
|
1052
|
+
}
|
|
1053
|
+
|
|
1054
|
+
const setupFile = findTestFileInModules(chunk.modules, setupFiles);
|
|
1055
|
+
if (setupFile) {
|
|
1056
|
+
recordChunk(setupHashes, setupFile, chunkKey, chunk.hash);
|
|
1057
|
+
}
|
|
1058
|
+
}
|
|
972
1059
|
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
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
|
+
}
|
|
978
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}`);
|
|
979
1090
|
}
|
|
980
1091
|
|
|
981
|
-
|
|
982
|
-
return Array.from(affectedFiles);
|
|
1092
|
+
return outcome.affectedPaths;
|
|
983
1093
|
};
|
|
984
1094
|
|
|
985
1095
|
const getBrowserProjects = (context: RstestContext): ProjectContext[] =>
|
|
@@ -1242,16 +1352,51 @@ const generateManifestModule = ({
|
|
|
1242
1352
|
project.normalizedConfig.exclude.patterns,
|
|
1243
1353
|
projectRootPosix,
|
|
1244
1354
|
);
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
lines.push(`
|
|
1355
|
+
const { includeSource } = project.normalizedConfig;
|
|
1356
|
+
const emitContext = (contextVarName: string, regExp: RegExp): void => {
|
|
1357
|
+
lines.push(
|
|
1358
|
+
`const ${contextVarName} = import.meta.webpackContext(${JSON.stringify(projectRootPosix)}, {`,
|
|
1359
|
+
);
|
|
1360
|
+
lines.push(' recursive: true,');
|
|
1361
|
+
lines.push(` regExp: ${regExp.toString()},`);
|
|
1362
|
+
if (excludeRegExp) {
|
|
1363
|
+
lines.push(` exclude: ${excludeRegExp.toString()},`);
|
|
1364
|
+
}
|
|
1365
|
+
lines.push(" mode: 'lazy',");
|
|
1366
|
+
lines.push('});');
|
|
1367
|
+
};
|
|
1368
|
+
|
|
1369
|
+
if (includeSource.length === 0) {
|
|
1370
|
+
emitContext(varName, includeRegExp);
|
|
1371
|
+
} else {
|
|
1372
|
+
// In-source test files (`includeSource`) carry an
|
|
1373
|
+
// `if (import.meta.rstest)` block. The include context can't see them,
|
|
1374
|
+
// so a second context over the `includeSource` globs backs
|
|
1375
|
+
// host-scheduled loads, while `keys()` only unions the entry-probed
|
|
1376
|
+
// in-source files (the probe does not apply inside the bundle, so raw
|
|
1377
|
+
// source-context keys would execute never-probed files and fail with
|
|
1378
|
+
// "No test suites found"). The probed list can go stale until a
|
|
1379
|
+
// manifest refresh; scheduled-by-path loading never does.
|
|
1380
|
+
emitContext(`${varName}_include`, includeRegExp);
|
|
1381
|
+
emitContext(`${varName}_source`, globPatternsToRegExp(includeSource));
|
|
1382
|
+
const probedKeys = testFiles.map((filePath) =>
|
|
1383
|
+
toContextKey(filePath, projectRootPosix),
|
|
1384
|
+
);
|
|
1385
|
+
lines.push(`const ${varName}_probed = ${JSON.stringify(probedKeys)};`);
|
|
1386
|
+
lines.push(
|
|
1387
|
+
`const ${varName}_includeKeys = new Set(${varName}_include.keys());`,
|
|
1388
|
+
);
|
|
1389
|
+
lines.push(`const ${varName} = Object.assign(`);
|
|
1390
|
+
lines.push(
|
|
1391
|
+
` (key) => ${varName}_includeKeys.has(key) ? ${varName}_include(key) : ${varName}_source(key),`,
|
|
1392
|
+
);
|
|
1393
|
+
lines.push(' {');
|
|
1394
|
+
lines.push(
|
|
1395
|
+
` keys: () => Array.from(new Set([...${varName}_includeKeys, ...${varName}_probed])),`,
|
|
1396
|
+
);
|
|
1397
|
+
lines.push(' },');
|
|
1398
|
+
lines.push(');');
|
|
1252
1399
|
}
|
|
1253
|
-
lines.push(" mode: 'lazy',");
|
|
1254
|
-
lines.push('});');
|
|
1255
1400
|
} else {
|
|
1256
1401
|
// One-shot runs: the file set is fixed and already filtered, so emit an
|
|
1257
1402
|
// explicit lazy-import map (one chunk per literal `import()`, like the
|
|
@@ -1374,9 +1519,6 @@ const cleanupWatchRuntime = (): Promise<void> => {
|
|
|
1374
1519
|
}
|
|
1375
1520
|
|
|
1376
1521
|
watchContext.cleanupPromise = (async () => {
|
|
1377
|
-
watchContext.closeCliShortcuts?.();
|
|
1378
|
-
watchContext.closeCliShortcuts = null;
|
|
1379
|
-
|
|
1380
1522
|
if (!watchContext.runtime) {
|
|
1381
1523
|
return;
|
|
1382
1524
|
}
|
|
@@ -1388,12 +1530,22 @@ const cleanupWatchRuntime = (): Promise<void> => {
|
|
|
1388
1530
|
return watchContext.cleanupPromise;
|
|
1389
1531
|
};
|
|
1390
1532
|
|
|
1391
|
-
const registerWatchCleanup = (): void => {
|
|
1533
|
+
const registerWatchCleanup = (embedded: boolean): void => {
|
|
1392
1534
|
if (watchContext.cleanupRegistered) {
|
|
1393
1535
|
return;
|
|
1394
1536
|
}
|
|
1537
|
+
watchContext.cleanupRegistered = true;
|
|
1395
1538
|
|
|
1396
|
-
|
|
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
|
+
}
|
|
1544
|
+
|
|
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) {
|
|
1397
1549
|
process.once(signal, () => {
|
|
1398
1550
|
void cleanupWatchRuntime();
|
|
1399
1551
|
});
|
|
@@ -1402,8 +1554,6 @@ const registerWatchCleanup = (): void => {
|
|
|
1402
1554
|
process.once('exit', () => {
|
|
1403
1555
|
void cleanupWatchRuntime();
|
|
1404
1556
|
});
|
|
1405
|
-
|
|
1406
|
-
watchContext.cleanupRegistered = true;
|
|
1407
1557
|
};
|
|
1408
1558
|
|
|
1409
1559
|
const createBrowserRuntime = async ({
|
|
@@ -1463,6 +1613,10 @@ const createBrowserRuntime = async ({
|
|
|
1463
1613
|
let browserLaunchOptions =
|
|
1464
1614
|
ensureConsistentBrowserLaunchOptions(browserProjects);
|
|
1465
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();
|
|
1466
1620
|
const manifestModules: Array<{
|
|
1467
1621
|
manifestPath: string;
|
|
1468
1622
|
project: ProjectContext;
|
|
@@ -1491,6 +1645,7 @@ const createBrowserRuntime = async ({
|
|
|
1491
1645
|
dispatchHandlers,
|
|
1492
1646
|
wss: undefined as unknown as WebSocketServer,
|
|
1493
1647
|
projectEntries,
|
|
1648
|
+
watchState,
|
|
1494
1649
|
};
|
|
1495
1650
|
};
|
|
1496
1651
|
|
|
@@ -1546,11 +1701,13 @@ const createBrowserRuntime = async ({
|
|
|
1546
1701
|
// Shared by every project — only the per-project `@rstest/browser-manifest`
|
|
1547
1702
|
// alias varies (one virtual manifest per server).
|
|
1548
1703
|
const staticRstestAliases = {
|
|
1549
|
-
// User test code
|
|
1550
|
-
|
|
1704
|
+
// User test code `import { describe, it } from '@rstest/core'` is NOT
|
|
1705
|
+
// aliased: `applyWebMockRspackConfig` keeps the request external against
|
|
1706
|
+
// `globalThis['@rstest/core']` (node parity), which also keeps the mock
|
|
1707
|
+
// hoister's provider-import ordering correct for `rs.hoisted` callbacks.
|
|
1551
1708
|
// User test code: import { page } from '@rstest/browser'
|
|
1552
1709
|
'@rstest/browser': resolveBrowserFile('browser.ts'),
|
|
1553
|
-
// Browser runtime APIs for entry.ts
|
|
1710
|
+
// Browser runtime APIs for entry.ts
|
|
1554
1711
|
// Uses dist file with extractSourceMap to preserve sourcemap chain for inline snapshots
|
|
1555
1712
|
'@rstest/core/internal/browser-runtime': browserRuntimePath,
|
|
1556
1713
|
};
|
|
@@ -1712,6 +1869,9 @@ const createBrowserRuntime = async ({
|
|
|
1712
1869
|
|
|
1713
1870
|
// Add plugin to merge user Rsbuild config with rstest required config
|
|
1714
1871
|
rsbuildInstance.addPlugins([
|
|
1872
|
+
// Same mock runtime as the node build (importActual doppelganger rule +
|
|
1873
|
+
// mock webpack runtime module); order-insensitive and self-contained.
|
|
1874
|
+
pluginMockRuntime,
|
|
1715
1875
|
{
|
|
1716
1876
|
name: 'rstest:browser-user-config',
|
|
1717
1877
|
setup(api) {
|
|
@@ -1766,6 +1926,10 @@ const createBrowserRuntime = async ({
|
|
|
1766
1926
|
define: {
|
|
1767
1927
|
'process.env': rstestEnvDefine,
|
|
1768
1928
|
'import.meta.env': rstestEnvDefine,
|
|
1929
|
+
// In-source `if (import.meta.rstest)` blocks read the
|
|
1930
|
+
// per-file runtime API the client entry publishes on
|
|
1931
|
+
// `globalThis` (node parity: `global['@rstest/core']`).
|
|
1932
|
+
'import.meta.rstest': importMetaRstestDefine('web'),
|
|
1769
1933
|
},
|
|
1770
1934
|
},
|
|
1771
1935
|
output: {
|
|
@@ -1774,10 +1938,45 @@ const createBrowserRuntime = async ({
|
|
|
1774
1938
|
sourceMap: {
|
|
1775
1939
|
js: 'source-map',
|
|
1776
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
|
+
},
|
|
1777
1955
|
},
|
|
1778
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
|
+
},
|
|
1779
1970
|
rspack: (rspackConfig) => {
|
|
1780
1971
|
rspackConfig.mode = 'development';
|
|
1972
|
+
// Web parameterization of the node mock transform:
|
|
1973
|
+
// RstestPlugin (hoist + path injection), the
|
|
1974
|
+
// `@rstest/core` global external, and
|
|
1975
|
+
// `exportsPresence: 'warn'`.
|
|
1976
|
+
applyWebMockRspackConfig(rspackConfig, {
|
|
1977
|
+
rspack,
|
|
1978
|
+
rootPath: project.rootPath,
|
|
1979
|
+
});
|
|
1781
1980
|
// lazyCompilation's only delivery transport is the HMR
|
|
1782
1981
|
// runtime, so it follows the same gate as HMR (see
|
|
1783
1982
|
// `shouldEnableBrowserHmr`): headed watch only, everything
|
|
@@ -1837,37 +2036,80 @@ const createBrowserRuntime = async ({
|
|
|
1837
2036
|
name: 'rstest:browser-watch',
|
|
1838
2037
|
setup(api) {
|
|
1839
2038
|
api.onBeforeDevCompile(() => {
|
|
1840
|
-
|
|
2039
|
+
watchState.compileStartTimes.set(project.name, Date.now());
|
|
2040
|
+
if (!watchState.hooksEnabled) {
|
|
1841
2041
|
return;
|
|
1842
2042
|
}
|
|
1843
2043
|
logger.log(color.cyan('\nFile changed, re-running tests...\n'));
|
|
1844
2044
|
});
|
|
1845
2045
|
|
|
1846
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
|
+
}
|
|
1847
2063
|
// Collect hashes even during initial build to establish baseline
|
|
1848
2064
|
if (stats) {
|
|
1849
|
-
|
|
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
|
+
]);
|
|
1850
2070
|
const entryTestFiles = new Set<string>(
|
|
1851
|
-
collectWatchTestFiles(
|
|
2071
|
+
collectWatchTestFiles(projectEntry ? [projectEntry] : []).map(
|
|
1852
2072
|
(file) => file.testPath,
|
|
1853
2073
|
),
|
|
1854
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
|
+
}
|
|
1855
2086
|
|
|
1856
2087
|
const statsJson = stats.toJson({ all: true });
|
|
1857
|
-
const affected = getAffectedTestFiles(
|
|
1858
|
-
statsJson.chunks,
|
|
2088
|
+
const affected = getAffectedTestFiles({
|
|
2089
|
+
chunks: statsJson.chunks,
|
|
1859
2090
|
entryTestFiles,
|
|
1860
|
-
|
|
1861
|
-
|
|
2091
|
+
setupFiles,
|
|
2092
|
+
state,
|
|
2093
|
+
});
|
|
1862
2094
|
|
|
1863
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
|
+
);
|
|
1864
2106
|
logger.debug(
|
|
1865
2107
|
`[Watch] Affected test files: ${affected.join(', ')}`,
|
|
1866
2108
|
);
|
|
1867
2109
|
}
|
|
1868
2110
|
}
|
|
1869
2111
|
|
|
1870
|
-
if (!
|
|
2112
|
+
if (!watchState.hooksEnabled) {
|
|
1871
2113
|
return;
|
|
1872
2114
|
}
|
|
1873
2115
|
|
|
@@ -1909,6 +2151,15 @@ const createBrowserRuntime = async ({
|
|
|
1909
2151
|
if (isDebug()) {
|
|
1910
2152
|
await rsbuildInstance.inspectConfig({
|
|
1911
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
|
+
),
|
|
1912
2163
|
extraConfigs: {
|
|
1913
2164
|
rstest: {
|
|
1914
2165
|
...context.normalizedConfig,
|
|
@@ -2058,6 +2309,7 @@ const createBrowserRuntime = async ({
|
|
|
2058
2309
|
dispatchHandlers,
|
|
2059
2310
|
wss,
|
|
2060
2311
|
projectEntries,
|
|
2312
|
+
watchState,
|
|
2061
2313
|
};
|
|
2062
2314
|
} catch (error) {
|
|
2063
2315
|
wss.close();
|
|
@@ -2423,7 +2675,7 @@ export const runBrowserController = async (
|
|
|
2423
2675
|
await notifyTestRunStart();
|
|
2424
2676
|
}
|
|
2425
2677
|
|
|
2426
|
-
const enableCliShortcuts = isWatchMode &&
|
|
2678
|
+
const enableCliShortcuts = isWatchMode && isTTY('stdin');
|
|
2427
2679
|
const browserTempOutputRoot = context.normalizedConfig.output.distPath.root;
|
|
2428
2680
|
const tempDir =
|
|
2429
2681
|
isWatchMode && watchContext.runtime
|
|
@@ -2437,15 +2689,13 @@ export const runBrowserController = async (
|
|
|
2437
2689
|
Date.now().toString(),
|
|
2438
2690
|
);
|
|
2439
2691
|
|
|
2440
|
-
// Track initial test files for watch mode
|
|
2441
|
-
if (isWatchMode) {
|
|
2442
|
-
watchContext.lastTestFiles = collectWatchTestFiles(projectEntries);
|
|
2443
|
-
}
|
|
2444
|
-
|
|
2445
2692
|
let runtime = isWatchMode ? watchContext.runtime : null;
|
|
2446
2693
|
|
|
2447
2694
|
// Define rerun callback for watch mode (will be populated later)
|
|
2448
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;
|
|
2449
2699
|
|
|
2450
2700
|
if (!runtime) {
|
|
2451
2701
|
try {
|
|
@@ -2476,15 +2726,113 @@ export const runBrowserController = async (
|
|
|
2476
2726
|
|
|
2477
2727
|
if (isWatchMode) {
|
|
2478
2728
|
watchContext.runtime = runtime;
|
|
2479
|
-
registerWatchCleanup();
|
|
2729
|
+
registerWatchCleanup(context.embedded);
|
|
2730
|
+
}
|
|
2731
|
+
}
|
|
2480
2732
|
|
|
2481
|
-
|
|
2482
|
-
|
|
2483
|
-
|
|
2484
|
-
|
|
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;
|
|
2485
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;
|
|
2486
2762
|
}
|
|
2487
|
-
|
|
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
|
+
};
|
|
2488
2836
|
|
|
2489
2837
|
projectEntries = runtime.projectEntries;
|
|
2490
2838
|
totalTests = projectEntries.reduce(
|
|
@@ -2556,7 +2904,9 @@ export const runBrowserController = async (
|
|
|
2556
2904
|
rootPath: normalize(context.rootPath),
|
|
2557
2905
|
projects: projectRuntimeConfigs,
|
|
2558
2906
|
snapshot: {
|
|
2559
|
-
updateSnapshot:
|
|
2907
|
+
updateSnapshot:
|
|
2908
|
+
options?.updateSnapshot ??
|
|
2909
|
+
context.snapshotManager.options.updateSnapshot,
|
|
2560
2910
|
},
|
|
2561
2911
|
// Container origin (fallback). Per-project runner origins below.
|
|
2562
2912
|
runnerUrl: `http://localhost:${runtime.containerServer.port}`,
|
|
@@ -2854,7 +3204,8 @@ export const runBrowserController = async (
|
|
|
2854
3204
|
const handleLog = async (payload: LogPayload): Promise<void> => {
|
|
2855
3205
|
const log: UserConsoleLog = {
|
|
2856
3206
|
content: payload.content,
|
|
2857
|
-
|
|
3207
|
+
// Same colored level label as the node worker's CustomConsole.
|
|
3208
|
+
name: getPrettyConsoleName(payload.level),
|
|
2858
3209
|
taskId: payload.taskId,
|
|
2859
3210
|
taskName: payload.taskName,
|
|
2860
3211
|
taskParentNames: payload.taskParentNames,
|
|
@@ -3117,6 +3468,12 @@ export const runBrowserController = async (
|
|
|
3117
3468
|
|
|
3118
3469
|
const inlineOptions: BrowserHostConfig = {
|
|
3119
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
|
+
},
|
|
3120
3477
|
testFile: file.testPath,
|
|
3121
3478
|
runId: `${run.token}:${session.id}`,
|
|
3122
3479
|
};
|
|
@@ -3286,20 +3643,16 @@ export const runBrowserController = async (
|
|
|
3286
3643
|
fatalError && fatalError !== fatalErrorBeforeRun
|
|
3287
3644
|
? fatalError
|
|
3288
3645
|
: undefined;
|
|
3289
|
-
await
|
|
3290
|
-
|
|
3291
|
-
|
|
3292
|
-
buildTime: 0,
|
|
3293
|
-
testTime,
|
|
3294
|
-
},
|
|
3295
|
-
filterRerunTestPaths: files.map((file) => file.testPath),
|
|
3646
|
+
await finalizeWatchRerun({
|
|
3647
|
+
rerunTestPaths: files.map((file) => file.testPath),
|
|
3648
|
+
testTime,
|
|
3296
3649
|
unhandledErrors: rerunError
|
|
3297
3650
|
? [rerunError]
|
|
3298
3651
|
: rerunFatalError
|
|
3299
3652
|
? [rerunFatalError]
|
|
3300
3653
|
: undefined,
|
|
3301
3654
|
});
|
|
3302
|
-
|
|
3655
|
+
logWatchReadyMessage(context, enableCliShortcuts);
|
|
3303
3656
|
}
|
|
3304
3657
|
},
|
|
3305
3658
|
onError: async (error) => {
|
|
@@ -3316,6 +3669,8 @@ export const runBrowserController = async (
|
|
|
3316
3669
|
},
|
|
3317
3670
|
});
|
|
3318
3671
|
|
|
3672
|
+
awaitHeadlessRerunIdle = () => latestRerunScheduler.whenIdle();
|
|
3673
|
+
|
|
3319
3674
|
if (allTestFiles.length === 0) {
|
|
3320
3675
|
const duration = {
|
|
3321
3676
|
totalTime: buildTime,
|
|
@@ -3335,6 +3690,7 @@ export const runBrowserController = async (
|
|
|
3335
3690
|
await destroyBrowserRuntime(runtime);
|
|
3336
3691
|
}
|
|
3337
3692
|
: undefined,
|
|
3693
|
+
watch: watchHandles,
|
|
3338
3694
|
};
|
|
3339
3695
|
|
|
3340
3696
|
if (isWatchMode) {
|
|
@@ -3346,18 +3702,17 @@ export const runBrowserController = async (
|
|
|
3346
3702
|
const newProjectEntries = await collectProjectEntries(context);
|
|
3347
3703
|
const rerunPlan = planWatchRerun({
|
|
3348
3704
|
projectEntries: newProjectEntries,
|
|
3349
|
-
previousTestFiles:
|
|
3350
|
-
affectedTestFiles:
|
|
3705
|
+
previousTestFiles: watchState.lastTestFiles,
|
|
3706
|
+
affectedTestFiles: drainPendingAffectedTestFiles(watchState),
|
|
3351
3707
|
});
|
|
3352
|
-
watchContext.affectedTestFiles = [];
|
|
3353
3708
|
|
|
3354
3709
|
if (rerunPlan.filesChanged) {
|
|
3355
|
-
|
|
3710
|
+
watchState.lastTestFiles = rerunPlan.currentTestFiles;
|
|
3356
3711
|
if (rerunPlan.currentTestFiles.length === 0) {
|
|
3357
3712
|
logger.log(
|
|
3358
3713
|
color.cyan('No browser test files remain after update.\n'),
|
|
3359
3714
|
);
|
|
3360
|
-
|
|
3715
|
+
logWatchReadyMessage(context, enableCliShortcuts);
|
|
3361
3716
|
return;
|
|
3362
3717
|
}
|
|
3363
3718
|
|
|
@@ -3366,14 +3721,16 @@ export const runBrowserController = async (
|
|
|
3366
3721
|
`Test file set changed, re-running ${rerunPlan.currentTestFiles.length} file(s)...\n`,
|
|
3367
3722
|
),
|
|
3368
3723
|
);
|
|
3369
|
-
|
|
3724
|
+
await latestRerunScheduler.enqueueLatest(
|
|
3725
|
+
rerunPlan.currentTestFiles,
|
|
3726
|
+
);
|
|
3370
3727
|
return;
|
|
3371
3728
|
}
|
|
3372
3729
|
|
|
3373
|
-
|
|
3730
|
+
logWatchReadyMessage(context, enableCliShortcuts);
|
|
3374
3731
|
};
|
|
3375
|
-
|
|
3376
|
-
|
|
3732
|
+
watchState.hooksEnabled = true;
|
|
3733
|
+
logWatchReadyMessage(context, enableCliShortcuts);
|
|
3377
3734
|
}
|
|
3378
3735
|
|
|
3379
3736
|
return result;
|
|
@@ -3388,26 +3745,25 @@ export const runBrowserController = async (
|
|
|
3388
3745
|
const newProjectEntries = await collectProjectEntries(context);
|
|
3389
3746
|
const rerunPlan = planWatchRerun({
|
|
3390
3747
|
projectEntries: newProjectEntries,
|
|
3391
|
-
previousTestFiles:
|
|
3392
|
-
affectedTestFiles:
|
|
3748
|
+
previousTestFiles: watchState.lastTestFiles,
|
|
3749
|
+
affectedTestFiles: drainPendingAffectedTestFiles(watchState),
|
|
3393
3750
|
});
|
|
3394
|
-
watchContext.affectedTestFiles = [];
|
|
3395
3751
|
|
|
3396
3752
|
if (rerunPlan.filesChanged) {
|
|
3397
3753
|
const deletedTestPaths = collectDeletedTestPaths(
|
|
3398
|
-
|
|
3754
|
+
watchState.lastTestFiles,
|
|
3399
3755
|
rerunPlan.currentTestFiles,
|
|
3400
3756
|
);
|
|
3401
3757
|
if (deletedTestPaths.length > 0) {
|
|
3402
3758
|
context.updateReporterResultState([], [], deletedTestPaths);
|
|
3403
3759
|
}
|
|
3404
|
-
|
|
3760
|
+
watchState.lastTestFiles = rerunPlan.currentTestFiles;
|
|
3405
3761
|
if (rerunPlan.currentTestFiles.length === 0) {
|
|
3406
3762
|
await latestRerunScheduler.enqueueLatest([]);
|
|
3407
3763
|
logger.log(
|
|
3408
3764
|
color.cyan('No browser test files remain after update.\n'),
|
|
3409
3765
|
);
|
|
3410
|
-
|
|
3766
|
+
logWatchReadyMessage(context, enableCliShortcuts);
|
|
3411
3767
|
return;
|
|
3412
3768
|
}
|
|
3413
3769
|
|
|
@@ -3416,7 +3772,7 @@ export const runBrowserController = async (
|
|
|
3416
3772
|
`Test file set changed, re-running ${rerunPlan.currentTestFiles.length} file(s)...\n`,
|
|
3417
3773
|
),
|
|
3418
3774
|
);
|
|
3419
|
-
|
|
3775
|
+
await latestRerunScheduler.enqueueLatest(rerunPlan.currentTestFiles);
|
|
3420
3776
|
return;
|
|
3421
3777
|
}
|
|
3422
3778
|
|
|
@@ -3426,7 +3782,7 @@ export const runBrowserController = async (
|
|
|
3426
3782
|
'No affected browser test files detected, skipping re-run.\n',
|
|
3427
3783
|
),
|
|
3428
3784
|
);
|
|
3429
|
-
|
|
3785
|
+
logWatchReadyMessage(context, enableCliShortcuts);
|
|
3430
3786
|
return;
|
|
3431
3787
|
}
|
|
3432
3788
|
|
|
@@ -3435,7 +3791,7 @@ export const runBrowserController = async (
|
|
|
3435
3791
|
`Re-running ${rerunPlan.affectedTestFiles.length} affected test file(s)...\n`,
|
|
3436
3792
|
),
|
|
3437
3793
|
);
|
|
3438
|
-
|
|
3794
|
+
await latestRerunScheduler.enqueueLatest(rerunPlan.affectedTestFiles);
|
|
3439
3795
|
};
|
|
3440
3796
|
}
|
|
3441
3797
|
|
|
@@ -3477,6 +3833,7 @@ export const runBrowserController = async (
|
|
|
3477
3833
|
// `closeHeadlessRuntime` is already `undefined` in watch mode, so the
|
|
3478
3834
|
// non-watch caller (core) receives the deferred close and watch does not.
|
|
3479
3835
|
close: closeHeadlessRuntime,
|
|
3836
|
+
watch: watchHandles,
|
|
3480
3837
|
};
|
|
3481
3838
|
|
|
3482
3839
|
if (isWatchMode) {
|
|
@@ -3488,8 +3845,8 @@ export const runBrowserController = async (
|
|
|
3488
3845
|
}
|
|
3489
3846
|
|
|
3490
3847
|
if (isWatchMode && triggerRerun) {
|
|
3491
|
-
|
|
3492
|
-
|
|
3848
|
+
watchState.hooksEnabled = true;
|
|
3849
|
+
logWatchReadyMessage(context, enableCliShortcuts);
|
|
3493
3850
|
}
|
|
3494
3851
|
|
|
3495
3852
|
return result;
|
|
@@ -3867,30 +4224,43 @@ export const runBrowserController = async (
|
|
|
3867
4224
|
// Define rerun logic for watch mode
|
|
3868
4225
|
if (isWatchMode) {
|
|
3869
4226
|
triggerRerun = async () => {
|
|
3870
|
-
|
|
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
|
+
]);
|
|
3871
4242
|
const rerunPlan = planWatchRerun({
|
|
3872
4243
|
projectEntries: newProjectEntries,
|
|
3873
|
-
previousTestFiles:
|
|
3874
|
-
affectedTestFiles:
|
|
4244
|
+
previousTestFiles: watchState.lastTestFiles,
|
|
4245
|
+
affectedTestFiles: drainPendingAffectedTestFiles(watchState),
|
|
3875
4246
|
});
|
|
3876
|
-
watchContext.affectedTestFiles = [];
|
|
3877
4247
|
|
|
3878
4248
|
if (rerunPlan.filesChanged) {
|
|
3879
4249
|
const deletedTestPaths = collectDeletedTestPaths(
|
|
3880
|
-
|
|
4250
|
+
watchState.lastTestFiles,
|
|
3881
4251
|
rerunPlan.currentTestFiles,
|
|
3882
4252
|
);
|
|
3883
4253
|
if (deletedTestPaths.length > 0) {
|
|
3884
4254
|
context.updateReporterResultState([], [], deletedTestPaths);
|
|
3885
4255
|
}
|
|
3886
|
-
|
|
4256
|
+
watchState.lastTestFiles = rerunPlan.currentTestFiles;
|
|
3887
4257
|
currentTestFiles = rerunPlan.currentTestFiles;
|
|
3888
4258
|
await rpcManager.notifyTestFileUpdate(currentTestFiles);
|
|
3889
4259
|
if (currentTestFiles.length === 0) {
|
|
3890
4260
|
logger.log(
|
|
3891
4261
|
color.cyan('No browser test files remain after update.\n'),
|
|
3892
4262
|
);
|
|
3893
|
-
|
|
4263
|
+
logWatchReadyMessage(context, enableCliShortcuts);
|
|
3894
4264
|
return;
|
|
3895
4265
|
}
|
|
3896
4266
|
await waitForRunnerFramesReady(
|
|
@@ -3927,26 +4297,22 @@ export const runBrowserController = async (
|
|
|
3927
4297
|
fatalError && fatalError !== fatalErrorBeforeRun
|
|
3928
4298
|
? fatalError
|
|
3929
4299
|
: undefined;
|
|
3930
|
-
await
|
|
3931
|
-
|
|
3932
|
-
|
|
3933
|
-
buildTime: 0,
|
|
3934
|
-
testTime,
|
|
3935
|
-
},
|
|
3936
|
-
filterRerunTestPaths: rerunPlan.normalizedAffectedTestFiles,
|
|
4300
|
+
await finalizeWatchRerun({
|
|
4301
|
+
rerunTestPaths: rerunPlan.normalizedAffectedTestFiles,
|
|
4302
|
+
testTime,
|
|
3937
4303
|
unhandledErrors: rerunError
|
|
3938
4304
|
? [rerunError]
|
|
3939
4305
|
: rerunFatalError
|
|
3940
4306
|
? [rerunFatalError]
|
|
3941
4307
|
: undefined,
|
|
3942
4308
|
});
|
|
3943
|
-
|
|
4309
|
+
logWatchReadyMessage(context, enableCliShortcuts);
|
|
3944
4310
|
}
|
|
3945
4311
|
} else if (!rerunPlan.filesChanged) {
|
|
3946
4312
|
logger.log(color.cyan('Tests will be re-executed automatically\n'));
|
|
3947
|
-
|
|
4313
|
+
logWatchReadyMessage(context, enableCliShortcuts);
|
|
3948
4314
|
} else {
|
|
3949
|
-
|
|
4315
|
+
logWatchReadyMessage(context, enableCliShortcuts);
|
|
3950
4316
|
}
|
|
3951
4317
|
};
|
|
3952
4318
|
}
|
|
@@ -3998,6 +4364,7 @@ export const runBrowserController = async (
|
|
|
3998
4364
|
// `closeContainerRuntime` is already `undefined` in watch mode, so the
|
|
3999
4365
|
// non-watch caller (core) receives the deferred close and watch does not.
|
|
4000
4366
|
close: closeContainerRuntime,
|
|
4367
|
+
watch: watchHandles,
|
|
4001
4368
|
};
|
|
4002
4369
|
|
|
4003
4370
|
if (isWatchMode) {
|
|
@@ -4010,8 +4377,8 @@ export const runBrowserController = async (
|
|
|
4010
4377
|
|
|
4011
4378
|
// Enable watch hooks AFTER initial test run to avoid duplicate runs
|
|
4012
4379
|
if (isWatchMode && triggerRerun) {
|
|
4013
|
-
|
|
4014
|
-
|
|
4380
|
+
watchState.hooksEnabled = true;
|
|
4381
|
+
logWatchReadyMessage(context, enableCliShortcuts);
|
|
4015
4382
|
}
|
|
4016
4383
|
|
|
4017
4384
|
return result;
|