@rstest/browser 0.8.1 → 0.8.3

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.
@@ -4,4 +4,5 @@ declare global {
4
4
  __RSTEST_BROWSER_OPTIONS__?: BrowserHostConfig;
5
5
  __rstest_dispatch__?: (message: BrowserClientMessage) => void;
6
6
  }
7
+ var __coverage__: Record<string, unknown> | undefined;
7
8
  }
@@ -14,4 +14,8 @@ export type ListBrowserTestsResult = {
14
14
  * This function creates a headless browser runtime, loads test files,
15
15
  * and collects their test structure (describe/test declarations).
16
16
  */
17
- export declare const listBrowserTests: (context: Rstest) => Promise<ListBrowserTestsResult>;
17
+ export declare const listBrowserTests: (context: Rstest, options?: {
18
+ shardedEntries?: Map<string, {
19
+ entries: Record<string, string>;
20
+ }>;
21
+ }) => Promise<ListBrowserTestsResult>;
package/dist/index.js CHANGED
@@ -2,7 +2,7 @@ import { __webpack_require__ } from "./rslib-runtime.js";
2
2
  import { existsSync } from "node:fs";
3
3
  import promises from "node:fs/promises";
4
4
  import { fileURLToPath } from "node:url";
5
- import { TEMP_RSTEST_OUTPUT_DIR, color, getSetupFiles, getTestEntries, isDebug, logger, rsbuild, serializableConfig } from "@rstest/core/browser";
5
+ import { TEMP_RSTEST_OUTPUT_DIR, color, getSetupFiles, getTestEntries, isDebug, loadCoverageProvider, logger, rsbuild, serializableConfig } from "@rstest/core/browser";
6
6
  import open_editor from "open-editor";
7
7
  import { basename, dirname, join, normalize, relative, resolve as external_pathe_resolve } from "pathe";
8
8
  import sirv from "sirv";
@@ -1917,6 +1917,7 @@ const htmlTemplate = `<!DOCTYPE html>
1917
1917
  </body>
1918
1918
  </html>
1919
1919
  `;
1920
+ const VIRTUAL_MANIFEST_FILENAME = 'virtual-manifest.ts';
1920
1921
  const destroyBrowserRuntime = async (runtime)=>{
1921
1922
  try {
1922
1923
  await runtime.browser?.close?.();
@@ -1966,6 +1967,7 @@ const createBrowserRuntime = async ({ context, manifestPath, manifestSource, tem
1966
1967
  const firstProject = browserProjects[0];
1967
1968
  const userPlugins = firstProject?.normalizedConfig.plugins || [];
1968
1969
  const userRsbuildConfig = firstProject?.normalizedConfig ?? {};
1970
+ const browserConfig = firstProject?.normalizedConfig.browser ?? context.normalizedConfig.browser;
1969
1971
  const browserRuntimePath = fileURLToPath(import.meta.resolve('@rstest/core/browser-runtime'));
1970
1972
  const rstestInternalAliases = {
1971
1973
  '@rstest/browser-manifest': manifestPath,
@@ -1981,8 +1983,8 @@ const createBrowserRuntime = async ({ context, manifestPath, manifestSource, tem
1981
1983
  plugins: userPlugins,
1982
1984
  server: {
1983
1985
  printUrls: false,
1984
- port: context.normalizedConfig.browser.port ?? 4000,
1985
- strictPort: context.normalizedConfig.browser.strictPort
1986
+ port: browserConfig.port ?? 4000,
1987
+ strictPort: browserConfig.strictPort
1986
1988
  },
1987
1989
  dev: {
1988
1990
  client: {
@@ -1990,7 +1992,7 @@ const createBrowserRuntime = async ({ context, manifestPath, manifestSource, tem
1990
1992
  }
1991
1993
  },
1992
1994
  environments: {
1993
- web: {}
1995
+ [firstProject?.environmentName || 'web']: {}
1994
1996
  }
1995
1997
  }
1996
1998
  });
@@ -2067,6 +2069,13 @@ const createBrowserRuntime = async ({ context, manifestPath, manifestSource, tem
2067
2069
  }
2068
2070
  }
2069
2071
  ]);
2072
+ const coverage = firstProject?.normalizedConfig.coverage;
2073
+ if (coverage?.enabled && 'list' !== context.command) {
2074
+ const { pluginCoverage } = await loadCoverageProvider(coverage, context.rootPath);
2075
+ rsbuildInstance.addPlugins([
2076
+ pluginCoverage(coverage)
2077
+ ]);
2078
+ }
2070
2079
  const devServer = await rsbuildInstance.createDevServer({
2071
2080
  getPortSilently: true
2072
2081
  });
@@ -2177,7 +2186,7 @@ const createBrowserRuntime = async ({ context, manifestPath, manifestSource, tem
2177
2186
  const wsPort = wss.address().port;
2178
2187
  logger.debug(`[Browser UI] WebSocket server started on port ${wsPort}`);
2179
2188
  let browserLauncher;
2180
- const browserName = context.normalizedConfig.browser.browser;
2189
+ const browserName = browserConfig.browser;
2181
2190
  try {
2182
2191
  const playwright = await import("playwright");
2183
2192
  browserLauncher = playwright[browserName];
@@ -2189,7 +2198,7 @@ const createBrowserRuntime = async ({ context, manifestPath, manifestSource, tem
2189
2198
  let browser;
2190
2199
  try {
2191
2200
  browser = await browserLauncher.launch({
2192
- headless: forceHeadless ?? context.normalizedConfig.browser.headless,
2201
+ headless: forceHeadless ?? browserConfig.headless,
2193
2202
  args: 'chromium' === browserName ? [
2194
2203
  '--disable-popup-blocking',
2195
2204
  '--no-first-run',
@@ -2214,6 +2223,25 @@ const createBrowserRuntime = async ({ context, manifestPath, manifestSource, tem
2214
2223
  wss
2215
2224
  };
2216
2225
  };
2226
+ async function resolveProjectEntries(context, shardedEntries) {
2227
+ if (shardedEntries) {
2228
+ const browserProjects = getBrowserProjects(context);
2229
+ const projectEntries = [];
2230
+ for (const project of browserProjects){
2231
+ const entryInfo = shardedEntries.get(project.environmentName);
2232
+ if (entryInfo && Object.keys(entryInfo.entries).length > 0) {
2233
+ const setup = getSetupFiles(project.normalizedConfig.setupFiles, project.rootPath);
2234
+ projectEntries.push({
2235
+ project,
2236
+ setupFiles: Object.values(setup),
2237
+ testFiles: Object.values(entryInfo.entries)
2238
+ });
2239
+ }
2240
+ }
2241
+ return projectEntries;
2242
+ }
2243
+ return collectProjectEntries(context);
2244
+ }
2217
2245
  const runBrowserController = async (context, options)=>{
2218
2246
  const { skipOnTestRunEnd = false } = options ?? {};
2219
2247
  const buildStart = Date.now();
@@ -2235,17 +2263,21 @@ const runBrowserController = async (context, options)=>{
2235
2263
  ensureProcessExitCode(1);
2236
2264
  return;
2237
2265
  }
2238
- const projectEntries = await collectProjectEntries(context);
2266
+ const projectEntries = await resolveProjectEntries(context, options?.shardedEntries);
2239
2267
  const totalTests = projectEntries.reduce((total, item)=>total + item.testFiles.length, 0);
2240
2268
  if (0 === totalTests) {
2241
2269
  const code = context.normalizedConfig.passWithNoTests ? 0 : 1;
2242
- if (!skipOnTestRunEnd) logger.log(color[code ? 'red' : 'yellow'](`No test files found, exiting with code ${code}.`));
2270
+ if (!skipOnTestRunEnd) {
2271
+ const message = `No test files found, exiting with code ${code}.`;
2272
+ if (0 === code) logger.log(color.yellow(message));
2273
+ else logger.error(color.red(message));
2274
+ }
2243
2275
  if (0 !== code) ensureProcessExitCode(code);
2244
2276
  return;
2245
2277
  }
2246
2278
  const isWatchMode = 'watch' === context.command;
2247
2279
  const tempDir = isWatchMode && watchContext.runtime ? watchContext.runtime.tempDir : isWatchMode ? join(context.rootPath, TEMP_RSTEST_OUTPUT_DIR, 'browser', 'watch') : join(context.rootPath, TEMP_RSTEST_OUTPUT_DIR, 'browser', Date.now().toString());
2248
- const manifestPath = join(tempDir, 'manifest.ts');
2280
+ const manifestPath = join(tempDir, VIRTUAL_MANIFEST_FILENAME);
2249
2281
  const manifestSource = generateManifestModule({
2250
2282
  manifestPath,
2251
2283
  entries: projectEntries
@@ -2499,15 +2531,15 @@ const runBrowserController = async (context, options)=>{
2499
2531
  }
2500
2532
  return result;
2501
2533
  };
2502
- const listBrowserTests = async (context)=>{
2503
- const projectEntries = await collectProjectEntries(context);
2534
+ const listBrowserTests = async (context, options)=>{
2535
+ const projectEntries = await resolveProjectEntries(context, options?.shardedEntries);
2504
2536
  const totalTests = projectEntries.reduce((total, item)=>total + item.testFiles.length, 0);
2505
2537
  if (0 === totalTests) return {
2506
2538
  list: [],
2507
2539
  close: async ()=>{}
2508
2540
  };
2509
2541
  const tempDir = join(context.rootPath, TEMP_RSTEST_OUTPUT_DIR, 'browser', `list-${Date.now()}`);
2510
- const manifestPath = join(tempDir, 'manifest.ts');
2542
+ const manifestPath = join(tempDir, VIRTUAL_MANIFEST_FILENAME);
2511
2543
  const manifestSource = generateManifestModule({
2512
2544
  manifestPath,
2513
2545
  entries: projectEntries
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rstest/browser",
3
- "version": "0.8.1",
3
+ "version": "0.8.3",
4
4
  "description": "Browser mode support for Rstest testing framework.",
5
5
  "bugs": {
6
6
  "url": "https://github.com/web-infra-dev/rstest/issues"
@@ -53,12 +53,12 @@
53
53
  "picomatch": "^4.0.3",
54
54
  "playwright": "^1.49.1",
55
55
  "@rstest/browser-ui": "0.0.0",
56
- "@rstest/core": "0.8.1",
56
+ "@rstest/core": "0.8.3",
57
57
  "@rstest/tsconfig": "0.0.1"
58
58
  },
59
59
  "peerDependencies": {
60
60
  "playwright": "^1.49.1",
61
- "@rstest/core": "^0.8.1"
61
+ "@rstest/core": "^0.8.3"
62
62
  },
63
63
  "peerDependenciesMeta": {
64
64
  "playwright": {
@@ -7,6 +7,7 @@ import {
7
7
  projectTestContexts,
8
8
  } from '@rstest/browser-manifest';
9
9
  import type {
10
+ CoverageMapData,
10
11
  RunnerHooks,
11
12
  RuntimeConfig,
12
13
  WorkerState,
@@ -35,6 +36,8 @@ declare global {
35
36
  __RSTEST_BROWSER_OPTIONS__?: BrowserHostConfig;
36
37
  __rstest_dispatch__?: (message: BrowserClientMessage) => void;
37
38
  }
39
+ // eslint-disable-next-line no-var
40
+ var __coverage__: Record<string, unknown> | undefined;
38
41
  }
39
42
 
40
43
  /**
@@ -589,6 +592,11 @@ const run = async () => {
589
592
  runtime.api,
590
593
  );
591
594
 
595
+ // Collect coverage data from global __coverage__ object
596
+ if (globalThis.__coverage__) {
597
+ result.coverage = globalThis.__coverage__ as CoverageMapData;
598
+ }
599
+
592
600
  send({
593
601
  type: 'file-complete',
594
602
  payload: result,
@@ -12,6 +12,7 @@ import {
12
12
  getTestEntries,
13
13
  isDebug,
14
14
  type ListCommandResult,
15
+ loadCoverageProvider,
15
16
  logger,
16
17
  type ProjectContext,
17
18
  type Reporter,
@@ -715,6 +716,11 @@ const htmlTemplate = `<!DOCTYPE html>
715
716
  </html>
716
717
  `;
717
718
 
719
+ // Workaround for noisy "removed ..." logs caused by VirtualModulesPlugin.
720
+ // Rsbuild suppresses the removed-file log if all removed paths include "virtual":
721
+ // https://github.com/web-infra-dev/rsbuild/blob/1258fa9dba5c321a4629b591a6dadbd2e26c6963/packages/core/src/createCompiler.ts#L73-L76
722
+ const VIRTUAL_MANIFEST_FILENAME = 'virtual-manifest.ts';
723
+
718
724
  // ============================================================================
719
725
  // Browser Runtime Lifecycle
720
726
  // ============================================================================
@@ -817,6 +823,8 @@ const createBrowserRuntime = async ({
817
823
  const firstProject = browserProjects[0];
818
824
  const userPlugins = firstProject?.normalizedConfig.plugins || [];
819
825
  const userRsbuildConfig = firstProject?.normalizedConfig ?? {};
826
+ const browserConfig =
827
+ firstProject?.normalizedConfig.browser ?? context.normalizedConfig.browser;
820
828
 
821
829
  // Rstest internal aliases that must not be overridden by user config
822
830
  const browserRuntimePath = fileURLToPath(
@@ -841,8 +849,8 @@ const createBrowserRuntime = async ({
841
849
  plugins: userPlugins,
842
850
  server: {
843
851
  printUrls: false,
844
- port: context.normalizedConfig.browser.port ?? 4000,
845
- strictPort: context.normalizedConfig.browser.strictPort,
852
+ port: browserConfig.port ?? 4000,
853
+ strictPort: browserConfig.strictPort,
846
854
  },
847
855
  dev: {
848
856
  client: {
@@ -850,7 +858,7 @@ const createBrowserRuntime = async ({
850
858
  },
851
859
  },
852
860
  environments: {
853
- web: {},
861
+ [firstProject?.environmentName || 'web']: {},
854
862
  },
855
863
  },
856
864
  });
@@ -970,6 +978,16 @@ const createBrowserRuntime = async ({
970
978
  ]);
971
979
  }
972
980
 
981
+ // Register coverage plugin for browser mode
982
+ const coverage = firstProject?.normalizedConfig.coverage;
983
+ if (coverage?.enabled && context.command !== 'list') {
984
+ const { pluginCoverage } = await loadCoverageProvider(
985
+ coverage,
986
+ context.rootPath,
987
+ );
988
+ rsbuildInstance.addPlugins([pluginCoverage(coverage)]);
989
+ }
990
+
973
991
  const devServer = await rsbuildInstance.createDevServer({
974
992
  getPortSilently: true,
975
993
  });
@@ -1134,7 +1152,7 @@ const createBrowserRuntime = async ({
1134
1152
  logger.debug(`[Browser UI] WebSocket server started on port ${wsPort}`);
1135
1153
 
1136
1154
  let browserLauncher: BrowserType;
1137
- const browserName = context.normalizedConfig.browser.browser;
1155
+ const browserName = browserConfig.browser;
1138
1156
  try {
1139
1157
  const playwright = await import('playwright');
1140
1158
  browserLauncher = playwright[browserName];
@@ -1147,7 +1165,7 @@ const createBrowserRuntime = async ({
1147
1165
  let browser: BrowserInstance;
1148
1166
  try {
1149
1167
  browser = await browserLauncher.launch({
1150
- headless: forceHeadless ?? context.normalizedConfig.browser.headless,
1168
+ headless: forceHeadless ?? browserConfig.headless,
1151
1169
  // Chromium-specific args (ignored by other browsers)
1152
1170
  args:
1153
1171
  browserName === 'chromium'
@@ -1178,6 +1196,32 @@ const createBrowserRuntime = async ({
1178
1196
  };
1179
1197
  };
1180
1198
 
1199
+ async function resolveProjectEntries(
1200
+ context: Rstest,
1201
+ shardedEntries?: Map<string, { entries: Record<string, string> }>,
1202
+ ): Promise<BrowserProjectEntries[]> {
1203
+ if (shardedEntries) {
1204
+ const browserProjects = getBrowserProjects(context);
1205
+ const projectEntries: BrowserProjectEntries[] = [];
1206
+ for (const project of browserProjects) {
1207
+ const entryInfo = shardedEntries.get(project.environmentName);
1208
+ if (entryInfo && Object.keys(entryInfo.entries).length > 0) {
1209
+ const setup = getSetupFiles(
1210
+ project.normalizedConfig.setupFiles,
1211
+ project.rootPath,
1212
+ );
1213
+ projectEntries.push({
1214
+ project,
1215
+ setupFiles: Object.values(setup),
1216
+ testFiles: Object.values(entryInfo.entries),
1217
+ });
1218
+ }
1219
+ }
1220
+ return projectEntries;
1221
+ }
1222
+ return collectProjectEntries(context);
1223
+ }
1224
+
1181
1225
  // ============================================================================
1182
1226
  // Main Entry Point
1183
1227
  // ============================================================================
@@ -1219,7 +1263,10 @@ export const runBrowserController = async (
1219
1263
  }
1220
1264
  }
1221
1265
 
1222
- const projectEntries = await collectProjectEntries(context);
1266
+ const projectEntries = await resolveProjectEntries(
1267
+ context,
1268
+ options?.shardedEntries,
1269
+ );
1223
1270
  const totalTests = projectEntries.reduce(
1224
1271
  (total, item) => total + item.testFiles.length,
1225
1272
  0,
@@ -1228,11 +1275,12 @@ export const runBrowserController = async (
1228
1275
  if (totalTests === 0) {
1229
1276
  const code = context.normalizedConfig.passWithNoTests ? 0 : 1;
1230
1277
  if (!skipOnTestRunEnd) {
1231
- logger.log(
1232
- color[code ? 'red' : 'yellow'](
1233
- `No test files found, exiting with code ${code}.`,
1234
- ),
1235
- );
1278
+ const message = `No test files found, exiting with code ${code}.`;
1279
+ if (code === 0) {
1280
+ logger.log(color.yellow(message));
1281
+ } else {
1282
+ logger.error(color.red(message));
1283
+ }
1236
1284
  }
1237
1285
 
1238
1286
  if (code !== 0) {
@@ -1253,8 +1301,8 @@ export const runBrowserController = async (
1253
1301
  'browser',
1254
1302
  Date.now().toString(),
1255
1303
  );
1256
- const manifestPath = join(tempDir, 'manifest.ts');
1257
1304
 
1305
+ const manifestPath = join(tempDir, VIRTUAL_MANIFEST_FILENAME);
1258
1306
  const manifestSource = generateManifestModule({
1259
1307
  manifestPath,
1260
1308
  entries: projectEntries,
@@ -1700,8 +1748,14 @@ export type ListBrowserTestsResult = {
1700
1748
  */
1701
1749
  export const listBrowserTests = async (
1702
1750
  context: Rstest,
1751
+ options?: {
1752
+ shardedEntries?: Map<string, { entries: Record<string, string> }>;
1753
+ },
1703
1754
  ): Promise<ListBrowserTestsResult> => {
1704
- const projectEntries = await collectProjectEntries(context);
1755
+ const projectEntries = await resolveProjectEntries(
1756
+ context,
1757
+ options?.shardedEntries,
1758
+ );
1705
1759
  const totalTests = projectEntries.reduce(
1706
1760
  (total, item) => total + item.testFiles.length,
1707
1761
  0,
@@ -1720,8 +1774,8 @@ export const listBrowserTests = async (
1720
1774
  'browser',
1721
1775
  `list-${Date.now()}`,
1722
1776
  );
1723
- const manifestPath = join(tempDir, 'manifest.ts');
1724
1777
 
1778
+ const manifestPath = join(tempDir, VIRTUAL_MANIFEST_FILENAME);
1725
1779
  const manifestSource = generateManifestModule({
1726
1780
  manifestPath,
1727
1781
  entries: projectEntries,