@rstest/browser 0.11.0 → 0.11.2
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/{328.37af068e21.js → 117.3da95caa39.js} +1006 -1006
- package/dist/browser-container/container-static/js/117.3da95caa39.js.LICENSE.txt +1 -0
- package/dist/browser-container/container-static/js/{index.c8fe4ff310.js → index.6e4da8c9ab.js} +9 -9
- package/dist/browser-container/index.html +1 -1
- package/dist/browserExecutor.d.ts +11 -0
- package/dist/configValidation.d.ts +6 -1
- package/dist/hostController.d.ts +2 -6
- package/dist/index.d.ts +4 -3
- package/dist/index.js +539 -292
- package/dist/protocol.d.ts +2 -2
- package/package.json +5 -5
- package/src/AGENTS.md +17 -1
- package/src/browserExecutor.ts +138 -0
- package/src/client/AGENTS.md +20 -13
- package/src/client/entry.ts +62 -2
- package/src/client/snapshot.ts +1 -2
- package/src/concurrency.ts +42 -25
- package/src/configValidation.ts +182 -1
- package/src/hostController.ts +476 -337
- package/src/index.ts +9 -5
- package/src/protocol.ts +2 -2
- package/dist/browser-container/container-static/js/328.37af068e21.js.LICENSE.txt +0 -1
package/src/hostController.ts
CHANGED
|
@@ -9,23 +9,32 @@ import {
|
|
|
9
9
|
type BrowserTestRunOptions,
|
|
10
10
|
type BrowserTestRunResult,
|
|
11
11
|
type CoverageMapData,
|
|
12
|
+
type ListBrowserTestsOptions,
|
|
12
13
|
color,
|
|
13
14
|
createCoverageProvider,
|
|
15
|
+
createRunnerEventSink,
|
|
16
|
+
createSilentConsoleController,
|
|
14
17
|
DEFAULT_TEST_TIMEOUT,
|
|
15
18
|
type FormattedError,
|
|
16
19
|
getNoTestFilesMessage,
|
|
17
20
|
getSetupFiles,
|
|
18
21
|
getTestEntries,
|
|
22
|
+
hasUserRstestConfigPlugins,
|
|
23
|
+
initModifyRstestConfigHooks,
|
|
19
24
|
isDebug,
|
|
20
25
|
type ListCommandResult,
|
|
21
26
|
loadCoverageProvider,
|
|
22
27
|
logger,
|
|
28
|
+
prepareWatchRerunState,
|
|
29
|
+
projectRuntimeConfig,
|
|
23
30
|
PhaseTracker,
|
|
24
31
|
type ProjectContext,
|
|
25
32
|
type Reporter,
|
|
33
|
+
type RunnerEventSink,
|
|
26
34
|
type RstestContext,
|
|
27
|
-
type RuntimeConfig,
|
|
28
35
|
resolveProjectBuildCache,
|
|
36
|
+
resolveSnapshotPathDefault,
|
|
37
|
+
resolveShardedEntries,
|
|
29
38
|
RSTEST_ENV_SYMBOL_KEY,
|
|
30
39
|
rsbuild,
|
|
31
40
|
serializableConfig,
|
|
@@ -36,7 +45,7 @@ import {
|
|
|
36
45
|
} from '@rstest/core/internal/browser';
|
|
37
46
|
import { type BirpcReturn, createBirpc } from 'birpc';
|
|
38
47
|
import openEditor from 'open-editor';
|
|
39
|
-
import {
|
|
48
|
+
import { dirname, join, normalize, relative, resolve } from 'pathe';
|
|
40
49
|
import picomatch from 'picomatch';
|
|
41
50
|
import sirv from 'sirv';
|
|
42
51
|
import { type WebSocket, WebSocketServer } from 'ws';
|
|
@@ -45,6 +54,7 @@ import {
|
|
|
45
54
|
createHostDispatchRouter,
|
|
46
55
|
type HostDispatchRouterOptions,
|
|
47
56
|
} from './dispatchCapabilities';
|
|
57
|
+
import { validateBrowserConfig } from './configValidation';
|
|
48
58
|
import { createHeadedSerialTaskQueue } from './headedSerialTaskQueue';
|
|
49
59
|
import { createHeadlessLatestRerunScheduler } from './headlessLatestRerunScheduler';
|
|
50
60
|
import { attachHeadlessRunnerTransport } from './headlessTransport';
|
|
@@ -97,6 +107,8 @@ import { collectWatchTestFiles, planWatchRerun } from './watchRerunPlanner';
|
|
|
97
107
|
const { createRsbuild, rspack } = rsbuild;
|
|
98
108
|
type RsbuildDevServer = rsbuild.RsbuildDevServer;
|
|
99
109
|
type RsbuildInstance = rsbuild.RsbuildInstance;
|
|
110
|
+
type RsbuildEnvironmentConfig = rsbuild.EnvironmentConfig &
|
|
111
|
+
Pick<rsbuild.RsbuildConfig, 'root'>;
|
|
100
112
|
|
|
101
113
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
102
114
|
const OPTIONS_PLACEHOLDER = '__RSTEST_OPTIONS_PLACEHOLDER__';
|
|
@@ -200,10 +212,6 @@ const getFileTaskId = (testPath: string): string => {
|
|
|
200
212
|
return `file:${testPath}`;
|
|
201
213
|
};
|
|
202
214
|
|
|
203
|
-
const getBufferedLogTaskId = (log: UserConsoleLog): string => {
|
|
204
|
-
return log.taskId ?? getFileTaskId(log.testPath);
|
|
205
|
-
};
|
|
206
|
-
|
|
207
215
|
const createDeferredPromise = <T>(): DeferredPromise<T> => {
|
|
208
216
|
let resolve!: DeferredPromise<T>['resolve'];
|
|
209
217
|
let reject!: DeferredPromise<T>['reject'];
|
|
@@ -433,6 +441,7 @@ type BrowserRuntime = {
|
|
|
433
441
|
dispatchHandlers: Map<string, BrowserDispatchHandler>;
|
|
434
442
|
wss: WebSocketServer;
|
|
435
443
|
rpcManager?: ContainerRpcManager;
|
|
444
|
+
projectEntries: BrowserProjectEntries[];
|
|
436
445
|
};
|
|
437
446
|
|
|
438
447
|
// ============================================================================
|
|
@@ -973,82 +982,25 @@ const getAffectedTestFiles = (
|
|
|
973
982
|
return Array.from(affectedFiles);
|
|
974
983
|
};
|
|
975
984
|
|
|
976
|
-
const
|
|
977
|
-
|
|
978
|
-
)
|
|
979
|
-
|
|
980
|
-
testNamePattern,
|
|
981
|
-
testTimeout,
|
|
982
|
-
passWithNoTests,
|
|
983
|
-
retry,
|
|
984
|
-
globals,
|
|
985
|
-
clearMocks,
|
|
986
|
-
resetMocks,
|
|
987
|
-
restoreMocks,
|
|
988
|
-
unstubEnvs,
|
|
989
|
-
unstubGlobals,
|
|
990
|
-
maxConcurrency,
|
|
991
|
-
printConsoleTrace,
|
|
992
|
-
disableConsoleIntercept,
|
|
993
|
-
testEnvironment,
|
|
994
|
-
hookTimeout,
|
|
995
|
-
isolate,
|
|
996
|
-
coverage,
|
|
997
|
-
snapshotFormat,
|
|
998
|
-
env,
|
|
999
|
-
bail,
|
|
1000
|
-
logHeapUsage,
|
|
1001
|
-
detectAsyncLeaks,
|
|
1002
|
-
chaiConfig,
|
|
1003
|
-
includeTaskLocation,
|
|
1004
|
-
silent,
|
|
1005
|
-
} = project.normalizedConfig;
|
|
985
|
+
const getBrowserProjects = (context: RstestContext): ProjectContext[] =>
|
|
986
|
+
context.projects.filter(
|
|
987
|
+
(project) => project.normalizedConfig.browser.enabled,
|
|
988
|
+
);
|
|
1006
989
|
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
// User-supplied `env` wins so explicit overrides still take effect.
|
|
1014
|
-
// See https://github.com/web-infra-dev/rstest/issues/1351
|
|
1015
|
-
env: {
|
|
1016
|
-
NODE_ENV: process.env.NODE_ENV,
|
|
1017
|
-
RSTEST: 'true',
|
|
1018
|
-
...env,
|
|
1019
|
-
},
|
|
1020
|
-
testNamePattern,
|
|
1021
|
-
testTimeout,
|
|
1022
|
-
hookTimeout,
|
|
1023
|
-
passWithNoTests,
|
|
1024
|
-
retry,
|
|
1025
|
-
globals,
|
|
1026
|
-
clearMocks,
|
|
1027
|
-
resetMocks,
|
|
1028
|
-
restoreMocks,
|
|
1029
|
-
unstubEnvs,
|
|
1030
|
-
unstubGlobals,
|
|
1031
|
-
maxConcurrency,
|
|
1032
|
-
printConsoleTrace,
|
|
1033
|
-
disableConsoleIntercept,
|
|
1034
|
-
testEnvironment,
|
|
1035
|
-
isolate,
|
|
1036
|
-
coverage,
|
|
1037
|
-
snapshotFormat,
|
|
1038
|
-
bail,
|
|
1039
|
-
logHeapUsage,
|
|
1040
|
-
detectAsyncLeaks,
|
|
1041
|
-
chaiConfig,
|
|
1042
|
-
includeTaskLocation,
|
|
1043
|
-
silent,
|
|
1044
|
-
};
|
|
1045
|
-
};
|
|
990
|
+
const getBrowserRsbuildEnvironmentConfig = (
|
|
991
|
+
project: ProjectContext,
|
|
992
|
+
): RsbuildEnvironmentConfig => ({
|
|
993
|
+
plugins: project.normalizedConfig.plugins,
|
|
994
|
+
root: project.rootPath,
|
|
995
|
+
});
|
|
1046
996
|
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
997
|
+
// Max testTimeout across browser projects, used as the host->client RPC timeout.
|
|
998
|
+
const getMaxTestTimeoutForRpc = (projects: ProjectContext[]): number =>
|
|
999
|
+
Math.max(
|
|
1000
|
+
...projects.map(
|
|
1001
|
+
(p) => p.normalizedConfig.testTimeout ?? DEFAULT_TEST_TIMEOUT,
|
|
1002
|
+
),
|
|
1050
1003
|
);
|
|
1051
|
-
};
|
|
1052
1004
|
|
|
1053
1005
|
const getBrowserLaunchOptions = (
|
|
1054
1006
|
project: ProjectContext,
|
|
@@ -1118,10 +1070,11 @@ const resolveProviderForTestPath = ({
|
|
|
1118
1070
|
|
|
1119
1071
|
const collectProjectEntries = async (
|
|
1120
1072
|
context: RstestContext,
|
|
1073
|
+
// The explicit browser-project subset the executor was constructed with. Falls
|
|
1074
|
+
// back to re-deriving from `context` for internal callers (e.g. the watch
|
|
1075
|
+
// plugin) that do not carry the plan's project list.
|
|
1076
|
+
browserProjects: ProjectContext[] = getBrowserProjects(context),
|
|
1121
1077
|
): Promise<BrowserProjectEntries[]> => {
|
|
1122
|
-
// Only collect entries for browser mode projects
|
|
1123
|
-
const browserProjects = getBrowserProjects(context);
|
|
1124
|
-
|
|
1125
1078
|
return Promise.all(
|
|
1126
1079
|
browserProjects.map(async (project) => {
|
|
1127
1080
|
const {
|
|
@@ -1455,16 +1408,28 @@ const registerWatchCleanup = (): void => {
|
|
|
1455
1408
|
|
|
1456
1409
|
const createBrowserRuntime = async ({
|
|
1457
1410
|
context,
|
|
1458
|
-
projectEntries,
|
|
1411
|
+
projectEntries: initialProjectEntries,
|
|
1412
|
+
browserProjects,
|
|
1413
|
+
shardedEntries,
|
|
1414
|
+
freezeShardedEntries,
|
|
1459
1415
|
tempDir,
|
|
1460
1416
|
isWatchMode,
|
|
1461
1417
|
onTriggerRerun,
|
|
1462
1418
|
containerDistPath,
|
|
1463
1419
|
containerDevServer,
|
|
1464
1420
|
forceHeadless,
|
|
1421
|
+
skipProviderLaunch,
|
|
1422
|
+
appliedModifyRstestConfigEnvironments,
|
|
1465
1423
|
}: {
|
|
1466
1424
|
context: RstestContext;
|
|
1467
1425
|
projectEntries: BrowserProjectEntries[];
|
|
1426
|
+
/**
|
|
1427
|
+
* The explicit browser-project subset (plan output). Drives launch-option
|
|
1428
|
+
* consistency and the container origin (`browserProjects[0]`).
|
|
1429
|
+
*/
|
|
1430
|
+
browserProjects: ProjectContext[];
|
|
1431
|
+
shardedEntries?: Map<string, { entries: Record<string, string> }>;
|
|
1432
|
+
freezeShardedEntries?: boolean;
|
|
1468
1433
|
tempDir: string;
|
|
1469
1434
|
isWatchMode: boolean;
|
|
1470
1435
|
onTriggerRerun?: () => Promise<void>;
|
|
@@ -1472,6 +1437,8 @@ const createBrowserRuntime = async ({
|
|
|
1472
1437
|
containerDevServer?: string;
|
|
1473
1438
|
/** Force headless mode regardless of user config (used for list command) */
|
|
1474
1439
|
forceHeadless?: boolean;
|
|
1440
|
+
skipProviderLaunch?: boolean;
|
|
1441
|
+
appliedModifyRstestConfigEnvironments?: Set<string>;
|
|
1475
1442
|
}): Promise<BrowserRuntime> => {
|
|
1476
1443
|
// ---- Shared singletons (created once, wired into every project server) ----
|
|
1477
1444
|
const containerHtmlTemplate = containerDistPath
|
|
@@ -1493,9 +1460,83 @@ const createBrowserRuntime = async ({
|
|
|
1493
1460
|
}
|
|
1494
1461
|
};
|
|
1495
1462
|
|
|
1496
|
-
|
|
1497
|
-
const browserLaunchOptions =
|
|
1463
|
+
let browserLaunchOptions =
|
|
1498
1464
|
ensureConsistentBrowserLaunchOptions(browserProjects);
|
|
1465
|
+
let projectEntries = initialProjectEntries;
|
|
1466
|
+
const manifestModules: Array<{
|
|
1467
|
+
manifestPath: string;
|
|
1468
|
+
project: ProjectContext;
|
|
1469
|
+
modules: Record<string, string>;
|
|
1470
|
+
}> = [];
|
|
1471
|
+
|
|
1472
|
+
const createRuntimeWithoutProvider = (): BrowserRuntime => {
|
|
1473
|
+
const firstProject = browserProjects[0]!;
|
|
1474
|
+
return {
|
|
1475
|
+
projectServers: new Map(),
|
|
1476
|
+
containerServer: {
|
|
1477
|
+
projectName: firstProject.name,
|
|
1478
|
+
environmentName: firstProject.environmentName,
|
|
1479
|
+
rsbuildInstance: undefined as unknown as RsbuildInstance,
|
|
1480
|
+
devServer: {
|
|
1481
|
+
close: async () => undefined,
|
|
1482
|
+
} as RsbuildDevServer,
|
|
1483
|
+
port: 0,
|
|
1484
|
+
manifestPath: '',
|
|
1485
|
+
},
|
|
1486
|
+
browser: undefined as unknown as BrowserProviderBrowser,
|
|
1487
|
+
browserLaunchOptions,
|
|
1488
|
+
wsPort: 0,
|
|
1489
|
+
tempDir,
|
|
1490
|
+
setContainerOptions,
|
|
1491
|
+
dispatchHandlers,
|
|
1492
|
+
wss: undefined as unknown as WebSocketServer,
|
|
1493
|
+
projectEntries,
|
|
1494
|
+
};
|
|
1495
|
+
};
|
|
1496
|
+
|
|
1497
|
+
const getProjectEntry = (project: ProjectContext) =>
|
|
1498
|
+
projectEntries.find(
|
|
1499
|
+
(item) => item.project.environmentName === project.environmentName,
|
|
1500
|
+
);
|
|
1501
|
+
|
|
1502
|
+
const refreshManifestModule = (manifestModule: {
|
|
1503
|
+
manifestPath: string;
|
|
1504
|
+
project: ProjectContext;
|
|
1505
|
+
modules: Record<string, string>;
|
|
1506
|
+
}): void => {
|
|
1507
|
+
const entry = getProjectEntry(manifestModule.project);
|
|
1508
|
+
manifestModule.modules[manifestModule.manifestPath] =
|
|
1509
|
+
generateManifestModule({
|
|
1510
|
+
manifestPath: manifestModule.manifestPath,
|
|
1511
|
+
entries: [
|
|
1512
|
+
{
|
|
1513
|
+
project: manifestModule.project,
|
|
1514
|
+
testFiles: entry?.testFiles ?? [],
|
|
1515
|
+
setupFiles: entry?.setupFiles ?? [],
|
|
1516
|
+
},
|
|
1517
|
+
],
|
|
1518
|
+
isWatchMode,
|
|
1519
|
+
});
|
|
1520
|
+
};
|
|
1521
|
+
|
|
1522
|
+
const refreshProjectEntries = async (): Promise<void> => {
|
|
1523
|
+
validateBrowserConfig(context);
|
|
1524
|
+
browserLaunchOptions =
|
|
1525
|
+
ensureConsistentBrowserLaunchOptions(browserProjects);
|
|
1526
|
+
const updatedShardedEntries = freezeShardedEntries
|
|
1527
|
+
? shardedEntries
|
|
1528
|
+
: context.normalizedConfig.shard
|
|
1529
|
+
? await resolveShardedEntries(context, { silent: true })
|
|
1530
|
+
: shardedEntries;
|
|
1531
|
+
projectEntries = await resolveProjectEntries(
|
|
1532
|
+
context,
|
|
1533
|
+
updatedShardedEntries,
|
|
1534
|
+
browserProjects,
|
|
1535
|
+
);
|
|
1536
|
+
for (const manifestModule of manifestModules) {
|
|
1537
|
+
refreshManifestModule(manifestModule);
|
|
1538
|
+
}
|
|
1539
|
+
};
|
|
1499
1540
|
|
|
1500
1541
|
// Rstest internal aliases that must not be overridden by user config
|
|
1501
1542
|
const browserRuntimePath = fileURLToPath(
|
|
@@ -1591,10 +1632,6 @@ const createBrowserRuntime = async ({
|
|
|
1591
1632
|
}
|
|
1592
1633
|
};
|
|
1593
1634
|
|
|
1594
|
-
const entryByEnvironmentName = new Map(
|
|
1595
|
-
projectEntries.map((entry) => [entry.project.environmentName, entry]),
|
|
1596
|
-
);
|
|
1597
|
-
|
|
1598
1635
|
// ---- Build one isolated rsbuild instance + dev server per project ----
|
|
1599
1636
|
const buildProjectServer = async (
|
|
1600
1637
|
project: ProjectContext,
|
|
@@ -1605,20 +1642,27 @@ const createBrowserRuntime = async ({
|
|
|
1605
1642
|
toSafeVarName(project.environmentName),
|
|
1606
1643
|
VIRTUAL_MANIFEST_FILENAME,
|
|
1607
1644
|
);
|
|
1608
|
-
const entry =
|
|
1609
|
-
const
|
|
1645
|
+
const entry = getProjectEntry(project);
|
|
1646
|
+
const virtualManifestModules = {
|
|
1647
|
+
[manifestPath]: generateManifestModule({
|
|
1648
|
+
manifestPath,
|
|
1649
|
+
entries: [
|
|
1650
|
+
{
|
|
1651
|
+
project,
|
|
1652
|
+
testFiles: entry?.testFiles ?? [],
|
|
1653
|
+
setupFiles: entry?.setupFiles ?? [],
|
|
1654
|
+
},
|
|
1655
|
+
],
|
|
1656
|
+
isWatchMode,
|
|
1657
|
+
}),
|
|
1658
|
+
};
|
|
1659
|
+
const virtualManifestPlugin = new rspack.experiments.VirtualModulesPlugin(
|
|
1660
|
+
virtualManifestModules,
|
|
1661
|
+
);
|
|
1662
|
+
manifestModules.push({
|
|
1610
1663
|
manifestPath,
|
|
1611
|
-
|
|
1612
|
-
|
|
1613
|
-
project,
|
|
1614
|
-
testFiles: entry?.testFiles ?? [],
|
|
1615
|
-
setupFiles: entry?.setupFiles ?? [],
|
|
1616
|
-
},
|
|
1617
|
-
],
|
|
1618
|
-
isWatchMode,
|
|
1619
|
-
});
|
|
1620
|
-
const virtualManifestPlugin = new rspack.experiments.VirtualModulesPlugin({
|
|
1621
|
-
[manifestPath]: manifestSource,
|
|
1664
|
+
project,
|
|
1665
|
+
modules: virtualManifestModules,
|
|
1622
1666
|
});
|
|
1623
1667
|
|
|
1624
1668
|
const rstestInternalAliases = {
|
|
@@ -1631,11 +1675,10 @@ const createBrowserRuntime = async ({
|
|
|
1631
1675
|
const enableHmr = shouldEnableBrowserHmr(isWatchMode, isHeadless);
|
|
1632
1676
|
|
|
1633
1677
|
const rsbuildInstance = await createRsbuild({
|
|
1634
|
-
callerName: 'rstest
|
|
1678
|
+
callerName: 'rstest',
|
|
1635
1679
|
rsbuildConfig: {
|
|
1636
1680
|
root: context.rootPath,
|
|
1637
1681
|
mode: 'development',
|
|
1638
|
-
plugins: project.normalizedConfig.plugins || [],
|
|
1639
1682
|
server: {
|
|
1640
1683
|
printUrls: false,
|
|
1641
1684
|
// Each project gets its own dev server. Honor an explicitly
|
|
@@ -1649,11 +1692,24 @@ const createBrowserRuntime = async ({
|
|
|
1649
1692
|
},
|
|
1650
1693
|
dev: createBrowserRsbuildDevConfig(enableHmr),
|
|
1651
1694
|
environments: {
|
|
1652
|
-
[project.environmentName]:
|
|
1695
|
+
[project.environmentName]:
|
|
1696
|
+
getBrowserRsbuildEnvironmentConfig(project),
|
|
1653
1697
|
},
|
|
1654
1698
|
},
|
|
1655
1699
|
});
|
|
1656
1700
|
|
|
1701
|
+
initModifyRstestConfigHooks(
|
|
1702
|
+
context,
|
|
1703
|
+
rsbuildInstance,
|
|
1704
|
+
[project],
|
|
1705
|
+
[project],
|
|
1706
|
+
{
|
|
1707
|
+
getEnvironmentConfig: getBrowserRsbuildEnvironmentConfig,
|
|
1708
|
+
onModifyRstestConfigApplied: refreshProjectEntries,
|
|
1709
|
+
appliedEnvironmentNames: appliedModifyRstestConfigEnvironments,
|
|
1710
|
+
},
|
|
1711
|
+
);
|
|
1712
|
+
|
|
1657
1713
|
// Add plugin to merge user Rsbuild config with rstest required config
|
|
1658
1714
|
rsbuildInstance.addPlugins([
|
|
1659
1715
|
{
|
|
@@ -1736,7 +1792,7 @@ const createBrowserRuntime = async ({
|
|
|
1736
1792
|
|
|
1737
1793
|
// Extract and merge sourcemaps from pre-built @rstest/core files
|
|
1738
1794
|
// This preserves the sourcemap chain for inline snapshot support
|
|
1739
|
-
// See: https://rspack.
|
|
1795
|
+
// See: https://rspack.rs/config/module-rules#rulesextractsourcemap
|
|
1740
1796
|
const browserRuntimeDir = dirname(browserRuntimePath);
|
|
1741
1797
|
rspackConfig.module = rspackConfig.module || {};
|
|
1742
1798
|
rspackConfig.module.rules =
|
|
@@ -1822,6 +1878,20 @@ const createBrowserRuntime = async ({
|
|
|
1822
1878
|
]);
|
|
1823
1879
|
}
|
|
1824
1880
|
|
|
1881
|
+
if (skipProviderLaunch) {
|
|
1882
|
+
await rsbuildInstance.initConfigs({ action: 'dev' });
|
|
1883
|
+
return {
|
|
1884
|
+
projectName: project.name,
|
|
1885
|
+
environmentName: project.environmentName,
|
|
1886
|
+
rsbuildInstance,
|
|
1887
|
+
devServer: {
|
|
1888
|
+
close: async () => undefined,
|
|
1889
|
+
} as RsbuildDevServer,
|
|
1890
|
+
port: 0,
|
|
1891
|
+
manifestPath,
|
|
1892
|
+
};
|
|
1893
|
+
}
|
|
1894
|
+
|
|
1825
1895
|
// Register coverage plugin if this project enables coverage
|
|
1826
1896
|
const coverage = project.normalizedConfig.coverage;
|
|
1827
1897
|
if (coverage?.enabled && context.command !== 'list') {
|
|
@@ -1948,6 +2018,10 @@ const createBrowserRuntime = async ({
|
|
|
1948
2018
|
throw error;
|
|
1949
2019
|
}
|
|
1950
2020
|
|
|
2021
|
+
if (skipProviderLaunch) {
|
|
2022
|
+
return createRuntimeWithoutProvider();
|
|
2023
|
+
}
|
|
2024
|
+
|
|
1951
2025
|
// browserProjects is non-empty (ensureConsistentBrowserLaunchOptions throws
|
|
1952
2026
|
// otherwise) and index 0 is the designated container origin.
|
|
1953
2027
|
const containerServer = projectServers.get(browserProjects[0]!.name)!;
|
|
@@ -1983,6 +2057,7 @@ const createBrowserRuntime = async ({
|
|
|
1983
2057
|
setContainerOptions,
|
|
1984
2058
|
dispatchHandlers,
|
|
1985
2059
|
wss,
|
|
2060
|
+
projectEntries,
|
|
1986
2061
|
};
|
|
1987
2062
|
} catch (error) {
|
|
1988
2063
|
wss.close();
|
|
@@ -1993,10 +2068,10 @@ const createBrowserRuntime = async ({
|
|
|
1993
2068
|
|
|
1994
2069
|
async function resolveProjectEntries(
|
|
1995
2070
|
context: RstestContext,
|
|
1996
|
-
shardedEntries
|
|
2071
|
+
shardedEntries: Map<string, { entries: Record<string, string> }> | undefined,
|
|
2072
|
+
browserProjects: ProjectContext[],
|
|
1997
2073
|
): Promise<BrowserProjectEntries[]> {
|
|
1998
2074
|
if (shardedEntries) {
|
|
1999
|
-
const browserProjects = getBrowserProjects(context);
|
|
2000
2075
|
const projectEntries: BrowserProjectEntries[] = [];
|
|
2001
2076
|
for (const project of browserProjects) {
|
|
2002
2077
|
const entryInfo = shardedEntries.get(project.environmentName);
|
|
@@ -2014,7 +2089,7 @@ async function resolveProjectEntries(
|
|
|
2014
2089
|
}
|
|
2015
2090
|
return projectEntries;
|
|
2016
2091
|
}
|
|
2017
|
-
return collectProjectEntries(context);
|
|
2092
|
+
return collectProjectEntries(context, browserProjects);
|
|
2018
2093
|
}
|
|
2019
2094
|
|
|
2020
2095
|
// ============================================================================
|
|
@@ -2026,11 +2101,18 @@ export const runBrowserController = async (
|
|
|
2026
2101
|
options?: BrowserTestRunOptions,
|
|
2027
2102
|
): Promise<BrowserTestRunResult | void> => {
|
|
2028
2103
|
const {
|
|
2029
|
-
skipOnTestRunEnd = false,
|
|
2030
2104
|
allowEmptyWatchRun = false,
|
|
2105
|
+
allowEmptyRun = false,
|
|
2106
|
+
filesOnly = false,
|
|
2031
2107
|
onTraceEvents,
|
|
2108
|
+
env,
|
|
2032
2109
|
} = options ?? {};
|
|
2033
2110
|
const buildStart = Date.now();
|
|
2111
|
+
// Non-watch vs watch is the live switch for self-finalize: in non-watch runs
|
|
2112
|
+
// core owns the unified finalize (reporters, exit code, coverage) through
|
|
2113
|
+
// `finalizeRunCycle`, so the host never self-finalizes and always returns a
|
|
2114
|
+
// fully-populated result with `close`. Watch reruns keep their host-driven
|
|
2115
|
+
// per-rerun finalize.
|
|
2034
2116
|
const isWatchMode = context.command === 'watch';
|
|
2035
2117
|
|
|
2036
2118
|
// Per-file PhaseTrackers, populated only when `--trace` is on (caller
|
|
@@ -2041,7 +2123,11 @@ export const runBrowserController = async (
|
|
|
2041
2123
|
const phaseTrackers = onTraceEvents
|
|
2042
2124
|
? new Map<string, PhaseTracker>()
|
|
2043
2125
|
: undefined;
|
|
2044
|
-
|
|
2126
|
+
// Explicit projects input (plan output) replaces re-deriving `browser.enabled`
|
|
2127
|
+
// projects from `context`, whose `projects` array is mutated during planning.
|
|
2128
|
+
// Falls back to re-derivation only when the caller passes no list at all —
|
|
2129
|
+
// an explicit empty subset must stay empty, not widen to every project.
|
|
2130
|
+
const browserProjects = options?.projects ?? getBrowserProjects(context);
|
|
2045
2131
|
const useHeadlessDirect = browserProjects.every(
|
|
2046
2132
|
(project) => project.normalizedConfig.browser.headless,
|
|
2047
2133
|
);
|
|
@@ -2109,7 +2195,7 @@ export const runBrowserController = async (
|
|
|
2109
2195
|
close,
|
|
2110
2196
|
};
|
|
2111
2197
|
|
|
2112
|
-
if (
|
|
2198
|
+
if (isWatchMode) {
|
|
2113
2199
|
for (const reporter of context.reporters) {
|
|
2114
2200
|
await (reporter as Reporter).onTestRunEnd?.({
|
|
2115
2201
|
results: [],
|
|
@@ -2133,11 +2219,16 @@ export const runBrowserController = async (
|
|
|
2133
2219
|
error: unknown,
|
|
2134
2220
|
cleanup?: () => Promise<void>,
|
|
2135
2221
|
): Promise<BrowserTestRunResult> => {
|
|
2136
|
-
|
|
2222
|
+
// Non-watch runs defer the exit code to core's `finalizeRunCycle`, which
|
|
2223
|
+
// raises it from the returned outcome's `errors`. Watch reruns keep owning
|
|
2224
|
+
// their own exit code.
|
|
2225
|
+
if (isWatchMode) {
|
|
2226
|
+
ensureProcessExitCode(1);
|
|
2227
|
+
}
|
|
2137
2228
|
|
|
2138
2229
|
const normalizedError = toError(error);
|
|
2139
2230
|
|
|
2140
|
-
if (cleanup &&
|
|
2231
|
+
if (cleanup && !isWatchMode) {
|
|
2141
2232
|
return buildErrorResult(normalizedError, cleanup);
|
|
2142
2233
|
}
|
|
2143
2234
|
|
|
@@ -2159,7 +2250,7 @@ export const runBrowserController = async (
|
|
|
2159
2250
|
};
|
|
2160
2251
|
|
|
2161
2252
|
const notifyTestRunStart = async (): Promise<void> => {
|
|
2162
|
-
if (
|
|
2253
|
+
if (!isWatchMode) {
|
|
2163
2254
|
return;
|
|
2164
2255
|
}
|
|
2165
2256
|
|
|
@@ -2188,7 +2279,7 @@ export const runBrowserController = async (
|
|
|
2188
2279
|
unhandledErrors?: Error[];
|
|
2189
2280
|
filterRerunTestPaths?: string[];
|
|
2190
2281
|
}): Promise<void> => {
|
|
2191
|
-
if (
|
|
2282
|
+
if (!isWatchMode) {
|
|
2192
2283
|
return;
|
|
2193
2284
|
}
|
|
2194
2285
|
|
|
@@ -2249,19 +2340,38 @@ export const runBrowserController = async (
|
|
|
2249
2340
|
}
|
|
2250
2341
|
}
|
|
2251
2342
|
|
|
2252
|
-
|
|
2343
|
+
let projectEntries = await resolveProjectEntries(
|
|
2253
2344
|
context,
|
|
2254
2345
|
options?.shardedEntries,
|
|
2346
|
+
browserProjects,
|
|
2255
2347
|
);
|
|
2256
|
-
|
|
2348
|
+
let totalTests = projectEntries.reduce(
|
|
2257
2349
|
(total, item) => total + item.testFiles.length,
|
|
2258
2350
|
0,
|
|
2259
2351
|
);
|
|
2260
2352
|
const shouldKeepWatchingWithEmptySet = isWatchMode && allowEmptyWatchRun;
|
|
2353
|
+
const shouldInitializeEmptyBrowserHooks =
|
|
2354
|
+
totalTests === 0 && hasUserRstestConfigPlugins(browserProjects);
|
|
2261
2355
|
|
|
2262
|
-
|
|
2356
|
+
const createEmptyRunResult = (): BrowserTestRunResult => {
|
|
2357
|
+
const elapsed = Math.max(0, Date.now() - buildStart);
|
|
2358
|
+
return {
|
|
2359
|
+
results: [],
|
|
2360
|
+
testResults: [],
|
|
2361
|
+
duration: {
|
|
2362
|
+
totalTime: elapsed,
|
|
2363
|
+
buildTime: elapsed,
|
|
2364
|
+
testTime: 0,
|
|
2365
|
+
},
|
|
2366
|
+
hasFailure: false,
|
|
2367
|
+
getSourcemap: getBrowserSourcemap,
|
|
2368
|
+
resolveSourcemap: resolveBrowserSourcemap,
|
|
2369
|
+
};
|
|
2370
|
+
};
|
|
2371
|
+
|
|
2372
|
+
const reportEmptyTestSet = (): boolean => {
|
|
2263
2373
|
const code = context.normalizedConfig.passWithNoTests ? 0 : 1;
|
|
2264
|
-
if (!
|
|
2374
|
+
if (isWatchMode || !allowEmptyRun) {
|
|
2265
2375
|
const message = shouldKeepWatchingWithEmptySet
|
|
2266
2376
|
? 'No test files found.'
|
|
2267
2377
|
: getNoTestFilesMessage({
|
|
@@ -2288,15 +2398,30 @@ export const runBrowserController = async (
|
|
|
2288
2398
|
}
|
|
2289
2399
|
}
|
|
2290
2400
|
|
|
2291
|
-
|
|
2401
|
+
// In non-watch runs the host returns a void outcome and core's
|
|
2402
|
+
// `reportNoTestFiles` owns the exit code and the no-test reporter lifecycle;
|
|
2403
|
+
// the host must not set the code itself. Watch keeps its own exit code.
|
|
2404
|
+
if (
|
|
2405
|
+
isWatchMode &&
|
|
2406
|
+
code !== 0 &&
|
|
2407
|
+
!shouldKeepWatchingWithEmptySet &&
|
|
2408
|
+
!allowEmptyRun
|
|
2409
|
+
) {
|
|
2292
2410
|
ensureProcessExitCode(code);
|
|
2293
2411
|
}
|
|
2294
|
-
|
|
2295
|
-
|
|
2412
|
+
|
|
2413
|
+
return !shouldKeepWatchingWithEmptySet;
|
|
2414
|
+
};
|
|
2415
|
+
|
|
2416
|
+
if (totalTests === 0 && !shouldInitializeEmptyBrowserHooks) {
|
|
2417
|
+
if (reportEmptyTestSet()) {
|
|
2418
|
+
return allowEmptyRun ? createEmptyRunResult() : undefined;
|
|
2296
2419
|
}
|
|
2297
2420
|
}
|
|
2298
2421
|
|
|
2299
|
-
|
|
2422
|
+
if (!filesOnly) {
|
|
2423
|
+
await notifyTestRunStart();
|
|
2424
|
+
}
|
|
2300
2425
|
|
|
2301
2426
|
const enableCliShortcuts = isWatchMode && isBrowserWatchCliShortcutsEnabled();
|
|
2302
2427
|
const browserTempOutputRoot = context.normalizedConfig.output.distPath.root;
|
|
@@ -2327,6 +2452,9 @@ export const runBrowserController = async (
|
|
|
2327
2452
|
runtime = await createBrowserRuntime({
|
|
2328
2453
|
context,
|
|
2329
2454
|
projectEntries,
|
|
2455
|
+
browserProjects,
|
|
2456
|
+
shardedEntries: options?.shardedEntries,
|
|
2457
|
+
freezeShardedEntries: options?.freezeShardedEntries,
|
|
2330
2458
|
tempDir,
|
|
2331
2459
|
isWatchMode,
|
|
2332
2460
|
onTriggerRerun: isWatchMode
|
|
@@ -2336,6 +2464,9 @@ export const runBrowserController = async (
|
|
|
2336
2464
|
: undefined,
|
|
2337
2465
|
containerDistPath,
|
|
2338
2466
|
containerDevServer,
|
|
2467
|
+
skipProviderLaunch: filesOnly,
|
|
2468
|
+
appliedModifyRstestConfigEnvironments:
|
|
2469
|
+
options?.appliedModifyRstestConfigEnvironments,
|
|
2339
2470
|
});
|
|
2340
2471
|
} catch (error) {
|
|
2341
2472
|
return failWithError(error, async () => {
|
|
@@ -2355,9 +2486,37 @@ export const runBrowserController = async (
|
|
|
2355
2486
|
}
|
|
2356
2487
|
}
|
|
2357
2488
|
|
|
2358
|
-
|
|
2489
|
+
projectEntries = runtime.projectEntries;
|
|
2490
|
+
totalTests = projectEntries.reduce(
|
|
2491
|
+
(total, item) => total + item.testFiles.length,
|
|
2492
|
+
0,
|
|
2493
|
+
);
|
|
2494
|
+
|
|
2359
2495
|
const buildTime = Date.now() - buildStart;
|
|
2360
2496
|
|
|
2497
|
+
if (filesOnly) {
|
|
2498
|
+
return {
|
|
2499
|
+
results: [],
|
|
2500
|
+
testResults: [],
|
|
2501
|
+
duration: {
|
|
2502
|
+
totalTime: buildTime,
|
|
2503
|
+
buildTime,
|
|
2504
|
+
testTime: 0,
|
|
2505
|
+
},
|
|
2506
|
+
hasFailure: false,
|
|
2507
|
+
getSourcemap: getBrowserSourcemap,
|
|
2508
|
+
resolveSourcemap: resolveBrowserSourcemap,
|
|
2509
|
+
close: () => destroyBrowserRuntime(runtime),
|
|
2510
|
+
};
|
|
2511
|
+
}
|
|
2512
|
+
|
|
2513
|
+
if (totalTests === 0 && reportEmptyTestSet()) {
|
|
2514
|
+
await destroyBrowserRuntime(runtime);
|
|
2515
|
+
return allowEmptyRun ? createEmptyRunResult() : undefined;
|
|
2516
|
+
}
|
|
2517
|
+
|
|
2518
|
+
const { browser, browserLaunchOptions, wsPort, wss } = runtime;
|
|
2519
|
+
|
|
2361
2520
|
// Collect all test files from project entries with project info
|
|
2362
2521
|
// Normalize paths to posix format for cross-platform compatibility
|
|
2363
2522
|
const allTestFiles: TestFileInfo[] = projectEntries.flatMap((entry) =>
|
|
@@ -2374,17 +2533,17 @@ export const runBrowserController = async (
|
|
|
2374
2533
|
name: project.name,
|
|
2375
2534
|
environmentName: project.environmentName,
|
|
2376
2535
|
projectRoot: normalize(project.rootPath),
|
|
2377
|
-
runtimeConfig: serializableConfig(
|
|
2536
|
+
runtimeConfig: serializableConfig(
|
|
2537
|
+
// `env` is the post-globalSetup change-set from the core pre-cycle
|
|
2538
|
+
// stage; the projection layers it between the static base and the
|
|
2539
|
+
// user `test.env` config.
|
|
2540
|
+
projectRuntimeConfig(project, { envMode: 'static', envOverlay: env }),
|
|
2541
|
+
),
|
|
2378
2542
|
viewport: project.normalizedConfig.browser.viewport,
|
|
2379
2543
|
}),
|
|
2380
2544
|
);
|
|
2381
2545
|
|
|
2382
|
-
|
|
2383
|
-
const maxTestTimeoutForRpc = Math.max(
|
|
2384
|
-
...browserProjects.map(
|
|
2385
|
-
(p) => p.normalizedConfig.testTimeout ?? DEFAULT_TEST_TIMEOUT,
|
|
2386
|
-
),
|
|
2387
|
-
);
|
|
2546
|
+
const maxTestTimeoutForRpc = getMaxTestTimeoutForRpc(browserProjects);
|
|
2388
2547
|
|
|
2389
2548
|
const projectRunnerUrls = Object.fromEntries(
|
|
2390
2549
|
[...runtime.projectServers].map(([name, server]) => [
|
|
@@ -2497,18 +2656,64 @@ export const runBrowserController = async (
|
|
|
2497
2656
|
const caseResults: TestResult[] = [];
|
|
2498
2657
|
let fatalError: Error | null = null;
|
|
2499
2658
|
|
|
2659
|
+
// Runner lifecycle events flow through the shared RunnerEventSink (the same
|
|
2660
|
+
// pump the node pool uses), so browser mode feeds stateManager and fans out to
|
|
2661
|
+
// reporters via one implementation. One sink is bound per browser project up
|
|
2662
|
+
// front from the executor's own project plan (`browserProjects`) — never from
|
|
2663
|
+
// `context.projects`, which planning mutates to also contain node projects. The
|
|
2664
|
+
// previous lazy resolver fell back to `context.projects[0]`, so a browser event
|
|
2665
|
+
// could be attributed to a *node* project's config; binding per browser project
|
|
2666
|
+
// here removes that fallback and keeps per-project `onConsoleLog` filtering and
|
|
2667
|
+
// `resolveSnapshotPath` correct across browser projects that share a relative
|
|
2668
|
+
// test path.
|
|
2669
|
+
const runnerSinks = new Map<string, RunnerEventSink>(
|
|
2670
|
+
browserProjects.map((project) => [
|
|
2671
|
+
project.name,
|
|
2672
|
+
createRunnerEventSink(context, project.normalizedConfig),
|
|
2673
|
+
]),
|
|
2674
|
+
);
|
|
2675
|
+
const firstBrowserSink = runnerSinks.get(browserProjects[0]!.name)!;
|
|
2676
|
+
|
|
2677
|
+
// testPath -> owning project name, stamped from the authoritative client
|
|
2678
|
+
// file-start event (it carries the manifest-resolved projectName) before any
|
|
2679
|
+
// other per-file event for that path fires — including on watch reruns, so the
|
|
2680
|
+
// mapping stays correct when a rerun adds a file. Fully eliminating this map in
|
|
2681
|
+
// favor of a project stamp on every wire event is deferred (it would add
|
|
2682
|
+
// `project` to the shared `TestResult`/`TestFileResult` payloads).
|
|
2683
|
+
const projectNameByTestPath = new Map<string, string>();
|
|
2684
|
+
|
|
2685
|
+
const sinkForProjectName = (projectName: string): RunnerEventSink =>
|
|
2686
|
+
runnerSinks.get(projectName) ?? firstBrowserSink;
|
|
2687
|
+
|
|
2688
|
+
const sinkForTestPath = (testPath: string): RunnerEventSink => {
|
|
2689
|
+
const projectName = projectNameByTestPath.get(testPath);
|
|
2690
|
+
return projectName ? sinkForProjectName(projectName) : firstBrowserSink;
|
|
2691
|
+
};
|
|
2692
|
+
|
|
2693
|
+
// Silent-console buffering runs through the shared controller — the same
|
|
2694
|
+
// engine the node worker uses — so `silent: 'passed-only'` buffers logs and
|
|
2695
|
+
// replays only the failing tasks'. Intercepted replays route through the
|
|
2696
|
+
// owning project's sink, so they honor per-project `onConsoleLog` and
|
|
2697
|
+
// `disableConsoleIntercept` (the browser host previously flushed straight to
|
|
2698
|
+
// reporters, bypassing both). `writeOriginalLog` is a host-side no-op: page
|
|
2699
|
+
// logs have no host "original stream" — the page console and headed terminal
|
|
2700
|
+
// forwarding already show them, so re-emitting here would double-print.
|
|
2701
|
+
const silentConsoleController = createSilentConsoleController({
|
|
2702
|
+
runtimeConfig: {
|
|
2703
|
+
silent: context.normalizedConfig.silent,
|
|
2704
|
+
disableConsoleIntercept: context.normalizedConfig.disableConsoleIntercept,
|
|
2705
|
+
},
|
|
2706
|
+
emitInterceptedLog: (log) =>
|
|
2707
|
+
sinkForTestPath(log.testPath).onConsoleLog(log),
|
|
2708
|
+
writeOriginalLog: () => {},
|
|
2709
|
+
});
|
|
2710
|
+
|
|
2500
2711
|
const snapshotRpcMethods = {
|
|
2501
2712
|
async resolveSnapshotPath(testPath: string): Promise<string> {
|
|
2502
|
-
|
|
2503
|
-
|
|
2504
|
-
context.normalizedConfig.resolveSnapshotPath
|
|
2505
|
-
|
|
2506
|
-
join(
|
|
2507
|
-
dirname(testPath),
|
|
2508
|
-
'__snapshots__',
|
|
2509
|
-
`${basename(testPath)}${snapExtension}`,
|
|
2510
|
-
));
|
|
2511
|
-
return resolver(testPath, snapExtension);
|
|
2713
|
+
return resolveSnapshotPathDefault(
|
|
2714
|
+
testPath,
|
|
2715
|
+
context.normalizedConfig.resolveSnapshotPath,
|
|
2716
|
+
);
|
|
2512
2717
|
},
|
|
2513
2718
|
async readSnapshotFile(filepath: string): Promise<string | null> {
|
|
2514
2719
|
try {
|
|
@@ -2534,6 +2739,7 @@ export const runBrowserController = async (
|
|
|
2534
2739
|
const handleTestFileStart = async (
|
|
2535
2740
|
payload: TestFileStartPayload,
|
|
2536
2741
|
): Promise<void> => {
|
|
2742
|
+
projectNameByTestPath.set(payload.testPath, payload.projectName);
|
|
2537
2743
|
if (phaseTrackers) {
|
|
2538
2744
|
const tracker = new PhaseTracker({
|
|
2539
2745
|
trace: {
|
|
@@ -2545,51 +2751,37 @@ export const runBrowserController = async (
|
|
|
2545
2751
|
tracker.transition('prepare');
|
|
2546
2752
|
phaseTrackers.set(payload.testPath, tracker);
|
|
2547
2753
|
}
|
|
2548
|
-
|
|
2549
|
-
|
|
2550
|
-
|
|
2551
|
-
|
|
2552
|
-
|
|
2553
|
-
|
|
2554
|
-
|
|
2555
|
-
),
|
|
2556
|
-
);
|
|
2754
|
+
// The client sends `{ testPath, projectName }`; the sink adapter builds the
|
|
2755
|
+
// `TestFileInfo` the reporters and stateManager expect.
|
|
2756
|
+
await sinkForProjectName(payload.projectName).onTestFileStart({
|
|
2757
|
+
testId: getFileTaskId(payload.testPath),
|
|
2758
|
+
testPath: payload.testPath,
|
|
2759
|
+
tests: [],
|
|
2760
|
+
});
|
|
2557
2761
|
};
|
|
2558
2762
|
|
|
2559
2763
|
const handleTestFileReady = async (
|
|
2560
2764
|
payload: TestFileReadyPayload,
|
|
2561
2765
|
): Promise<void> => {
|
|
2562
2766
|
phaseTrackers?.get(payload.testPath)?.transition('tests');
|
|
2563
|
-
await
|
|
2564
|
-
context.reporters.map((reporter) =>
|
|
2565
|
-
(reporter as Reporter).onTestFileReady?.(payload),
|
|
2566
|
-
),
|
|
2567
|
-
);
|
|
2767
|
+
await sinkForTestPath(payload.testPath).onTestFileReady(payload);
|
|
2568
2768
|
};
|
|
2569
2769
|
|
|
2570
2770
|
const handleTestSuiteStart = async (
|
|
2571
2771
|
payload: TestSuiteStartPayload,
|
|
2572
2772
|
): Promise<void> => {
|
|
2573
2773
|
phaseTrackers?.get(payload.testPath)?.recordSuiteStart(payload);
|
|
2574
|
-
await
|
|
2575
|
-
context.reporters.map((reporter) =>
|
|
2576
|
-
(reporter as Reporter).onTestSuiteStart?.(payload),
|
|
2577
|
-
),
|
|
2578
|
-
);
|
|
2774
|
+
await sinkForTestPath(payload.testPath).onTestSuiteStart(payload);
|
|
2579
2775
|
};
|
|
2580
2776
|
|
|
2581
2777
|
const handleTestSuiteResult = async (
|
|
2582
2778
|
payload: TestSuiteResultPayload,
|
|
2583
2779
|
): Promise<void> => {
|
|
2584
2780
|
phaseTrackers?.get(payload.testPath)?.recordSuiteResult(payload);
|
|
2585
|
-
await
|
|
2586
|
-
context.reporters.map((reporter) =>
|
|
2587
|
-
(reporter as Reporter).onTestSuiteResult?.(payload),
|
|
2588
|
-
),
|
|
2589
|
-
);
|
|
2781
|
+
await sinkForTestPath(payload.testPath).onTestSuiteResult(payload);
|
|
2590
2782
|
|
|
2591
2783
|
if (context.normalizedConfig.silent === 'passed-only') {
|
|
2592
|
-
|
|
2784
|
+
silentConsoleController.flushBufferedLogsForTask({
|
|
2593
2785
|
taskId: payload.testId,
|
|
2594
2786
|
status: payload.status,
|
|
2595
2787
|
taskParentNames: payload.parentNames,
|
|
@@ -2603,24 +2795,17 @@ export const runBrowserController = async (
|
|
|
2603
2795
|
payload: TestCaseStartPayload,
|
|
2604
2796
|
): Promise<void> => {
|
|
2605
2797
|
phaseTrackers?.get(payload.testPath)?.recordCaseStart(payload);
|
|
2606
|
-
await
|
|
2607
|
-
|
|
2608
|
-
(reporter as Reporter).onTestCaseStart?.(payload),
|
|
2609
|
-
),
|
|
2610
|
-
);
|
|
2798
|
+
// Fire-and-forget on both transports (the sink does not await case-start).
|
|
2799
|
+
sinkForTestPath(payload.testPath).onTestCaseStart(payload);
|
|
2611
2800
|
};
|
|
2612
2801
|
|
|
2613
2802
|
const handleTestCaseResult = async (payload: TestResult): Promise<void> => {
|
|
2614
2803
|
caseResults.push(payload);
|
|
2615
2804
|
phaseTrackers?.get(payload.testPath)?.recordCaseResult(payload);
|
|
2616
|
-
await
|
|
2617
|
-
context.reporters.map((reporter) =>
|
|
2618
|
-
(reporter as Reporter).onTestCaseResult?.(payload),
|
|
2619
|
-
),
|
|
2620
|
-
);
|
|
2805
|
+
await sinkForTestPath(payload.testPath).onTestCaseResult(payload);
|
|
2621
2806
|
|
|
2622
2807
|
if (context.normalizedConfig.silent === 'passed-only') {
|
|
2623
|
-
|
|
2808
|
+
silentConsoleController.flushBufferedLogsForTask({
|
|
2624
2809
|
taskId: payload.testId,
|
|
2625
2810
|
status: payload.status,
|
|
2626
2811
|
taskParentNames: payload.parentNames,
|
|
@@ -2635,9 +2820,6 @@ export const runBrowserController = async (
|
|
|
2635
2820
|
): Promise<void> => {
|
|
2636
2821
|
reporterResults.push(payload);
|
|
2637
2822
|
context.updateReporterResultState([payload], payload.results);
|
|
2638
|
-
if (payload.snapshotResult) {
|
|
2639
|
-
context.snapshotManager.add(payload.snapshotResult);
|
|
2640
|
-
}
|
|
2641
2823
|
|
|
2642
2824
|
if (phaseTrackers) {
|
|
2643
2825
|
const tracker = phaseTrackers.get(payload.testPath);
|
|
@@ -2650,7 +2832,7 @@ export const runBrowserController = async (
|
|
|
2650
2832
|
}
|
|
2651
2833
|
|
|
2652
2834
|
if (context.normalizedConfig.silent === 'passed-only') {
|
|
2653
|
-
|
|
2835
|
+
silentConsoleController.flushBufferedLogsForTask({
|
|
2654
2836
|
taskId: payload.testId,
|
|
2655
2837
|
status: payload.status,
|
|
2656
2838
|
taskParentNames: payload.parentNames,
|
|
@@ -2659,12 +2841,12 @@ export const runBrowserController = async (
|
|
|
2659
2841
|
});
|
|
2660
2842
|
}
|
|
2661
2843
|
|
|
2662
|
-
|
|
2663
|
-
|
|
2664
|
-
|
|
2665
|
-
|
|
2666
|
-
);
|
|
2667
|
-
if (payload.status === 'fail') {
|
|
2844
|
+
// Feeds stateManager, fans out onTestFileResult to reporters, and ingests
|
|
2845
|
+
// payload.snapshotResult (the snapshotManager.add moved into the sink).
|
|
2846
|
+
await sinkForTestPath(payload.testPath).onTestFileResult(payload);
|
|
2847
|
+
// In non-watch runs core owns the exit code via `finalizeRunCycle` (the
|
|
2848
|
+
// failing file rides the returned outcome); watch reruns set it here.
|
|
2849
|
+
if (isWatchMode && payload.status === 'fail') {
|
|
2668
2850
|
ensureProcessExitCode(1);
|
|
2669
2851
|
}
|
|
2670
2852
|
};
|
|
@@ -2681,135 +2863,17 @@ export const runBrowserController = async (
|
|
|
2681
2863
|
type: payload.type,
|
|
2682
2864
|
trace: payload.trace,
|
|
2683
2865
|
};
|
|
2684
|
-
|
|
2685
|
-
return;
|
|
2686
|
-
}
|
|
2687
|
-
|
|
2688
|
-
if (context.normalizedConfig.silent === 'passed-only') {
|
|
2689
|
-
bufferConsoleLog(log);
|
|
2690
|
-
return;
|
|
2691
|
-
}
|
|
2692
|
-
|
|
2693
|
-
if (context.normalizedConfig.disableConsoleIntercept) {
|
|
2694
|
-
return;
|
|
2695
|
-
}
|
|
2696
|
-
|
|
2697
|
-
await emitUserConsoleLog(log);
|
|
2866
|
+
silentConsoleController.onConsoleLog(log);
|
|
2698
2867
|
};
|
|
2699
2868
|
|
|
2700
2869
|
const handleFatal = async (payload: FatalPayload): Promise<void> => {
|
|
2701
2870
|
const error = new Error(payload.message);
|
|
2702
2871
|
error.stack = payload.stack;
|
|
2703
2872
|
fatalError = error;
|
|
2704
|
-
|
|
2705
|
-
|
|
2706
|
-
|
|
2707
|
-
|
|
2708
|
-
const suiteIdsByChain = new Map<string, string>();
|
|
2709
|
-
|
|
2710
|
-
const getSuiteChainKey = (names: string[]): string => {
|
|
2711
|
-
return names.join('\u0000');
|
|
2712
|
-
};
|
|
2713
|
-
|
|
2714
|
-
const pushTaskId = (taskIds: string[], taskId: string): void => {
|
|
2715
|
-
if (!taskIds.includes(taskId)) {
|
|
2716
|
-
taskIds.push(taskId);
|
|
2717
|
-
}
|
|
2718
|
-
};
|
|
2719
|
-
|
|
2720
|
-
const shouldEmitUserConsoleLog = (log: UserConsoleLog): boolean => {
|
|
2721
|
-
return (
|
|
2722
|
-
context.normalizedConfig.onConsoleLog?.(log.content, log.type) !== false
|
|
2723
|
-
);
|
|
2724
|
-
};
|
|
2725
|
-
|
|
2726
|
-
const emitUserConsoleLog = async (log: UserConsoleLog): Promise<void> => {
|
|
2727
|
-
if (!shouldEmitUserConsoleLog(log)) {
|
|
2728
|
-
return;
|
|
2729
|
-
}
|
|
2730
|
-
|
|
2731
|
-
await Promise.all(
|
|
2732
|
-
context.reporters.map((reporter) =>
|
|
2733
|
-
(reporter as Reporter).onUserConsoleLog?.(log),
|
|
2734
|
-
),
|
|
2735
|
-
);
|
|
2736
|
-
};
|
|
2737
|
-
|
|
2738
|
-
const bufferConsoleLog = (log: UserConsoleLog): void => {
|
|
2739
|
-
const taskId = getBufferedLogTaskId(log);
|
|
2740
|
-
const logs = bufferedConsoleLogs.get(taskId) || [];
|
|
2741
|
-
logs.push(log);
|
|
2742
|
-
bufferedConsoleLogs.set(taskId, logs);
|
|
2743
|
-
|
|
2744
|
-
if (log.taskType === 'suite' && log.taskId) {
|
|
2745
|
-
suiteIdsByChain.set(
|
|
2746
|
-
getSuiteChainKey([...(log.taskParentNames || []), log.taskName || '']),
|
|
2747
|
-
log.taskId,
|
|
2748
|
-
);
|
|
2749
|
-
}
|
|
2750
|
-
};
|
|
2751
|
-
|
|
2752
|
-
const flushBufferedLogsForTask = async ({
|
|
2753
|
-
taskId,
|
|
2754
|
-
status,
|
|
2755
|
-
taskParentNames,
|
|
2756
|
-
taskType,
|
|
2757
|
-
testPath,
|
|
2758
|
-
}: {
|
|
2759
|
-
taskId: string;
|
|
2760
|
-
status: TestResult['status'];
|
|
2761
|
-
taskParentNames?: string[];
|
|
2762
|
-
taskType?: 'file' | 'suite' | 'case';
|
|
2763
|
-
testPath: string;
|
|
2764
|
-
}): Promise<void> => {
|
|
2765
|
-
if (status !== 'fail') {
|
|
2766
|
-
bufferedConsoleLogs.delete(taskId);
|
|
2767
|
-
return;
|
|
2768
|
-
}
|
|
2769
|
-
|
|
2770
|
-
const taskIdsToFlush: string[] = [];
|
|
2771
|
-
|
|
2772
|
-
if (taskType === 'case') {
|
|
2773
|
-
pushTaskId(taskIdsToFlush, getFileTaskId(testPath));
|
|
2774
|
-
|
|
2775
|
-
const suiteNames = taskParentNames || [];
|
|
2776
|
-
for (let i = 0; i < suiteNames.length; i++) {
|
|
2777
|
-
const suiteId = suiteIdsByChain.get(
|
|
2778
|
-
getSuiteChainKey(suiteNames.slice(0, i + 1)),
|
|
2779
|
-
);
|
|
2780
|
-
|
|
2781
|
-
if (suiteId) {
|
|
2782
|
-
pushTaskId(taskIdsToFlush, suiteId);
|
|
2783
|
-
}
|
|
2784
|
-
}
|
|
2785
|
-
|
|
2786
|
-
pushTaskId(taskIdsToFlush, taskId);
|
|
2787
|
-
}
|
|
2788
|
-
|
|
2789
|
-
if (taskType === 'suite') {
|
|
2790
|
-
pushTaskId(taskIdsToFlush, getFileTaskId(testPath));
|
|
2791
|
-
pushTaskId(taskIdsToFlush, taskId);
|
|
2792
|
-
}
|
|
2793
|
-
|
|
2794
|
-
if (taskType === 'file') {
|
|
2795
|
-
pushTaskId(taskIdsToFlush, taskId);
|
|
2796
|
-
}
|
|
2797
|
-
|
|
2798
|
-
for (const bufferedTaskId of taskIdsToFlush) {
|
|
2799
|
-
const logs = bufferedConsoleLogs.get(bufferedTaskId);
|
|
2800
|
-
if (!logs) {
|
|
2801
|
-
continue;
|
|
2802
|
-
}
|
|
2803
|
-
|
|
2804
|
-
bufferedConsoleLogs.delete(bufferedTaskId);
|
|
2805
|
-
|
|
2806
|
-
for (const log of logs) {
|
|
2807
|
-
await Promise.all(
|
|
2808
|
-
context.reporters.map((reporter) =>
|
|
2809
|
-
(reporter as Reporter).onUserConsoleLog?.(log),
|
|
2810
|
-
),
|
|
2811
|
-
);
|
|
2812
|
-
}
|
|
2873
|
+
// Non-watch runs surface the fatal error through the returned outcome and
|
|
2874
|
+
// let core's `finalizeRunCycle` set the exit code; watch reruns set it here.
|
|
2875
|
+
if (isWatchMode) {
|
|
2876
|
+
ensureProcessExitCode(1);
|
|
2813
2877
|
}
|
|
2814
2878
|
};
|
|
2815
2879
|
|
|
@@ -3117,6 +3181,18 @@ export const runBrowserController = async (
|
|
|
3117
3181
|
}
|
|
3118
3182
|
};
|
|
3119
3183
|
|
|
3184
|
+
// Bailed files never run, so they carry no case results — mirror the node
|
|
3185
|
+
// pool's skip result (`runInPool.ts`) so the summary reports them as skipped
|
|
3186
|
+
// rather than dropping them silently.
|
|
3187
|
+
const makeSkippedFileResult = (file: TestFileInfo): TestFileResult => ({
|
|
3188
|
+
testId: getFileTaskId(file.testPath),
|
|
3189
|
+
status: 'skip',
|
|
3190
|
+
name: '',
|
|
3191
|
+
testPath: file.testPath,
|
|
3192
|
+
project: file.projectName,
|
|
3193
|
+
results: [],
|
|
3194
|
+
});
|
|
3195
|
+
|
|
3120
3196
|
const runFilesWithPool = async (files: TestFileInfo[]): Promise<void> => {
|
|
3121
3197
|
if (files.length === 0) {
|
|
3122
3198
|
return;
|
|
@@ -3134,6 +3210,7 @@ export const runBrowserController = async (
|
|
|
3134
3210
|
|
|
3135
3211
|
const queue = [...files];
|
|
3136
3212
|
const concurrency = getHeadlessConcurrency(context, queue.length);
|
|
3213
|
+
const bail = context.normalizedConfig.bail;
|
|
3137
3214
|
|
|
3138
3215
|
const worker = async (): Promise<void> => {
|
|
3139
3216
|
while (
|
|
@@ -3141,6 +3218,19 @@ export const runBrowserController = async (
|
|
|
3141
3218
|
!run.cancelled &&
|
|
3142
3219
|
runLifecycle.isTokenActive(run.token)
|
|
3143
3220
|
) {
|
|
3221
|
+
// Cross-file bail gate (parity with the node pool's pickup-time skip
|
|
3222
|
+
// at `runInPool.ts`): once the cycle-wide failed count reaches `bail`,
|
|
3223
|
+
// drain the remaining files as skipped instead of running them. The
|
|
3224
|
+
// count is cycle-scoped because `stateManager` is reset at the top of
|
|
3225
|
+
// every run/rerun (initial run and `prepareWatchRerunState`).
|
|
3226
|
+
if (bail && context.stateManager.getCountOfFailedTests() >= bail) {
|
|
3227
|
+
let skipped = queue.shift();
|
|
3228
|
+
while (skipped) {
|
|
3229
|
+
await handleTestFileComplete(makeSkippedFileResult(skipped));
|
|
3230
|
+
skipped = queue.shift();
|
|
3231
|
+
}
|
|
3232
|
+
return;
|
|
3233
|
+
}
|
|
3144
3234
|
const next = queue.shift();
|
|
3145
3235
|
if (!next) {
|
|
3146
3236
|
return;
|
|
@@ -3173,6 +3263,12 @@ export const runBrowserController = async (
|
|
|
3173
3263
|
await cancelRun(run, false);
|
|
3174
3264
|
},
|
|
3175
3265
|
runFiles: async (files) => {
|
|
3266
|
+
// Clear the previous cycle's stateManager/snapshotManager before the
|
|
3267
|
+
// rerun streams new events through the shared sink — otherwise failed
|
|
3268
|
+
// counts (bail) and snapshot summaries accumulate across reruns. The
|
|
3269
|
+
// initial run does not reach here (it calls `runFilesWithPool`
|
|
3270
|
+
// directly), so only reruns reset.
|
|
3271
|
+
prepareWatchRerunState(context);
|
|
3176
3272
|
await notifyTestRunStart();
|
|
3177
3273
|
|
|
3178
3274
|
const rerunStartTime = Date.now();
|
|
@@ -3233,7 +3329,7 @@ export const runBrowserController = async (
|
|
|
3233
3329
|
hasFailure: false,
|
|
3234
3330
|
getSourcemap: getBrowserSourcemap,
|
|
3235
3331
|
resolveSourcemap: resolveBrowserSourcemap,
|
|
3236
|
-
close:
|
|
3332
|
+
close: !isWatchMode
|
|
3237
3333
|
? async () => {
|
|
3238
3334
|
sessionRegistry.clear();
|
|
3239
3335
|
await destroyBrowserRuntime(runtime);
|
|
@@ -3241,7 +3337,7 @@ export const runBrowserController = async (
|
|
|
3241
3337
|
: undefined,
|
|
3242
3338
|
};
|
|
3243
3339
|
|
|
3244
|
-
if (
|
|
3340
|
+
if (isWatchMode) {
|
|
3245
3341
|
await notifyTestRunEnd({ duration });
|
|
3246
3342
|
}
|
|
3247
3343
|
|
|
@@ -3365,7 +3461,9 @@ export const runBrowserController = async (
|
|
|
3365
3461
|
const isFailure = reporterResults.some(
|
|
3366
3462
|
(result: TestFileResult) => result.status === 'fail',
|
|
3367
3463
|
);
|
|
3368
|
-
|
|
3464
|
+
// Non-watch runs let core's `finalizeRunCycle` own the exit code from the
|
|
3465
|
+
// returned outcome; watch reruns set it here.
|
|
3466
|
+
if (isWatchMode && isFailure) {
|
|
3369
3467
|
ensureProcessExitCode(1);
|
|
3370
3468
|
}
|
|
3371
3469
|
|
|
@@ -3376,10 +3474,12 @@ export const runBrowserController = async (
|
|
|
3376
3474
|
hasFailure: isFailure,
|
|
3377
3475
|
getSourcemap: getBrowserSourcemap,
|
|
3378
3476
|
resolveSourcemap: resolveBrowserSourcemap,
|
|
3379
|
-
|
|
3477
|
+
// `closeHeadlessRuntime` is already `undefined` in watch mode, so the
|
|
3478
|
+
// non-watch caller (core) receives the deferred close and watch does not.
|
|
3479
|
+
close: closeHeadlessRuntime,
|
|
3380
3480
|
};
|
|
3381
3481
|
|
|
3382
|
-
if (
|
|
3482
|
+
if (isWatchMode) {
|
|
3383
3483
|
try {
|
|
3384
3484
|
await notifyTestRunEnd({ duration });
|
|
3385
3485
|
} finally {
|
|
@@ -3754,7 +3854,11 @@ export const runBrowserController = async (
|
|
|
3754
3854
|
}
|
|
3755
3855
|
} catch (error) {
|
|
3756
3856
|
fatalError = fatalError ?? toError(error);
|
|
3757
|
-
|
|
3857
|
+
// Non-watch: the fatal error rides the returned outcome and core owns the
|
|
3858
|
+
// exit code; watch reruns set it here.
|
|
3859
|
+
if (isWatchMode) {
|
|
3860
|
+
ensureProcessExitCode(1);
|
|
3861
|
+
}
|
|
3758
3862
|
}
|
|
3759
3863
|
|
|
3760
3864
|
testTime = Date.now() - testStart;
|
|
@@ -3800,6 +3904,10 @@ export const runBrowserController = async (
|
|
|
3800
3904
|
`Re-running ${rerunPlan.normalizedAffectedTestFiles.length} affected test file(s)...\n`,
|
|
3801
3905
|
),
|
|
3802
3906
|
);
|
|
3907
|
+
// Match the headless path: reset per-cycle state before the rerun
|
|
3908
|
+
// streams new events, so bail counts and snapshot summaries do not
|
|
3909
|
+
// accumulate across headed reruns.
|
|
3910
|
+
prepareWatchRerunState(context);
|
|
3803
3911
|
await notifyTestRunStart();
|
|
3804
3912
|
|
|
3805
3913
|
const rerunStartTime = Date.now();
|
|
@@ -3874,7 +3982,9 @@ export const runBrowserController = async (
|
|
|
3874
3982
|
const isFailure = reporterResults.some(
|
|
3875
3983
|
(result: TestFileResult) => result.status === 'fail',
|
|
3876
3984
|
);
|
|
3877
|
-
|
|
3985
|
+
// Non-watch runs let core's `finalizeRunCycle` own the exit code from the
|
|
3986
|
+
// returned outcome; watch reruns set it here.
|
|
3987
|
+
if (isWatchMode && isFailure) {
|
|
3878
3988
|
ensureProcessExitCode(1);
|
|
3879
3989
|
}
|
|
3880
3990
|
|
|
@@ -3885,10 +3995,12 @@ export const runBrowserController = async (
|
|
|
3885
3995
|
hasFailure: isFailure,
|
|
3886
3996
|
getSourcemap: getBrowserSourcemap,
|
|
3887
3997
|
resolveSourcemap: resolveBrowserSourcemap,
|
|
3888
|
-
|
|
3998
|
+
// `closeContainerRuntime` is already `undefined` in watch mode, so the
|
|
3999
|
+
// non-watch caller (core) receives the deferred close and watch does not.
|
|
4000
|
+
close: closeContainerRuntime,
|
|
3889
4001
|
};
|
|
3890
4002
|
|
|
3891
|
-
if (
|
|
4003
|
+
if (isWatchMode) {
|
|
3892
4004
|
try {
|
|
3893
4005
|
await notifyTestRunEnd({ duration });
|
|
3894
4006
|
} finally {
|
|
@@ -3926,20 +4038,20 @@ export type ListBrowserTestsResult = {
|
|
|
3926
4038
|
*/
|
|
3927
4039
|
export const listBrowserTests = async (
|
|
3928
4040
|
context: RstestContext,
|
|
3929
|
-
options?:
|
|
3930
|
-
shardedEntries?: Map<string, { entries: Record<string, string> }>;
|
|
3931
|
-
},
|
|
4041
|
+
options?: ListBrowserTestsOptions,
|
|
3932
4042
|
): Promise<ListBrowserTestsResult> => {
|
|
4043
|
+
const browserProjects = options?.projects ?? getBrowserProjects(context);
|
|
3933
4044
|
const projectEntries = await resolveProjectEntries(
|
|
3934
4045
|
context,
|
|
3935
4046
|
options?.shardedEntries,
|
|
4047
|
+
browserProjects,
|
|
3936
4048
|
);
|
|
3937
4049
|
const totalTests = projectEntries.reduce(
|
|
3938
4050
|
(total, item) => total + item.testFiles.length,
|
|
3939
4051
|
0,
|
|
3940
4052
|
);
|
|
3941
4053
|
|
|
3942
|
-
if (totalTests === 0) {
|
|
4054
|
+
if (totalTests === 0 && !hasUserRstestConfigPlugins(browserProjects)) {
|
|
3943
4055
|
return {
|
|
3944
4056
|
list: [],
|
|
3945
4057
|
close: async () => {},
|
|
@@ -3953,19 +4065,23 @@ export const listBrowserTests = async (
|
|
|
3953
4065
|
`list-${Date.now()}`,
|
|
3954
4066
|
);
|
|
3955
4067
|
|
|
3956
|
-
const browserProjects = getBrowserProjects(context);
|
|
3957
|
-
|
|
3958
4068
|
// Create a simplified browser runtime for collect mode
|
|
3959
4069
|
let runtime: BrowserRuntime;
|
|
3960
4070
|
try {
|
|
3961
4071
|
runtime = await createBrowserRuntime({
|
|
3962
4072
|
context,
|
|
3963
4073
|
projectEntries,
|
|
4074
|
+
browserProjects,
|
|
4075
|
+
shardedEntries: options?.shardedEntries,
|
|
4076
|
+
freezeShardedEntries: options?.freezeShardedEntries,
|
|
3964
4077
|
tempDir,
|
|
3965
4078
|
isWatchMode: false,
|
|
3966
4079
|
containerDistPath: undefined,
|
|
3967
4080
|
containerDevServer: undefined,
|
|
3968
4081
|
forceHeadless: true, // Always use headless for list command
|
|
4082
|
+
skipProviderLaunch: options?.filesOnly,
|
|
4083
|
+
appliedModifyRstestConfigEnvironments:
|
|
4084
|
+
options?.appliedModifyRstestConfigEnvironments,
|
|
3969
4085
|
});
|
|
3970
4086
|
} catch (error) {
|
|
3971
4087
|
const providers = [
|
|
@@ -3982,6 +4098,29 @@ export const listBrowserTests = async (
|
|
|
3982
4098
|
throw error;
|
|
3983
4099
|
}
|
|
3984
4100
|
|
|
4101
|
+
if (options?.filesOnly) {
|
|
4102
|
+
const list = runtime.projectEntries.flatMap((entry) =>
|
|
4103
|
+
entry.testFiles.map((testPath) => ({
|
|
4104
|
+
testPath,
|
|
4105
|
+
project: entry.project.name,
|
|
4106
|
+
tests: [],
|
|
4107
|
+
})),
|
|
4108
|
+
);
|
|
4109
|
+
await destroyBrowserRuntime(runtime);
|
|
4110
|
+
return {
|
|
4111
|
+
list,
|
|
4112
|
+
close: async () => {},
|
|
4113
|
+
};
|
|
4114
|
+
}
|
|
4115
|
+
|
|
4116
|
+
if (!runtime.projectEntries.some((entry) => entry.testFiles.length > 0)) {
|
|
4117
|
+
await destroyBrowserRuntime(runtime);
|
|
4118
|
+
return {
|
|
4119
|
+
list: [],
|
|
4120
|
+
close: async () => {},
|
|
4121
|
+
};
|
|
4122
|
+
}
|
|
4123
|
+
|
|
3985
4124
|
const { browser, browserLaunchOptions } = runtime;
|
|
3986
4125
|
|
|
3987
4126
|
// Get browser projects for runtime config
|
|
@@ -3991,17 +4130,14 @@ export const listBrowserTests = async (
|
|
|
3991
4130
|
name: project.name,
|
|
3992
4131
|
environmentName: project.environmentName,
|
|
3993
4132
|
projectRoot: normalize(project.rootPath),
|
|
3994
|
-
runtimeConfig: serializableConfig(
|
|
4133
|
+
runtimeConfig: serializableConfig(
|
|
4134
|
+
projectRuntimeConfig(project, { envMode: 'static' }),
|
|
4135
|
+
),
|
|
3995
4136
|
viewport: project.normalizedConfig.browser.viewport,
|
|
3996
4137
|
}),
|
|
3997
4138
|
);
|
|
3998
4139
|
|
|
3999
|
-
|
|
4000
|
-
const maxTestTimeoutForRpc = Math.max(
|
|
4001
|
-
...browserProjects.map(
|
|
4002
|
-
(p) => p.normalizedConfig.testTimeout ?? DEFAULT_TEST_TIMEOUT,
|
|
4003
|
-
),
|
|
4004
|
-
);
|
|
4140
|
+
const maxTestTimeoutForRpc = getMaxTestTimeoutForRpc(browserProjects);
|
|
4005
4141
|
|
|
4006
4142
|
const hostOptions: BrowserHostConfig = {
|
|
4007
4143
|
rootPath: normalize(context.rootPath),
|
|
@@ -4026,6 +4162,10 @@ export const listBrowserTests = async (
|
|
|
4026
4162
|
|
|
4027
4163
|
const serializedOptions = serializeForInlineScript(hostOptions);
|
|
4028
4164
|
|
|
4165
|
+
// Per-page collect watchdog: a test file whose module evaluation stalls must
|
|
4166
|
+
// not hang `rstest list` forever.
|
|
4167
|
+
const collectTimeoutMs = 30_000;
|
|
4168
|
+
|
|
4029
4169
|
const collectFromServer = async (
|
|
4030
4170
|
server: BrowserProjectServer,
|
|
4031
4171
|
): Promise<{ results: ListCommandResult[]; error: Error | null }> => {
|
|
@@ -4092,20 +4232,19 @@ export const listBrowserTests = async (
|
|
|
4092
4232
|
waitUntil: 'load',
|
|
4093
4233
|
});
|
|
4094
4234
|
|
|
4095
|
-
// Wait for collection to complete with timeout
|
|
4096
|
-
const timeoutMs = 30000;
|
|
4235
|
+
// Wait for collection to complete with the shared collect timeout.
|
|
4097
4236
|
let timeoutId: ReturnType<typeof setTimeout> | undefined;
|
|
4098
4237
|
const timeoutPromise = new Promise<void>((resolve) => {
|
|
4099
4238
|
timeoutId = setTimeout(() => {
|
|
4100
4239
|
if (!collectCompleted) {
|
|
4101
4240
|
logger.warn(
|
|
4102
4241
|
color.yellow(
|
|
4103
|
-
`[List] Browser test collection timed out after ${
|
|
4242
|
+
`[List] Browser test collection timed out after ${collectTimeoutMs}ms`,
|
|
4104
4243
|
),
|
|
4105
4244
|
);
|
|
4106
4245
|
}
|
|
4107
4246
|
resolve();
|
|
4108
|
-
},
|
|
4247
|
+
}, collectTimeoutMs);
|
|
4109
4248
|
});
|
|
4110
4249
|
|
|
4111
4250
|
await Promise.race([collectPromise, timeoutPromise]);
|