@rstest/browser 0.9.10 → 0.10.1
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/augmentExpect.d.ts +1 -1
- package/dist/browser-container/container-static/js/{102.e63e4aa16d.js → 188.480c3c49b5.js} +4543 -4250
- package/dist/browser-container/container-static/js/188.480c3c49b5.js.LICENSE.txt +1 -0
- package/dist/browser-container/container-static/js/{index.af52585634.js → index.29fc60be33.js} +10 -10
- package/dist/browser-container/container-static/js/{lib-react.9e703e1b29.js → lib-react.5ae9b90ee5.js} +24 -24
- package/dist/browser-container/container-static/js/lib-react.5ae9b90ee5.js.LICENSE.txt +1 -0
- package/dist/browser-container/index.html +1 -1
- package/dist/browser.d.ts +2 -2
- package/dist/client/api.d.ts +2 -2
- package/dist/client/browserRpc.d.ts +2 -2
- package/dist/client/dispatchTransport.d.ts +1 -1
- package/dist/client/locator.d.ts +2 -2
- package/dist/concurrency.d.ts +1 -1
- package/dist/dispatchCapabilities.d.ts +2 -2
- package/dist/dispatchRouter.d.ts +1 -1
- package/dist/headlessTransport.d.ts +2 -2
- package/dist/hostController.d.ts +2 -2
- package/dist/index.d.ts +3 -3
- package/dist/index.js +205 -23
- package/dist/protocol.d.ts +6 -2
- package/dist/providers/index.d.ts +1 -1
- package/dist/providers/playwright/compileLocator.d.ts +1 -1
- package/dist/providers/playwright/dispatchBrowserRpc.d.ts +1 -1
- package/dist/providers/playwright/expectUtils.d.ts +1 -1
- package/dist/providers/playwright/implementation.d.ts +1 -1
- package/dist/providers/playwright/index.d.ts +1 -1
- package/dist/providers/playwright/runtime.d.ts +1 -1
- package/dist/providers/playwright/textMatcher.d.ts +2 -2
- package/dist/sessionRegistry.d.ts +1 -1
- package/dist/viewportPresets.d.ts +1 -1
- package/dist/watchRerunPlanner.d.ts +1 -1
- package/package.json +9 -9
- package/src/client/entry.ts +75 -14
- package/src/hostController.ts +405 -70
- package/src/protocol.ts +4 -0
- package/dist/browser-container/container-static/js/102.e63e4aa16d.js.LICENSE.txt +0 -1
- package/dist/browser-container/container-static/js/lib-react.9e703e1b29.js.LICENSE.txt +0 -1
package/src/hostController.ts
CHANGED
|
@@ -12,16 +12,19 @@ import {
|
|
|
12
12
|
color,
|
|
13
13
|
createCoverageProvider,
|
|
14
14
|
type FormattedError,
|
|
15
|
+
getNoTestFilesMessage,
|
|
15
16
|
getSetupFiles,
|
|
16
17
|
getTestEntries,
|
|
17
18
|
isDebug,
|
|
18
19
|
type ListCommandResult,
|
|
19
20
|
loadCoverageProvider,
|
|
20
21
|
logger,
|
|
22
|
+
PhaseTracker,
|
|
21
23
|
type ProjectContext,
|
|
22
24
|
type Reporter,
|
|
23
25
|
type Rstest,
|
|
24
26
|
type RuntimeConfig,
|
|
27
|
+
resolveProjectBuildCache,
|
|
25
28
|
rsbuild,
|
|
26
29
|
serializableConfig,
|
|
27
30
|
type Test,
|
|
@@ -94,6 +97,18 @@ type RsbuildInstance = rsbuild.RsbuildInstance;
|
|
|
94
97
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
95
98
|
const OPTIONS_PLACEHOLDER = '__RSTEST_OPTIONS_PLACEHOLDER__';
|
|
96
99
|
|
|
100
|
+
/**
|
|
101
|
+
* Monotonic counter for synthetic per-file Perfetto `pid` values in `--trace`
|
|
102
|
+
* mode. Browser host runs every test file inside the same Node process, so
|
|
103
|
+
* without an override every file would emit events under the same `pid` and
|
|
104
|
+
* share a single track labelled `worker <hostPid>`. Giving each file its own
|
|
105
|
+
* synthetic `pid` makes the track title surface the file path instead,
|
|
106
|
+
* matching node mode's default `isolate: true` behavior. The 1_000_000_000
|
|
107
|
+
* base keeps each synthetic `pid` well clear of real OS `pid` values in
|
|
108
|
+
* mixed-mode traces.
|
|
109
|
+
*/
|
|
110
|
+
let nextBrowserFilePid = 1_000_000_000;
|
|
111
|
+
|
|
97
112
|
/**
|
|
98
113
|
* Serialize JSON for inline <script> injection.
|
|
99
114
|
* Escapes '<' to prevent accidental </script> break-out.
|
|
@@ -154,6 +169,10 @@ type TestFileStartPayload = {
|
|
|
154
169
|
type LogPayload = {
|
|
155
170
|
level: 'log' | 'warn' | 'error' | 'info' | 'debug';
|
|
156
171
|
content: string;
|
|
172
|
+
taskId?: string;
|
|
173
|
+
taskName?: string;
|
|
174
|
+
taskParentNames?: string[];
|
|
175
|
+
taskType?: 'file' | 'suite' | 'case';
|
|
157
176
|
testPath: string;
|
|
158
177
|
type: 'stdout' | 'stderr';
|
|
159
178
|
trace?: string;
|
|
@@ -186,6 +205,14 @@ type DeferredPromise<T> = {
|
|
|
186
205
|
reject: (reason?: unknown) => void;
|
|
187
206
|
};
|
|
188
207
|
|
|
208
|
+
const getFileTaskId = (testPath: string): string => {
|
|
209
|
+
return `file:${testPath}`;
|
|
210
|
+
};
|
|
211
|
+
|
|
212
|
+
const getBufferedLogTaskId = (log: UserConsoleLog): string => {
|
|
213
|
+
return log.taskId ?? getFileTaskId(log.testPath);
|
|
214
|
+
};
|
|
215
|
+
|
|
189
216
|
const createDeferredPromise = <T>(): DeferredPromise<T> => {
|
|
190
217
|
let resolve!: DeferredPromise<T>['resolve'];
|
|
191
218
|
let reject!: DeferredPromise<T>['reject'];
|
|
@@ -795,12 +822,20 @@ const getRuntimeConfigFromProject = (
|
|
|
795
822
|
env,
|
|
796
823
|
bail,
|
|
797
824
|
logHeapUsage,
|
|
825
|
+
detectAsyncLeaks,
|
|
798
826
|
chaiConfig,
|
|
799
827
|
includeTaskLocation,
|
|
828
|
+
silent,
|
|
800
829
|
} = project.normalizedConfig;
|
|
801
830
|
|
|
802
831
|
return {
|
|
803
|
-
env
|
|
832
|
+
// Propagate NODE_ENV from the host so `import.meta.env.NODE_ENV` resolves
|
|
833
|
+
// to `'test'` in browser tests (matches Node mode). User-supplied `env`
|
|
834
|
+
// wins so explicit overrides still take effect.
|
|
835
|
+
env: {
|
|
836
|
+
NODE_ENV: process.env.NODE_ENV,
|
|
837
|
+
...env,
|
|
838
|
+
},
|
|
804
839
|
testNamePattern,
|
|
805
840
|
testTimeout,
|
|
806
841
|
hookTimeout,
|
|
@@ -821,8 +856,10 @@ const getRuntimeConfigFromProject = (
|
|
|
821
856
|
snapshotFormat,
|
|
822
857
|
bail,
|
|
823
858
|
logHeapUsage,
|
|
859
|
+
detectAsyncLeaks,
|
|
824
860
|
chaiConfig,
|
|
825
861
|
includeTaskLocation,
|
|
862
|
+
silent,
|
|
826
863
|
};
|
|
827
864
|
};
|
|
828
865
|
|
|
@@ -916,6 +953,7 @@ const collectProjectEntries = async (
|
|
|
916
953
|
rootPath: context.rootPath,
|
|
917
954
|
projectRoot: project.rootPath,
|
|
918
955
|
fileFilters: context.fileFilters || [],
|
|
956
|
+
fileFilterMode: context.fileFilterMode,
|
|
919
957
|
});
|
|
920
958
|
|
|
921
959
|
const setup = getSetupFiles(setupFiles, project.rootPath);
|
|
@@ -1280,6 +1318,10 @@ const createBrowserRuntime = async ({
|
|
|
1280
1318
|
}
|
|
1281
1319
|
|
|
1282
1320
|
const userRsbuildConfig = project.normalizedConfig;
|
|
1321
|
+
const buildCache = resolveProjectBuildCache({
|
|
1322
|
+
context,
|
|
1323
|
+
project,
|
|
1324
|
+
});
|
|
1283
1325
|
const setupFiles = Object.values(
|
|
1284
1326
|
getSetupFiles(
|
|
1285
1327
|
project.normalizedConfig.setupFiles,
|
|
@@ -1287,53 +1329,65 @@ const createBrowserRuntime = async ({
|
|
|
1287
1329
|
),
|
|
1288
1330
|
);
|
|
1289
1331
|
// Merge order: current config -> userConfig -> rstest required config (highest priority)
|
|
1290
|
-
const merged = mergeEnvironmentConfig(
|
|
1291
|
-
|
|
1292
|
-
|
|
1332
|
+
const merged = mergeEnvironmentConfig(
|
|
1333
|
+
config,
|
|
1334
|
+
{
|
|
1335
|
+
...userRsbuildConfig,
|
|
1336
|
+
performance: buildCache
|
|
1337
|
+
? {
|
|
1338
|
+
...userRsbuildConfig.performance,
|
|
1339
|
+
buildCache,
|
|
1340
|
+
}
|
|
1341
|
+
: userRsbuildConfig.performance,
|
|
1293
1342
|
},
|
|
1294
|
-
|
|
1295
|
-
|
|
1296
|
-
|
|
1297
|
-
'import.meta.env': 'globalThis[Symbol.for("rstest.env")]',
|
|
1343
|
+
{
|
|
1344
|
+
resolve: {
|
|
1345
|
+
alias: rstestInternalAliases,
|
|
1298
1346
|
},
|
|
1299
|
-
|
|
1300
|
-
|
|
1301
|
-
|
|
1302
|
-
|
|
1303
|
-
|
|
1304
|
-
js: 'source-map',
|
|
1347
|
+
source: {
|
|
1348
|
+
define: {
|
|
1349
|
+
'process.env': 'globalThis[Symbol.for("rstest.env")]',
|
|
1350
|
+
'import.meta.env': 'globalThis[Symbol.for("rstest.env")]',
|
|
1351
|
+
},
|
|
1305
1352
|
},
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
|
|
1309
|
-
|
|
1310
|
-
|
|
1311
|
-
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
|
|
1331
|
-
|
|
1332
|
-
|
|
1333
|
-
|
|
1353
|
+
output: {
|
|
1354
|
+
target: 'web',
|
|
1355
|
+
// Enable source map for inline snapshot support
|
|
1356
|
+
sourceMap: {
|
|
1357
|
+
js: 'source-map',
|
|
1358
|
+
},
|
|
1359
|
+
},
|
|
1360
|
+
tools: {
|
|
1361
|
+
rspack: (rspackConfig) => {
|
|
1362
|
+
rspackConfig.mode = 'development';
|
|
1363
|
+
rspackConfig.lazyCompilation =
|
|
1364
|
+
createBrowserLazyCompilationConfig(setupFiles);
|
|
1365
|
+
rspackConfig.plugins = rspackConfig.plugins || [];
|
|
1366
|
+
rspackConfig.plugins.push(virtualManifestPlugin);
|
|
1367
|
+
|
|
1368
|
+
applyDefaultWatchOptions(rspackConfig, isWatchMode);
|
|
1369
|
+
|
|
1370
|
+
// Extract and merge sourcemaps from pre-built @rstest/core files
|
|
1371
|
+
// This preserves the sourcemap chain for inline snapshot support
|
|
1372
|
+
// See: https://rspack.dev/config/module-rules#rulesextractsourcemap
|
|
1373
|
+
const browserRuntimeDir = dirname(browserRuntimePath);
|
|
1374
|
+
rspackConfig.module = rspackConfig.module || {};
|
|
1375
|
+
rspackConfig.module.rules = rspackConfig.module.rules || [];
|
|
1376
|
+
rspackConfig.module.rules.unshift({
|
|
1377
|
+
test: /\.js$/,
|
|
1378
|
+
include: browserRuntimeDir,
|
|
1379
|
+
extractSourceMap: true,
|
|
1380
|
+
});
|
|
1381
|
+
|
|
1382
|
+
if (isDebug()) {
|
|
1383
|
+
logger.log(
|
|
1384
|
+
`[rstest:browser] extractSourceMap rule added for: ${browserRuntimeDir}`,
|
|
1385
|
+
);
|
|
1386
|
+
}
|
|
1387
|
+
},
|
|
1334
1388
|
},
|
|
1335
1389
|
},
|
|
1336
|
-
|
|
1390
|
+
);
|
|
1337
1391
|
|
|
1338
1392
|
// Completely overwrite entry to prevent Rsbuild default entry detection from taking effect.
|
|
1339
1393
|
// In browser mode, entry is fully controlled by rstest (not user's src/index.ts).
|
|
@@ -1655,8 +1709,22 @@ export const runBrowserController = async (
|
|
|
1655
1709
|
context: Rstest,
|
|
1656
1710
|
options?: BrowserTestRunOptions,
|
|
1657
1711
|
): Promise<BrowserTestRunResult | void> => {
|
|
1658
|
-
const {
|
|
1712
|
+
const {
|
|
1713
|
+
skipOnTestRunEnd = false,
|
|
1714
|
+
allowEmptyWatchRun = false,
|
|
1715
|
+
onTraceEvents,
|
|
1716
|
+
} = options ?? {};
|
|
1659
1717
|
const buildStart = Date.now();
|
|
1718
|
+
const isWatchMode = context.command === 'watch';
|
|
1719
|
+
|
|
1720
|
+
// Per-file PhaseTrackers, populated only when `--trace` is on (caller
|
|
1721
|
+
// passes `onTraceEvents`). The browser host shares one Node process across
|
|
1722
|
+
// every test file, so each tracker is assigned a synthetic per-file pid
|
|
1723
|
+
// (`nextBrowserFilePid`) that lets Perfetto render each file as its own
|
|
1724
|
+
// process track with the file path as the title.
|
|
1725
|
+
const phaseTrackers = onTraceEvents
|
|
1726
|
+
? new Map<string, PhaseTracker>()
|
|
1727
|
+
: undefined;
|
|
1660
1728
|
const browserProjects = getBrowserProjects(context);
|
|
1661
1729
|
const useHeadlessDirect = browserProjects.every(
|
|
1662
1730
|
(project) => project.normalizedConfig.browser.headless,
|
|
@@ -1873,27 +1941,47 @@ export const runBrowserController = async (
|
|
|
1873
1941
|
(total, item) => total + item.testFiles.length,
|
|
1874
1942
|
0,
|
|
1875
1943
|
);
|
|
1944
|
+
const shouldKeepWatchingWithEmptySet = isWatchMode && allowEmptyWatchRun;
|
|
1876
1945
|
|
|
1877
1946
|
if (totalTests === 0) {
|
|
1878
1947
|
const code = context.normalizedConfig.passWithNoTests ? 0 : 1;
|
|
1879
1948
|
if (!skipOnTestRunEnd) {
|
|
1880
|
-
const message =
|
|
1949
|
+
const message = shouldKeepWatchingWithEmptySet
|
|
1950
|
+
? 'No test files found.'
|
|
1951
|
+
: getNoTestFilesMessage({
|
|
1952
|
+
context,
|
|
1953
|
+
code,
|
|
1954
|
+
defaultMessage: `No test files found, exiting with code ${code}.`,
|
|
1955
|
+
});
|
|
1881
1956
|
if (code === 0) {
|
|
1882
1957
|
logger.log(color.yellow(message));
|
|
1883
1958
|
} else {
|
|
1884
1959
|
logger.error(color.red(message));
|
|
1885
1960
|
}
|
|
1961
|
+
|
|
1962
|
+
if (context.relatedFilters?.length) {
|
|
1963
|
+
logger.log(
|
|
1964
|
+
color.gray('related: '),
|
|
1965
|
+
context.relatedFilters.join(color.gray(', ')),
|
|
1966
|
+
);
|
|
1967
|
+
} else if (context.fileFilters?.length) {
|
|
1968
|
+
logger.log(
|
|
1969
|
+
color.gray('filter: '),
|
|
1970
|
+
context.fileFilters.join(color.gray(', ')),
|
|
1971
|
+
);
|
|
1972
|
+
}
|
|
1886
1973
|
}
|
|
1887
1974
|
|
|
1888
|
-
if (code !== 0) {
|
|
1975
|
+
if (code !== 0 && !shouldKeepWatchingWithEmptySet) {
|
|
1889
1976
|
ensureProcessExitCode(code);
|
|
1890
1977
|
}
|
|
1891
|
-
|
|
1978
|
+
if (!shouldKeepWatchingWithEmptySet) {
|
|
1979
|
+
return;
|
|
1980
|
+
}
|
|
1892
1981
|
}
|
|
1893
1982
|
|
|
1894
1983
|
await notifyTestRunStart();
|
|
1895
1984
|
|
|
1896
|
-
const isWatchMode = context.command === 'watch';
|
|
1897
1985
|
const enableCliShortcuts = isWatchMode && isBrowserWatchCliShortcutsEnabled();
|
|
1898
1986
|
const browserTempOutputRoot = context.normalizedConfig.output.distPath.root;
|
|
1899
1987
|
const tempDir =
|
|
@@ -2124,9 +2212,21 @@ export const runBrowserController = async (
|
|
|
2124
2212
|
const handleTestFileStart = async (
|
|
2125
2213
|
payload: TestFileStartPayload,
|
|
2126
2214
|
): Promise<void> => {
|
|
2215
|
+
if (phaseTrackers) {
|
|
2216
|
+
const tracker = new PhaseTracker({
|
|
2217
|
+
trace: {
|
|
2218
|
+
testPath: payload.testPath,
|
|
2219
|
+
project: payload.projectName,
|
|
2220
|
+
},
|
|
2221
|
+
pid: nextBrowserFilePid++,
|
|
2222
|
+
});
|
|
2223
|
+
tracker.transition('prepare');
|
|
2224
|
+
phaseTrackers.set(payload.testPath, tracker);
|
|
2225
|
+
}
|
|
2127
2226
|
await Promise.all(
|
|
2128
2227
|
context.reporters.map((reporter) =>
|
|
2129
2228
|
(reporter as Reporter).onTestFileStart?.({
|
|
2229
|
+
testId: getFileTaskId(payload.testPath),
|
|
2130
2230
|
testPath: payload.testPath,
|
|
2131
2231
|
tests: [],
|
|
2132
2232
|
}),
|
|
@@ -2137,6 +2237,7 @@ export const runBrowserController = async (
|
|
|
2137
2237
|
const handleTestFileReady = async (
|
|
2138
2238
|
payload: TestFileReadyPayload,
|
|
2139
2239
|
): Promise<void> => {
|
|
2240
|
+
phaseTrackers?.get(payload.testPath)?.transition('tests');
|
|
2140
2241
|
await Promise.all(
|
|
2141
2242
|
context.reporters.map((reporter) =>
|
|
2142
2243
|
(reporter as Reporter).onTestFileReady?.(payload),
|
|
@@ -2147,6 +2248,7 @@ export const runBrowserController = async (
|
|
|
2147
2248
|
const handleTestSuiteStart = async (
|
|
2148
2249
|
payload: TestSuiteStartPayload,
|
|
2149
2250
|
): Promise<void> => {
|
|
2251
|
+
phaseTrackers?.get(payload.testPath)?.recordSuiteStart(payload);
|
|
2150
2252
|
await Promise.all(
|
|
2151
2253
|
context.reporters.map((reporter) =>
|
|
2152
2254
|
(reporter as Reporter).onTestSuiteStart?.(payload),
|
|
@@ -2157,16 +2259,28 @@ export const runBrowserController = async (
|
|
|
2157
2259
|
const handleTestSuiteResult = async (
|
|
2158
2260
|
payload: TestSuiteResultPayload,
|
|
2159
2261
|
): Promise<void> => {
|
|
2262
|
+
phaseTrackers?.get(payload.testPath)?.recordSuiteResult(payload);
|
|
2160
2263
|
await Promise.all(
|
|
2161
2264
|
context.reporters.map((reporter) =>
|
|
2162
2265
|
(reporter as Reporter).onTestSuiteResult?.(payload),
|
|
2163
2266
|
),
|
|
2164
2267
|
);
|
|
2268
|
+
|
|
2269
|
+
if (context.normalizedConfig.silent === 'passed-only') {
|
|
2270
|
+
await flushBufferedLogsForTask({
|
|
2271
|
+
taskId: payload.testId,
|
|
2272
|
+
status: payload.status,
|
|
2273
|
+
taskParentNames: payload.parentNames,
|
|
2274
|
+
taskType: 'suite',
|
|
2275
|
+
testPath: payload.testPath,
|
|
2276
|
+
});
|
|
2277
|
+
}
|
|
2165
2278
|
};
|
|
2166
2279
|
|
|
2167
2280
|
const handleTestCaseStart = async (
|
|
2168
2281
|
payload: TestCaseStartPayload,
|
|
2169
2282
|
): Promise<void> => {
|
|
2283
|
+
phaseTrackers?.get(payload.testPath)?.recordCaseStart(payload);
|
|
2170
2284
|
await Promise.all(
|
|
2171
2285
|
context.reporters.map((reporter) =>
|
|
2172
2286
|
(reporter as Reporter).onTestCaseStart?.(payload),
|
|
@@ -2176,11 +2290,22 @@ export const runBrowserController = async (
|
|
|
2176
2290
|
|
|
2177
2291
|
const handleTestCaseResult = async (payload: TestResult): Promise<void> => {
|
|
2178
2292
|
caseResults.push(payload);
|
|
2293
|
+
phaseTrackers?.get(payload.testPath)?.recordCaseResult(payload);
|
|
2179
2294
|
await Promise.all(
|
|
2180
2295
|
context.reporters.map((reporter) =>
|
|
2181
2296
|
(reporter as Reporter).onTestCaseResult?.(payload),
|
|
2182
2297
|
),
|
|
2183
2298
|
);
|
|
2299
|
+
|
|
2300
|
+
if (context.normalizedConfig.silent === 'passed-only') {
|
|
2301
|
+
await flushBufferedLogsForTask({
|
|
2302
|
+
taskId: payload.testId,
|
|
2303
|
+
status: payload.status,
|
|
2304
|
+
taskParentNames: payload.parentNames,
|
|
2305
|
+
taskType: 'case',
|
|
2306
|
+
testPath: payload.testPath,
|
|
2307
|
+
});
|
|
2308
|
+
}
|
|
2184
2309
|
};
|
|
2185
2310
|
|
|
2186
2311
|
const handleTestFileComplete = async (
|
|
@@ -2191,6 +2316,27 @@ export const runBrowserController = async (
|
|
|
2191
2316
|
if (payload.snapshotResult) {
|
|
2192
2317
|
context.snapshotManager.add(payload.snapshotResult);
|
|
2193
2318
|
}
|
|
2319
|
+
|
|
2320
|
+
if (phaseTrackers) {
|
|
2321
|
+
const tracker = phaseTrackers.get(payload.testPath);
|
|
2322
|
+
if (tracker) {
|
|
2323
|
+
tracker.end();
|
|
2324
|
+
const events = tracker.getTraceEvents();
|
|
2325
|
+
if (events) onTraceEvents?.(events);
|
|
2326
|
+
phaseTrackers.delete(payload.testPath);
|
|
2327
|
+
}
|
|
2328
|
+
}
|
|
2329
|
+
|
|
2330
|
+
if (context.normalizedConfig.silent === 'passed-only') {
|
|
2331
|
+
await flushBufferedLogsForTask({
|
|
2332
|
+
taskId: payload.testId,
|
|
2333
|
+
status: payload.status,
|
|
2334
|
+
taskParentNames: payload.parentNames,
|
|
2335
|
+
taskType: 'file',
|
|
2336
|
+
testPath: payload.testPath,
|
|
2337
|
+
});
|
|
2338
|
+
}
|
|
2339
|
+
|
|
2194
2340
|
await Promise.all(
|
|
2195
2341
|
context.reporters.map((reporter) =>
|
|
2196
2342
|
(reporter as Reporter).onTestFileResult?.(payload),
|
|
@@ -2205,19 +2351,28 @@ export const runBrowserController = async (
|
|
|
2205
2351
|
const log: UserConsoleLog = {
|
|
2206
2352
|
content: payload.content,
|
|
2207
2353
|
name: payload.level,
|
|
2354
|
+
taskId: payload.taskId,
|
|
2355
|
+
taskName: payload.taskName,
|
|
2356
|
+
taskParentNames: payload.taskParentNames,
|
|
2357
|
+
taskType: payload.taskType,
|
|
2208
2358
|
testPath: payload.testPath,
|
|
2209
2359
|
type: payload.type,
|
|
2210
2360
|
trace: payload.trace,
|
|
2211
2361
|
};
|
|
2212
|
-
|
|
2213
|
-
|
|
2214
|
-
|
|
2215
|
-
|
|
2216
|
-
|
|
2217
|
-
|
|
2218
|
-
|
|
2219
|
-
|
|
2362
|
+
if (context.normalizedConfig.silent === true) {
|
|
2363
|
+
return;
|
|
2364
|
+
}
|
|
2365
|
+
|
|
2366
|
+
if (context.normalizedConfig.silent === 'passed-only') {
|
|
2367
|
+
bufferConsoleLog(log);
|
|
2368
|
+
return;
|
|
2369
|
+
}
|
|
2370
|
+
|
|
2371
|
+
if (context.normalizedConfig.disableConsoleIntercept) {
|
|
2372
|
+
return;
|
|
2220
2373
|
}
|
|
2374
|
+
|
|
2375
|
+
await emitUserConsoleLog(log);
|
|
2221
2376
|
};
|
|
2222
2377
|
|
|
2223
2378
|
const handleFatal = async (payload: FatalPayload): Promise<void> => {
|
|
@@ -2227,6 +2382,113 @@ export const runBrowserController = async (
|
|
|
2227
2382
|
ensureProcessExitCode(1);
|
|
2228
2383
|
};
|
|
2229
2384
|
|
|
2385
|
+
const bufferedConsoleLogs = new Map<string, UserConsoleLog[]>();
|
|
2386
|
+
const suiteIdsByChain = new Map<string, string>();
|
|
2387
|
+
|
|
2388
|
+
const getSuiteChainKey = (names: string[]): string => {
|
|
2389
|
+
return names.join('\u0000');
|
|
2390
|
+
};
|
|
2391
|
+
|
|
2392
|
+
const pushTaskId = (taskIds: string[], taskId: string): void => {
|
|
2393
|
+
if (!taskIds.includes(taskId)) {
|
|
2394
|
+
taskIds.push(taskId);
|
|
2395
|
+
}
|
|
2396
|
+
};
|
|
2397
|
+
|
|
2398
|
+
const shouldEmitUserConsoleLog = (log: UserConsoleLog): boolean => {
|
|
2399
|
+
return context.normalizedConfig.onConsoleLog?.(log.content) !== false;
|
|
2400
|
+
};
|
|
2401
|
+
|
|
2402
|
+
const emitUserConsoleLog = async (log: UserConsoleLog): Promise<void> => {
|
|
2403
|
+
if (!shouldEmitUserConsoleLog(log)) {
|
|
2404
|
+
return;
|
|
2405
|
+
}
|
|
2406
|
+
|
|
2407
|
+
await Promise.all(
|
|
2408
|
+
context.reporters.map((reporter) =>
|
|
2409
|
+
(reporter as Reporter).onUserConsoleLog?.(log),
|
|
2410
|
+
),
|
|
2411
|
+
);
|
|
2412
|
+
};
|
|
2413
|
+
|
|
2414
|
+
const bufferConsoleLog = (log: UserConsoleLog): void => {
|
|
2415
|
+
const taskId = getBufferedLogTaskId(log);
|
|
2416
|
+
const logs = bufferedConsoleLogs.get(taskId) || [];
|
|
2417
|
+
logs.push(log);
|
|
2418
|
+
bufferedConsoleLogs.set(taskId, logs);
|
|
2419
|
+
|
|
2420
|
+
if (log.taskType === 'suite' && log.taskId) {
|
|
2421
|
+
suiteIdsByChain.set(
|
|
2422
|
+
getSuiteChainKey([...(log.taskParentNames || []), log.taskName || '']),
|
|
2423
|
+
log.taskId,
|
|
2424
|
+
);
|
|
2425
|
+
}
|
|
2426
|
+
};
|
|
2427
|
+
|
|
2428
|
+
const flushBufferedLogsForTask = async ({
|
|
2429
|
+
taskId,
|
|
2430
|
+
status,
|
|
2431
|
+
taskParentNames,
|
|
2432
|
+
taskType,
|
|
2433
|
+
testPath,
|
|
2434
|
+
}: {
|
|
2435
|
+
taskId: string;
|
|
2436
|
+
status: TestResult['status'];
|
|
2437
|
+
taskParentNames?: string[];
|
|
2438
|
+
taskType?: 'file' | 'suite' | 'case';
|
|
2439
|
+
testPath: string;
|
|
2440
|
+
}): Promise<void> => {
|
|
2441
|
+
if (status !== 'fail') {
|
|
2442
|
+
bufferedConsoleLogs.delete(taskId);
|
|
2443
|
+
return;
|
|
2444
|
+
}
|
|
2445
|
+
|
|
2446
|
+
const taskIdsToFlush: string[] = [];
|
|
2447
|
+
|
|
2448
|
+
if (taskType === 'case') {
|
|
2449
|
+
pushTaskId(taskIdsToFlush, getFileTaskId(testPath));
|
|
2450
|
+
|
|
2451
|
+
const suiteNames = taskParentNames || [];
|
|
2452
|
+
for (let i = 0; i < suiteNames.length; i++) {
|
|
2453
|
+
const suiteId = suiteIdsByChain.get(
|
|
2454
|
+
getSuiteChainKey(suiteNames.slice(0, i + 1)),
|
|
2455
|
+
);
|
|
2456
|
+
|
|
2457
|
+
if (suiteId) {
|
|
2458
|
+
pushTaskId(taskIdsToFlush, suiteId);
|
|
2459
|
+
}
|
|
2460
|
+
}
|
|
2461
|
+
|
|
2462
|
+
pushTaskId(taskIdsToFlush, taskId);
|
|
2463
|
+
}
|
|
2464
|
+
|
|
2465
|
+
if (taskType === 'suite') {
|
|
2466
|
+
pushTaskId(taskIdsToFlush, getFileTaskId(testPath));
|
|
2467
|
+
pushTaskId(taskIdsToFlush, taskId);
|
|
2468
|
+
}
|
|
2469
|
+
|
|
2470
|
+
if (taskType === 'file') {
|
|
2471
|
+
pushTaskId(taskIdsToFlush, taskId);
|
|
2472
|
+
}
|
|
2473
|
+
|
|
2474
|
+
for (const bufferedTaskId of taskIdsToFlush) {
|
|
2475
|
+
const logs = bufferedConsoleLogs.get(bufferedTaskId);
|
|
2476
|
+
if (!logs) {
|
|
2477
|
+
continue;
|
|
2478
|
+
}
|
|
2479
|
+
|
|
2480
|
+
bufferedConsoleLogs.delete(bufferedTaskId);
|
|
2481
|
+
|
|
2482
|
+
for (const log of logs) {
|
|
2483
|
+
await Promise.all(
|
|
2484
|
+
context.reporters.map((reporter) =>
|
|
2485
|
+
(reporter as Reporter).onUserConsoleLog?.(log),
|
|
2486
|
+
),
|
|
2487
|
+
);
|
|
2488
|
+
}
|
|
2489
|
+
}
|
|
2490
|
+
};
|
|
2491
|
+
|
|
2230
2492
|
const runSnapshotRpc = async (
|
|
2231
2493
|
request: SnapshotRpcRequest,
|
|
2232
2494
|
): Promise<unknown> => {
|
|
@@ -2613,6 +2875,69 @@ export const runBrowserController = async (
|
|
|
2613
2875
|
},
|
|
2614
2876
|
});
|
|
2615
2877
|
|
|
2878
|
+
if (allTestFiles.length === 0) {
|
|
2879
|
+
const duration = {
|
|
2880
|
+
totalTime: buildTime,
|
|
2881
|
+
buildTime,
|
|
2882
|
+
testTime: 0,
|
|
2883
|
+
};
|
|
2884
|
+
const result = {
|
|
2885
|
+
results: reporterResults,
|
|
2886
|
+
testResults: caseResults,
|
|
2887
|
+
duration,
|
|
2888
|
+
hasFailure: false,
|
|
2889
|
+
getSourcemap: getBrowserSourcemap,
|
|
2890
|
+
resolveSourcemap: resolveBrowserSourcemap,
|
|
2891
|
+
close: skipOnTestRunEnd
|
|
2892
|
+
? async () => {
|
|
2893
|
+
sessionRegistry.clear();
|
|
2894
|
+
await destroyBrowserRuntime(runtime);
|
|
2895
|
+
}
|
|
2896
|
+
: undefined,
|
|
2897
|
+
};
|
|
2898
|
+
|
|
2899
|
+
if (!skipOnTestRunEnd) {
|
|
2900
|
+
await notifyTestRunEnd({ duration });
|
|
2901
|
+
}
|
|
2902
|
+
|
|
2903
|
+
if (isWatchMode) {
|
|
2904
|
+
triggerRerun = async () => {
|
|
2905
|
+
const newProjectEntries = await collectProjectEntries(context);
|
|
2906
|
+
const rerunPlan = planWatchRerun({
|
|
2907
|
+
projectEntries: newProjectEntries,
|
|
2908
|
+
previousTestFiles: watchContext.lastTestFiles,
|
|
2909
|
+
affectedTestFiles: watchContext.affectedTestFiles,
|
|
2910
|
+
});
|
|
2911
|
+
watchContext.affectedTestFiles = [];
|
|
2912
|
+
|
|
2913
|
+
if (rerunPlan.filesChanged) {
|
|
2914
|
+
watchContext.lastTestFiles = rerunPlan.currentTestFiles;
|
|
2915
|
+
if (rerunPlan.currentTestFiles.length === 0) {
|
|
2916
|
+
logger.log(
|
|
2917
|
+
color.cyan('No browser test files remain after update.\n'),
|
|
2918
|
+
);
|
|
2919
|
+
logBrowserWatchReadyMessage(enableCliShortcuts);
|
|
2920
|
+
return;
|
|
2921
|
+
}
|
|
2922
|
+
|
|
2923
|
+
logger.log(
|
|
2924
|
+
color.cyan(
|
|
2925
|
+
`Test file set changed, re-running ${rerunPlan.currentTestFiles.length} file(s)...\n`,
|
|
2926
|
+
),
|
|
2927
|
+
);
|
|
2928
|
+
void latestRerunScheduler.enqueueLatest(rerunPlan.currentTestFiles);
|
|
2929
|
+
return;
|
|
2930
|
+
}
|
|
2931
|
+
|
|
2932
|
+
logBrowserWatchReadyMessage(enableCliShortcuts);
|
|
2933
|
+
};
|
|
2934
|
+
watchContext.hooksEnabled = true;
|
|
2935
|
+
logBrowserWatchReadyMessage(enableCliShortcuts);
|
|
2936
|
+
}
|
|
2937
|
+
|
|
2938
|
+
return result;
|
|
2939
|
+
}
|
|
2940
|
+
|
|
2616
2941
|
const testStart = Date.now();
|
|
2617
2942
|
await runFilesWithPool(allTestFiles);
|
|
2618
2943
|
const testTime = Date.now() - testStart;
|
|
@@ -3089,24 +3414,27 @@ export const runBrowserController = async (
|
|
|
3089
3414
|
});
|
|
3090
3415
|
};
|
|
3091
3416
|
|
|
3092
|
-
|
|
3093
|
-
|
|
3094
|
-
|
|
3095
|
-
|
|
3096
|
-
|
|
3417
|
+
let testTime = 0;
|
|
3418
|
+
if (currentTestFiles.length > 0) {
|
|
3419
|
+
const testStart = Date.now();
|
|
3420
|
+
try {
|
|
3421
|
+
await waitForRunnerFramesReady(
|
|
3422
|
+
currentTestFiles.map((file) => file.testPath),
|
|
3423
|
+
);
|
|
3097
3424
|
|
|
3098
|
-
|
|
3099
|
-
|
|
3100
|
-
|
|
3101
|
-
|
|
3425
|
+
for (const file of currentTestFiles) {
|
|
3426
|
+
await enqueueHeadedReload(file);
|
|
3427
|
+
if (fatalError) {
|
|
3428
|
+
break;
|
|
3429
|
+
}
|
|
3102
3430
|
}
|
|
3431
|
+
} catch (error) {
|
|
3432
|
+
fatalError = fatalError ?? toError(error);
|
|
3433
|
+
ensureProcessExitCode(1);
|
|
3103
3434
|
}
|
|
3104
|
-
} catch (error) {
|
|
3105
|
-
fatalError = fatalError ?? toError(error);
|
|
3106
|
-
ensureProcessExitCode(1);
|
|
3107
|
-
}
|
|
3108
3435
|
|
|
3109
|
-
|
|
3436
|
+
testTime = Date.now() - testStart;
|
|
3437
|
+
}
|
|
3110
3438
|
|
|
3111
3439
|
// Define rerun logic for watch mode
|
|
3112
3440
|
if (isWatchMode) {
|
|
@@ -3130,6 +3458,13 @@ export const runBrowserController = async (
|
|
|
3130
3458
|
watchContext.lastTestFiles = rerunPlan.currentTestFiles;
|
|
3131
3459
|
currentTestFiles = rerunPlan.currentTestFiles;
|
|
3132
3460
|
await rpcManager.notifyTestFileUpdate(currentTestFiles);
|
|
3461
|
+
if (currentTestFiles.length === 0) {
|
|
3462
|
+
logger.log(
|
|
3463
|
+
color.cyan('No browser test files remain after update.\n'),
|
|
3464
|
+
);
|
|
3465
|
+
logBrowserWatchReadyMessage(enableCliShortcuts);
|
|
3466
|
+
return;
|
|
3467
|
+
}
|
|
3133
3468
|
await waitForRunnerFramesReady(
|
|
3134
3469
|
currentTestFiles.map((file) => file.testPath),
|
|
3135
3470
|
);
|
package/src/protocol.ts
CHANGED
|
@@ -106,6 +106,10 @@ export type BrowserClientMessage =
|
|
|
106
106
|
payload: {
|
|
107
107
|
level: 'log' | 'warn' | 'error' | 'info' | 'debug';
|
|
108
108
|
content: string;
|
|
109
|
+
taskId?: string;
|
|
110
|
+
taskName?: string;
|
|
111
|
+
taskParentNames?: string[];
|
|
112
|
+
taskType?: 'file' | 'suite' | 'case';
|
|
109
113
|
testPath: string;
|
|
110
114
|
type: 'stdout' | 'stderr';
|
|
111
115
|
trace?: string;
|