@rstest/browser 0.11.5 → 0.11.7

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.
Files changed (86) hide show
  1. package/dist/browser-container/container-static/css/index.43daaf6c93.css +1 -0
  2. package/dist/browser-container/container-static/js/260.3739fe815c.js +194 -0
  3. package/dist/browser-container/container-static/js/index.c0f87bfb3e.js +1 -0
  4. package/dist/browser-container/index.html +1 -1
  5. package/dist/browserExecutor.d.ts +7 -5
  6. package/dist/browserRsbuild.d.ts +139 -0
  7. package/dist/browserV8Coverage.d.ts +15 -0
  8. package/dist/client/240~1.js +150 -0
  9. package/dist/{augmentExpect.d.ts → client/client/augmentExpect.d.ts} +1 -1
  10. package/dist/client/{browserRpc.d.ts → client/browserRpc.d.ts} +1 -1
  11. package/dist/client/{dispatchTransport.d.ts → client/dispatchTransport.d.ts} +8 -4
  12. package/dist/client/client/index.d.ts +2 -0
  13. package/dist/client/client/runIdentity.d.ts +12 -0
  14. package/dist/client/client/runner.d.ts +4 -0
  15. package/dist/client/client/snapshot.d.ts +27 -0
  16. package/dist/client/client/sourceMapSupport.d.ts +36 -0
  17. package/dist/{browser.js → client/index.js} +3 -102
  18. package/dist/client/protocol.d.ts +225 -0
  19. package/dist/client/rpcProtocol.d.ts +138 -0
  20. package/dist/client/runner.js +675 -0
  21. package/dist/client/sourceMap/sourceMapLoader.d.ts +25 -0
  22. package/dist/containerRpc.d.ts +60 -0
  23. package/dist/dispatchCapabilities.d.ts +1 -2
  24. package/dist/headedRunRegistry.d.ts +51 -0
  25. package/dist/headedScheduler.d.ts +58 -0
  26. package/dist/headlessScheduler.d.ts +38 -0
  27. package/dist/headlessTransport.d.ts +4 -2
  28. package/dist/hostController.d.ts +12 -47
  29. package/dist/hostPayloads.d.ts +24 -0
  30. package/dist/index.js +2019 -1516
  31. package/dist/protocol.d.ts +48 -8
  32. package/dist/providers/index.d.ts +21 -0
  33. package/dist/providers/playwright/v8Coverage.d.ts +2 -0
  34. package/dist/rpcProtocol.d.ts +1 -2
  35. package/dist/schedulerSeam.d.ts +42 -0
  36. package/dist/sourceMap/sourceMapLoader.d.ts +12 -1
  37. package/dist/watchRerunPlanner.d.ts +20 -4
  38. package/dist/watchRuntime.d.ts +21 -0
  39. package/dist/watchSignals.d.ts +29 -0
  40. package/package.json +12 -12
  41. package/dist/33.js +0 -7
  42. package/dist/browser-container/container-static/css/index.0521ff95b1.css +0 -1
  43. package/dist/browser-container/container-static/js/506.b2f920fa67.js +0 -194
  44. package/dist/browser-container/container-static/js/index.d27fd63f5e.js +0 -1
  45. package/dist/browser.d.ts +0 -2
  46. package/dist/headlessLatestRerunScheduler.d.ts +0 -18
  47. package/src/augmentExpect.ts +0 -62
  48. package/src/browser.ts +0 -3
  49. package/src/browserExecutor.ts +0 -140
  50. package/src/browserRpcRegistry.ts +0 -57
  51. package/src/client/api.ts +0 -213
  52. package/src/client/browserRpc.ts +0 -86
  53. package/src/client/dispatchTransport.ts +0 -240
  54. package/src/client/entry.ts +0 -807
  55. package/src/client/locator.ts +0 -448
  56. package/src/client/snapshot.ts +0 -113
  57. package/src/client/sourceMapSupport.ts +0 -159
  58. package/src/concurrency.ts +0 -65
  59. package/src/configValidation.ts +0 -263
  60. package/src/dispatchCapabilities.ts +0 -182
  61. package/src/dispatchRouter.ts +0 -82
  62. package/src/env.d.ts +0 -54
  63. package/src/headedSerialTaskQueue.ts +0 -23
  64. package/src/headlessLatestRerunScheduler.ts +0 -76
  65. package/src/headlessTransport.ts +0 -28
  66. package/src/hostController.ts +0 -4633
  67. package/src/index.ts +0 -51
  68. package/src/manifest.d.ts +0 -41
  69. package/src/protocol.ts +0 -225
  70. package/src/providers/index.ts +0 -126
  71. package/src/providers/playwright/compileLocator.ts +0 -130
  72. package/src/providers/playwright/dispatchBrowserRpc.ts +0 -372
  73. package/src/providers/playwright/expectUtils.ts +0 -57
  74. package/src/providers/playwright/implementation.ts +0 -35
  75. package/src/providers/playwright/index.ts +0 -1
  76. package/src/providers/playwright/runtime.ts +0 -159
  77. package/src/providers/playwright/textMatcher.ts +0 -10
  78. package/src/rpcProtocol.ts +0 -218
  79. package/src/runSession.ts +0 -110
  80. package/src/sessionRegistry.ts +0 -89
  81. package/src/sourceMap/sourceMapLoader.ts +0 -96
  82. package/src/viewportPresets.ts +0 -69
  83. package/src/watchRerunPlanner.ts +0 -77
  84. /package/dist/{rslib-runtime.js → 612~0.js} +0 -0
  85. /package/dist/client/{api.d.ts → client/api.d.ts} +0 -0
  86. /package/dist/client/{locator.d.ts → client/locator.d.ts} +0 -0
package/dist/index.js CHANGED
@@ -1,15 +1,16 @@
1
- import { DEFAULT_TEST_TIMEOUT, FATAL_SIGNALS, PhaseTracker, RSTEST_ENV_SYMBOL_KEY, applyWatchInvalidation, applyWebMockRspackConfig, browserIgnoredRuntimeConfigKeys, buildBrowserCoverageMap, color, createCoverageProvider, createRunnerEventSink, createSilentConsoleController, finalizeRunCycle, getNoTestFilesMessage, getNumCpus as browser_getNumCpus, getPrettyConsoleName, getSetupFiles, getTestEntries, hasUserRstestConfigPlugins, importMetaRstestDefine, initModifyRstestConfigHooks, isDebug, isTTY, loadCoverageProvider, logWatchReadyMessage, logger, pluginMockRuntime, prepareWatchRerunState, projectRuntimeConfig, resolveProjectBuildCache, resolveShardedEntries, resolveSnapshotPathDefault, resolveWorkerCount, rsbuild, serializableConfig } from "@rstest/core/internal/browser";
2
- import { existsSync } from "node:fs";
1
+ import { DEFAULT_TEST_TIMEOUT, FATAL_SIGNALS, FIXTURE_CLEANUP_TIMEOUT_MS, PhaseTracker, RSTEST_ENV_SYMBOL_KEY, applyWatchInvalidation, applyWebMockRspackConfig, browserIgnoredRuntimeConfigKeys, buildBrowserCoverageMap, color, createCoverageProvider, createFileCleanupTimeoutResult, createRunnerEventSink, createSilentConsoleController, getNumCpus as browser_getNumCpus, getPrettyConsoleName, getSetupFiles, getTestEntries, hasUserRstestConfigPlugins, initModifyRstestConfigHooks, isDebug, isTTY, loadCoverageProvider, logWatchReadyMessage, logger, pluginMockRuntime, projectRuntimeConfig, resolveProjectBuildCache, resolveShardedEntries, resolveSnapshotPathDefault, resolveWorkerCount, rsbuild, serializableConfig } from "@rstest/core/internal/browser";
3
2
  import promises from "node:fs/promises";
3
+ import { dirname, isAbsolute as external_pathe_isAbsolute, join, normalize, relative, resolve as external_pathe_resolve } from "pathe";
4
+ import { existsSync } from "node:fs";
4
5
  import { fileURLToPath } from "node:url";
5
6
  import { isDeepStrictEqual } from "node:util";
6
7
  import open_editor from "open-editor";
7
- import { dirname, join, normalize, relative, resolve as external_pathe_resolve } from "pathe";
8
8
  import sirv from "sirv";
9
9
  import { WebSocketServer } from "ws";
10
+ import { AnyMap, TraceMap, encodedMap } from "@jridgewell/trace-mapping";
10
11
  import convert_source_map from "convert-source-map";
11
- import { __webpack_require__ } from "./rslib-runtime.js";
12
- import { DISPATCH_RPC_BRIDGE_NAME, DISPATCH_NAMESPACE_SNAPSHOT, DISPATCH_MESSAGE_TYPE, DISPATCH_NAMESPACE_BROWSER } from "./33.js";
12
+ import { randomUUID } from "node:crypto";
13
+ import { __webpack_require__ } from "./612~0.js";
13
14
  __webpack_require__.add({
14
15
  "../../node_modules/.pnpm/picomatch@4.0.5/node_modules/picomatch/index.js" (module, __unused_rspack_exports, __webpack_require__) {
15
16
  const pico = __webpack_require__("../../node_modules/.pnpm/picomatch@4.0.5/node_modules/picomatch/lib/picomatch.js");
@@ -1654,218 +1655,6 @@ __webpack_require__.add({
1654
1655
  };
1655
1656
  }
1656
1657
  });
1657
- const TYPE_REQUEST = "q";
1658
- const TYPE_RESPONSE = "s";
1659
- function createPromiseWithResolvers() {
1660
- let resolve;
1661
- let reject;
1662
- return {
1663
- promise: new Promise((res, rej)=>{
1664
- resolve = res;
1665
- reject = rej;
1666
- }),
1667
- resolve,
1668
- reject
1669
- };
1670
- }
1671
- const random = Math.random.bind(Math);
1672
- const urlAlphabet = "useandom-26T198340PX75pxJACKVERYMINDBUSHWOLF_GQZbfghjklqvwyzrict";
1673
- function nanoid(size = 21) {
1674
- let id = "";
1675
- let i = size;
1676
- while(i--)id += urlAlphabet[64 * random() | 0];
1677
- return id;
1678
- }
1679
- const DEFAULT_TIMEOUT = 6e4;
1680
- const defaultSerialize = (i)=>i;
1681
- const defaultDeserialize = defaultSerialize;
1682
- const { clearTimeout: dist_clearTimeout, setTimeout: dist_setTimeout } = globalThis;
1683
- function createBirpc($functions, options) {
1684
- const { post, on, off = ()=>{}, eventNames = [], serialize = defaultSerialize, deserialize = defaultDeserialize, resolver, bind = "rpc", timeout = DEFAULT_TIMEOUT, proxify = true } = options;
1685
- let $closed = false;
1686
- const _rpcPromiseMap = /* @__PURE__ */ new Map();
1687
- let _promiseInit;
1688
- let rpc;
1689
- async function _call(method, args, event, optional) {
1690
- if ($closed) throw new Error(`[birpc] rpc is closed, cannot call "${method}"`);
1691
- const req = {
1692
- m: method,
1693
- a: args,
1694
- t: TYPE_REQUEST
1695
- };
1696
- if (optional) req.o = true;
1697
- const send = async (_req)=>post(serialize(_req));
1698
- if (event) return void await send(req);
1699
- if (_promiseInit) try {
1700
- await _promiseInit;
1701
- } finally{
1702
- _promiseInit = void 0;
1703
- }
1704
- let { promise, resolve, reject } = createPromiseWithResolvers();
1705
- const id = nanoid();
1706
- req.i = id;
1707
- let timeoutId;
1708
- async function handler(newReq = req) {
1709
- if (timeout >= 0) {
1710
- timeoutId = dist_setTimeout(()=>{
1711
- try {
1712
- if (options.onTimeoutError?.call(rpc, method, args) !== true) throw new Error(`[birpc] timeout on calling "${method}"`);
1713
- } catch (e) {
1714
- reject(e);
1715
- }
1716
- _rpcPromiseMap.delete(id);
1717
- }, timeout);
1718
- if ("object" == typeof timeoutId) timeoutId = timeoutId.unref?.();
1719
- }
1720
- _rpcPromiseMap.set(id, {
1721
- resolve,
1722
- reject,
1723
- timeoutId,
1724
- method
1725
- });
1726
- await send(newReq);
1727
- return promise;
1728
- }
1729
- try {
1730
- if (options.onRequest) await options.onRequest.call(rpc, req, handler, resolve);
1731
- else await handler();
1732
- } catch (e) {
1733
- if (options.onGeneralError?.call(rpc, e) !== true) throw e;
1734
- return;
1735
- } finally{
1736
- dist_clearTimeout(timeoutId);
1737
- _rpcPromiseMap.delete(id);
1738
- }
1739
- return promise;
1740
- }
1741
- const builtinMethods = {
1742
- $call: (method, ...args)=>_call(method, args, false),
1743
- $callOptional: (method, ...args)=>_call(method, args, false, true),
1744
- $callEvent: (method, ...args)=>_call(method, args, true),
1745
- $callRaw: (options$1)=>_call(options$1.method, options$1.args, options$1.event, options$1.optional),
1746
- $rejectPendingCalls,
1747
- get $closed () {
1748
- return $closed;
1749
- },
1750
- get $meta () {
1751
- return options.meta;
1752
- },
1753
- $close,
1754
- $functions
1755
- };
1756
- rpc = proxify ? new Proxy({}, {
1757
- get (_, method) {
1758
- if (Object.prototype.hasOwnProperty.call(builtinMethods, method)) return builtinMethods[method];
1759
- if ("then" === method && !eventNames.includes("then") && !("then" in $functions)) return;
1760
- const sendEvent = (...args)=>_call(method, args, true);
1761
- if (eventNames.includes(method)) {
1762
- sendEvent.asEvent = sendEvent;
1763
- return sendEvent;
1764
- }
1765
- const sendCall = (...args)=>_call(method, args, false);
1766
- sendCall.asEvent = sendEvent;
1767
- return sendCall;
1768
- }
1769
- }) : builtinMethods;
1770
- function $close(customError) {
1771
- $closed = true;
1772
- _rpcPromiseMap.forEach(({ reject, method })=>{
1773
- const error = /* @__PURE__ */ new Error(`[birpc] rpc is closed, cannot call "${method}"`);
1774
- if (customError) {
1775
- customError.cause ??= error;
1776
- return reject(customError);
1777
- }
1778
- reject(error);
1779
- });
1780
- _rpcPromiseMap.clear();
1781
- off(onMessage);
1782
- }
1783
- function $rejectPendingCalls(handler) {
1784
- const handlerResults = Array.from(_rpcPromiseMap.values()).map(({ method, reject })=>{
1785
- if (!handler) return reject(/* @__PURE__ */ new Error(`[birpc]: rejected pending call "${method}".`));
1786
- return handler({
1787
- method,
1788
- reject
1789
- });
1790
- });
1791
- _rpcPromiseMap.clear();
1792
- return handlerResults;
1793
- }
1794
- async function onMessage(data, ...extra) {
1795
- let msg;
1796
- try {
1797
- msg = deserialize(data);
1798
- } catch (e) {
1799
- if (options.onGeneralError?.call(rpc, e) !== true) throw e;
1800
- return;
1801
- }
1802
- if (msg.t === TYPE_REQUEST) {
1803
- const { m: method, a: args, o: optional } = msg;
1804
- let result, error;
1805
- let fn = await (resolver ? resolver.call(rpc, method, $functions[method]) : $functions[method]);
1806
- if (optional) fn ||= ()=>void 0;
1807
- if (fn) try {
1808
- result = await fn.apply("rpc" === bind ? rpc : $functions, args);
1809
- } catch (e) {
1810
- error = e;
1811
- }
1812
- else error = /* @__PURE__ */ new Error(`[birpc] function "${method}" not found`);
1813
- if (msg.i) {
1814
- if (error && options.onFunctionError) {
1815
- if (true === options.onFunctionError.call(rpc, error, method, args)) return;
1816
- }
1817
- if (!error) try {
1818
- await post(serialize({
1819
- t: TYPE_RESPONSE,
1820
- i: msg.i,
1821
- r: result
1822
- }), ...extra);
1823
- return;
1824
- } catch (e) {
1825
- error = e;
1826
- if (options.onGeneralError?.call(rpc, e, method, args) !== true) throw e;
1827
- }
1828
- try {
1829
- await post(serialize({
1830
- t: TYPE_RESPONSE,
1831
- i: msg.i,
1832
- e: error
1833
- }), ...extra);
1834
- } catch (e) {
1835
- if (options.onGeneralError?.call(rpc, e, method, args) !== true) throw e;
1836
- }
1837
- }
1838
- } else {
1839
- const { i: ack, r: result, e: error } = msg;
1840
- const promise = _rpcPromiseMap.get(ack);
1841
- if (promise) {
1842
- dist_clearTimeout(promise.timeoutId);
1843
- if (error) promise.reject(error);
1844
- else promise.resolve(result);
1845
- }
1846
- _rpcPromiseMap.delete(ack);
1847
- }
1848
- }
1849
- _promiseInit = on(onMessage);
1850
- return rpc;
1851
- }
1852
- const DEFAULT_MAX_HEADLESS_WORKERS = 12;
1853
- const resolveHeadlessWorkerCount = ({ command, maxWorkers, totalTasks, numCpus = browser_getNumCpus() })=>{
1854
- const base = Math.max(Math.min(DEFAULT_MAX_HEADLESS_WORKERS, numCpus - 1), 1);
1855
- return resolveWorkerCount({
1856
- command,
1857
- maxWorkers,
1858
- totalTasks,
1859
- recommended: base,
1860
- watchRecommended: Math.max(Math.floor(base / 2), 1),
1861
- numCpus
1862
- });
1863
- };
1864
- const getHeadlessConcurrency = (context, totalTests)=>resolveHeadlessWorkerCount({
1865
- command: context.command,
1866
- maxWorkers: context.normalizedConfig.pool.maxWorkers,
1867
- totalTasks: totalTests
1868
- });
1869
1658
  const toErrorMessage = (error)=>error instanceof Error ? error.message : String(error);
1870
1659
  class HostDispatchRouter {
1871
1660
  handlers = new Map();
@@ -1914,6 +1703,12 @@ class HostDispatchRouter {
1914
1703
  }
1915
1704
  }
1916
1705
  }
1706
+ const DISPATCH_MESSAGE_TYPE = '__rstest_dispatch__';
1707
+ const DISPATCH_RPC_BRIDGE_NAME = '__rstest_dispatch_rpc__';
1708
+ const DISPATCH_NAMESPACE_BROWSER = 'browser';
1709
+ const DISPATCH_NAMESPACE_FILE_CLEANUP = 'file-cleanup';
1710
+ const DISPATCH_NAMESPACE_SNAPSHOT = 'snapshot';
1711
+ const RSTEST_BROWSER_CACHE_CLEANERS_KEY = '@rstest/browser/cache-cleaners';
1917
1712
  const snapshotRequestBuilders = {
1918
1713
  resolveSnapshotPath: (id, args)=>({
1919
1714
  id,
@@ -2054,11 +1849,6 @@ const ignoredKeyWarnings = {
2054
1849
  message: ()=>"Ignoring isolate: false in browser mode: each test file still runs in a fresh context.",
2055
1850
  browserOnly: true
2056
1851
  },
2057
- federation: {
2058
- isNonDefault: (config)=>true === config.federation,
2059
- message: ()=>'Ignoring federation in browser mode: it only applies to the Node runner.',
2060
- browserOnly: true
2061
- },
2062
1852
  detectAsyncLeaks: {
2063
1853
  isNonDefault: (config)=>true === config.detectAsyncLeaks,
2064
1854
  message: ()=>"Ignoring detectAsyncLeaks in browser mode: it relies on node async_hooks."
@@ -2109,8 +1899,9 @@ const reportUnsupportedBrowserOptions = (context)=>{
2109
1899
  const globalConfig = context.normalizedConfig;
2110
1900
  const { coverage } = globalConfig;
2111
1901
  if ('list' !== context.command && coverage.enabled && 'v8' === coverage.provider) {
2112
- if (isBrowserOnlyRun) throw new Error("Coverage provider 'v8' is not supported in browser mode: browser projects produce no v8 coverage. Use the default istanbul provider (coverage.provider: 'istanbul') for browser coverage.");
2113
- logger.warn(color.yellow("Coverage provider 'v8' produces no coverage for browser project files; use the 'istanbul' provider to collect browser coverage. Node projects will still use v8."));
1902
+ const unsupportedProject = browserProjects.find((project)=>'chromium' !== project.normalizedConfig.browser.browser);
1903
+ if (unsupportedProject) if (isBrowserOnlyRun) throw new Error(`Coverage provider 'v8' in browser mode requires Chromium. Project ${JSON.stringify(unsupportedProject.name)} uses ${JSON.stringify(unsupportedProject.normalizedConfig.browser.browser)}. Use Chromium or coverage.provider: 'istanbul'.`);
1904
+ else logger.warn(color.yellow(`Coverage provider 'v8' requires Chromium for browser project ${JSON.stringify(unsupportedProject.name)}. Its files will not produce coverage; node projects will still use v8.`));
2114
1905
  }
2115
1906
  const warnings = new Set();
2116
1907
  if ('forks' !== globalConfig.pool.type) warnings.add(`Ignoring pool.type '${globalConfig.pool.type}' in browser mode.`);
@@ -2141,104 +1932,6 @@ const validateBrowserConfig = (context)=>{
2141
1932
  }
2142
1933
  reportUnsupportedBrowserOptions(context);
2143
1934
  };
2144
- const createHeadedSerialTaskQueue = ()=>{
2145
- let queue = Promise.resolve();
2146
- const enqueue = (task)=>{
2147
- const next = queue.then(task);
2148
- queue = next.catch(()=>{});
2149
- return next;
2150
- };
2151
- return {
2152
- enqueue
2153
- };
2154
- };
2155
- const createHeadlessLatestRerunScheduler = (options)=>{
2156
- let pendingFiles = null;
2157
- let draining = null;
2158
- let latestEnqueueVersion = 0;
2159
- const runDrainLoop = async ()=>{
2160
- while(pendingFiles){
2161
- const nextFiles = pendingFiles;
2162
- pendingFiles = null;
2163
- try {
2164
- await options.runFiles(nextFiles);
2165
- } catch (error) {
2166
- try {
2167
- await options.onError?.(error);
2168
- } catch {}
2169
- }
2170
- }
2171
- };
2172
- const ensureDrainLoop = ()=>{
2173
- if (draining) return;
2174
- draining = runDrainLoop().finally(()=>{
2175
- draining = null;
2176
- });
2177
- };
2178
- return {
2179
- async enqueueLatest (files) {
2180
- const enqueueVersion = ++latestEnqueueVersion;
2181
- const activeRun = options.getActiveRun();
2182
- if (activeRun && !options.isRunCancelled(activeRun)) {
2183
- options.onInterrupt?.(activeRun);
2184
- options.invalidateActiveRun();
2185
- await options.interruptActiveRun(activeRun);
2186
- }
2187
- if (enqueueVersion !== latestEnqueueVersion) return;
2188
- pendingFiles = files;
2189
- ensureDrainLoop();
2190
- },
2191
- async whenIdle () {
2192
- await draining;
2193
- }
2194
- };
2195
- };
2196
- const attachHeadlessRunnerTransport = async (page, handlers)=>{
2197
- await page.exposeFunction(DISPATCH_MESSAGE_TYPE, handlers.onDispatchMessage);
2198
- await page.exposeFunction(DISPATCH_RPC_BRIDGE_NAME, handlers.onDispatchRpc);
2199
- };
2200
- const isRecord = (value)=>'object' == typeof value && null !== value;
2201
- const readString = (value, key, label)=>{
2202
- const result = value[key];
2203
- if ('string' != typeof result) throw new Error(`Invalid browser RPC request: ${label} must be a string`);
2204
- return result;
2205
- };
2206
- const readUnknownArray = (value, key, label)=>{
2207
- const result = value[key];
2208
- if (!Array.isArray(result)) throw new Error(`Invalid browser RPC request: ${label} must be an array`);
2209
- return result;
2210
- };
2211
- const parseBrowserLocatorIR = (value, label)=>{
2212
- if (!isRecord(value)) throw new Error(`Invalid browser RPC request: ${label} must be an object`);
2213
- const steps = value.steps;
2214
- if (!Array.isArray(steps)) throw new Error(`Invalid browser RPC request: ${label}.steps must be an array`);
2215
- return value;
2216
- };
2217
- const validateBrowserRpcRequest = (payload)=>{
2218
- if (!isRecord(payload)) throw new Error('Invalid browser RPC request: payload must be an object');
2219
- const kind = readString(payload, 'kind', 'kind');
2220
- if ('locator' !== kind && 'expect' !== kind && 'config' !== kind) throw new Error(`Invalid browser RPC request: unsupported kind ${JSON.stringify(kind)}`);
2221
- const request = {
2222
- id: readString(payload, 'id', 'id'),
2223
- testPath: readString(payload, 'testPath', 'testPath'),
2224
- runId: readString(payload, 'runId', 'runId'),
2225
- kind,
2226
- locator: parseBrowserLocatorIR(payload.locator, 'locator'),
2227
- method: readString(payload, 'method', 'method'),
2228
- args: readUnknownArray(payload, 'args', 'args')
2229
- };
2230
- const isNot = payload.isNot;
2231
- if (void 0 !== isNot) {
2232
- if ('boolean' != typeof isNot) throw new Error('Invalid browser RPC request: isNot must be a boolean');
2233
- request.isNot = isNot;
2234
- }
2235
- const timeout = payload.timeout;
2236
- if (void 0 !== timeout) {
2237
- if ('number' != typeof timeout) throw new Error('Invalid browser RPC request: timeout must be a number');
2238
- request.timeout = timeout;
2239
- }
2240
- return request;
2241
- };
2242
1935
  const supportedLocatorActions = new Set([
2243
1936
  'click',
2244
1937
  'dblclick',
@@ -2660,6 +2353,17 @@ async function launchPlaywrightBrowser({ browserName, headless, providerOptions
2660
2353
  browser: wrappedBrowser
2661
2354
  };
2662
2355
  }
2356
+ const asCoveragePage = (page)=>page;
2357
+ const createPlaywrightV8CoverageCollector = ()=>({
2358
+ async start (page) {
2359
+ await asCoveragePage(page).coverage.startJSCoverage({
2360
+ resetOnNavigation: false
2361
+ });
2362
+ },
2363
+ async take (page) {
2364
+ return asCoveragePage(page).coverage.stopJSCoverage();
2365
+ }
2366
+ });
2663
2367
  const playwrightProviderImplementation = {
2664
2368
  name: 'playwright',
2665
2369
  async launchRuntime ({ browserName, headless, providerOptions }) {
@@ -2669,6 +2373,9 @@ const playwrightProviderImplementation = {
2669
2373
  providerOptions
2670
2374
  });
2671
2375
  },
2376
+ createV8CoverageCollector ({ browserName }) {
2377
+ return 'chromium' === browserName ? createPlaywrightV8CoverageCollector() : null;
2378
+ },
2672
2379
  async dispatchRpc ({ containerPage, runnerPage, request, timeoutFallbackMs }) {
2673
2380
  return dispatchPlaywrightBrowserRpc({
2674
2381
  containerPage: containerPage,
@@ -2686,165 +2393,18 @@ function getBrowserProviderImplementation(provider) {
2686
2393
  if (!implementation) throw new Error(`Unsupported browser provider: ${String(provider)}`);
2687
2394
  return implementation;
2688
2395
  }
2689
- const createCancelSignal = ()=>{
2690
- let settled = false;
2691
- let resolveSignal = ()=>{};
2692
- const signal = new Promise((resolve)=>{
2693
- resolveSignal = ()=>{
2694
- if (!settled) {
2695
- settled = true;
2696
- resolve();
2697
- }
2698
- };
2699
- });
2700
- return {
2701
- signal,
2702
- resolve: resolveSignal
2703
- };
2704
- };
2705
- const createRunSession = (token)=>{
2706
- const { signal, resolve } = createCancelSignal();
2707
- return {
2708
- token,
2709
- cancelled: false,
2710
- cancelSignal: signal,
2711
- signalCancel: resolve
2712
- };
2713
- };
2714
- class RunSessionLifecycle {
2715
- currentToken = 0;
2716
- active = null;
2717
- get activeSession() {
2718
- return this.active;
2719
- }
2720
- get activeToken() {
2721
- return this.currentToken;
2722
- }
2723
- createSession(factory) {
2724
- const session = factory(++this.currentToken);
2725
- this.active = session;
2726
- return session;
2727
- }
2728
- isTokenActive(token) {
2729
- return token === this.currentToken;
2730
- }
2731
- isTokenStale(token) {
2732
- return !this.isTokenActive(token);
2733
- }
2734
- invalidateActiveToken() {
2735
- this.currentToken += 1;
2736
- return this.currentToken;
2737
- }
2738
- clearIfActive(session) {
2739
- if (this.active === session) this.active = null;
2740
- }
2741
- async cancel(session, options) {
2742
- const waitForDone = options?.waitForDone ?? true;
2743
- if (!session.cancelled) {
2744
- session.cancelled = true;
2745
- session.signalCancel();
2746
- await options?.onCancel?.(session);
2747
- }
2748
- if (waitForDone) await session.done?.catch(()=>{});
2749
- }
2750
- async cancelActive(options) {
2751
- if (!this.active) return;
2752
- await this.cancel(this.active, options);
2753
- }
2754
- }
2755
- class RunnerSessionRegistry {
2756
- nextId = 0;
2757
- sessionsById = new Map();
2758
- sessionIdByTestFile = new Map();
2759
- register(input) {
2760
- const id = input.id ?? `runner-session-${++this.nextId}`;
2761
- const createdAt = input.createdAt ?? Date.now();
2762
- const record = {
2763
- ...input,
2764
- id,
2765
- createdAt
2766
- };
2767
- this.sessionsById.set(id, record);
2768
- this.sessionIdByTestFile.set(record.testFile, id);
2769
- return record;
2770
- }
2771
- getById(id) {
2772
- return this.sessionsById.get(id);
2773
- }
2774
- getByTestFile(testFile) {
2775
- const id = this.sessionIdByTestFile.get(testFile);
2776
- if (!id) return;
2777
- return this.sessionsById.get(id);
2778
- }
2779
- list() {
2780
- return Array.from(this.sessionsById.values());
2781
- }
2782
- listByRunToken(runToken) {
2783
- return this.list().filter((session)=>session.runToken === runToken);
2784
- }
2785
- deleteById(id) {
2786
- const record = this.sessionsById.get(id);
2787
- if (!record) return false;
2788
- this.sessionsById.delete(id);
2789
- if (this.sessionIdByTestFile.get(record.testFile) === id) this.sessionIdByTestFile.delete(record.testFile);
2790
- return true;
2791
- }
2792
- deleteByTestFile(testFile) {
2793
- const id = this.sessionIdByTestFile.get(testFile);
2794
- if (!id) return false;
2795
- return this.deleteById(id);
2796
- }
2797
- clear() {
2798
- this.sessionsById.clear();
2799
- this.sessionIdByTestFile.clear();
2800
- }
2801
- }
2802
- const normalizeJavaScriptUrl = (value, options)=>{
2803
- try {
2804
- const url = options?.origin ? new URL(value, options.origin) : new URL(value);
2805
- if ('http:' !== url.protocol && 'https:' !== url.protocol) return null;
2806
- url.search = '';
2807
- url.hash = '';
2808
- return url.toString();
2809
- } catch {
2810
- return null;
2811
- }
2812
- };
2813
- const resolveInlineSourceMap = (code)=>{
2814
- const converter = convert_source_map.fromSource(code);
2815
- if (!converter) return null;
2816
- return converter.toObject();
2817
- };
2818
- const fetchSourceMap = async (jsUrl, fetcher)=>{
2819
- const jsResponse = await fetcher(jsUrl);
2820
- if (!jsResponse.ok) return null;
2821
- const code = await jsResponse.text();
2822
- const inlineMap = resolveInlineSourceMap(code);
2823
- if (inlineMap) return inlineMap;
2824
- const mapResponse = await fetcher(`${jsUrl}.map`);
2825
- if (!mapResponse.ok) return null;
2826
- return await mapResponse.json();
2827
- };
2828
- const loadSourceMapWithCache = async ({ jsUrl, cache, force = false, origin, fetcher = fetch })=>{
2829
- const normalizedUrl = normalizeJavaScriptUrl(jsUrl, {
2830
- origin
2831
- });
2832
- if (!normalizedUrl) return null;
2833
- if (!force && cache.has(normalizedUrl)) return cache.get(normalizedUrl) ?? null;
2834
- try {
2835
- const sourceMap = await fetchSourceMap(normalizedUrl, fetcher);
2836
- cache.set(normalizedUrl, sourceMap);
2837
- return sourceMap;
2838
- } catch {
2839
- cache.set(normalizedUrl, null);
2840
- return null;
2841
- }
2842
- };
2843
2396
  const serializeTestFiles = (files)=>JSON.stringify(files.map((f)=>`${f.projectName}:${f.testPath}`).sort());
2844
2397
  const normalizeTestFiles = (files)=>files.map((file)=>({
2845
2398
  ...file,
2846
2399
  testPath: normalize(file.testPath)
2847
2400
  }));
2401
+ const collectTestPaths = (files)=>[
2402
+ ...new Set(files.map((file)=>file.testPath))
2403
+ ];
2404
+ const collectDeletedTestPaths = (previous, current)=>{
2405
+ const currentPathSet = new Set(current.map((file)=>file.testPath));
2406
+ return previous.map((file)=>file.testPath).filter((testPath)=>!currentPathSet.has(testPath));
2407
+ };
2848
2408
  const collectWatchTestFiles = (projectEntries)=>projectEntries.flatMap((entry)=>entry.testFiles.map((testPath)=>({
2849
2409
  testPath: normalize(testPath),
2850
2410
  projectName: entry.project.name
@@ -2852,179 +2412,129 @@ const collectWatchTestFiles = (projectEntries)=>projectEntries.flatMap((entry)=>
2852
2412
  const planWatchRerun = ({ projectEntries, previousTestFiles, affectedTestFiles })=>{
2853
2413
  const currentTestFiles = collectWatchTestFiles(projectEntries);
2854
2414
  const normalizedPrevious = normalizeTestFiles(previousTestFiles);
2855
- const filesChanged = serializeTestFiles(currentTestFiles) !== serializeTestFiles(normalizedPrevious);
2856
- const normalizedAffectedTestFiles = affectedTestFiles.map((testFile)=>normalize(testFile));
2857
- const currentFileMap = new Map(currentTestFiles.map((file)=>[
2858
- file.testPath,
2859
- file
2860
- ]));
2861
- const matchedAffectedFiles = normalizedAffectedTestFiles.map((testFile)=>currentFileMap.get(testFile)).filter((file)=>Boolean(file));
2415
+ const fileSetChanged = serializeTestFiles(currentTestFiles) !== serializeTestFiles(normalizedPrevious);
2416
+ if (fileSetChanged) {
2417
+ const fileSetUpdate = {
2418
+ currentTestFiles,
2419
+ deletedTestPaths: collectDeletedTestPaths(normalizedPrevious, currentTestFiles)
2420
+ };
2421
+ if (0 === currentTestFiles.length) return {
2422
+ fileSetUpdate,
2423
+ decision: {
2424
+ kind: 'empty',
2425
+ message: 'No browser test files remain after update.\n'
2426
+ }
2427
+ };
2428
+ return {
2429
+ fileSetUpdate,
2430
+ decision: {
2431
+ kind: 'rerun',
2432
+ testPaths: collectTestPaths(currentTestFiles),
2433
+ message: `Test file set changed, re-running ${currentTestFiles.length} file(s)...\n`
2434
+ }
2435
+ };
2436
+ }
2437
+ const affectedPathSet = new Set(affectedTestFiles.map((testFile)=>normalize(testFile)));
2438
+ const matchedAffectedFiles = currentTestFiles.filter(({ testPath })=>affectedPathSet.has(testPath));
2439
+ if (0 === matchedAffectedFiles.length) return {
2440
+ decision: {
2441
+ kind: 'idle',
2442
+ message: 'No affected browser test files detected, skipping re-run.\n'
2443
+ }
2444
+ };
2862
2445
  return {
2863
- currentTestFiles,
2864
- filesChanged,
2865
- normalizedAffectedTestFiles,
2866
- affectedTestFiles: matchedAffectedFiles
2446
+ decision: {
2447
+ kind: 'rerun',
2448
+ testPaths: collectTestPaths(matchedAffectedFiles),
2449
+ message: `Re-running ${matchedAffectedFiles.length} affected test file(s)...\n`
2450
+ }
2867
2451
  };
2868
2452
  };
2453
+ const commitWatchFileSetUpdate = (update, watchState, pruneDeletedTestPaths)=>{
2454
+ if (!update) return;
2455
+ if (update.deletedTestPaths.length > 0) pruneDeletedTestPaths(update.deletedTestPaths);
2456
+ watchState.lastTestFiles = update.currentTestFiles;
2457
+ };
2869
2458
  const picomatch = __webpack_require__("../../node_modules/.pnpm/picomatch@4.0.5/node_modules/picomatch/index.js");
2870
2459
  var picomatch_default = /*#__PURE__*/ __webpack_require__.n(picomatch);
2871
2460
  const { createRsbuild: createRsbuild, rspack: rspack } = rsbuild;
2872
- const hostController_dirname = dirname(fileURLToPath(import.meta.url));
2461
+ const browserRsbuild_dirname = dirname(fileURLToPath(import.meta.url));
2873
2462
  const OPTIONS_PLACEHOLDER = '__RSTEST_OPTIONS_PLACEHOLDER__';
2874
- let nextBrowserFilePid = 1000000000;
2875
2463
  const serializeForInlineScript = (value)=>JSON.stringify(value).replace(/</g, '\\u003c').replace(/\u2028/g, '\\u2028').replace(/\u2029/g, '\\u2029');
2464
+ class RstestBrowserRuntimePlugin {
2465
+ apply(compiler) {
2466
+ const { RuntimeModule } = compiler.webpack;
2467
+ class BrowserRuntimeModule extends RuntimeModule {
2468
+ constructor(){
2469
+ super('rstest_browser_runtime');
2470
+ }
2471
+ generate() {
2472
+ return `
2473
+ var __rstest_browser_test_entry_ids__ = Object.create(null);
2474
+
2475
+ __webpack_require__.rstest_register_browser_test_entry = (path, id) => {
2476
+ __rstest_browser_test_entry_ids__[path] = id;
2477
+ };
2478
+
2479
+ function __rstest_clean_browser_test_entry__(testEntryPath) {
2480
+ var testEntryId = __rstest_browser_test_entry_ids__[testEntryPath];
2481
+ if (testEntryId !== undefined) {
2482
+ delete __webpack_module_cache__[testEntryId];
2483
+ }
2484
+ }
2485
+
2486
+ (globalThis[${JSON.stringify(RSTEST_BROWSER_CACHE_CLEANERS_KEY)}] ??= new Set()).add(__rstest_clean_browser_test_entry__);
2487
+ `;
2488
+ }
2489
+ }
2490
+ compiler.hooks.thisCompilation.tap('RstestBrowserRuntimePlugin', (compilation)=>{
2491
+ compilation.hooks.additionalTreeRuntimeRequirements.tap('RstestAddBrowserRuntimePlugin', (chunk)=>{
2492
+ compilation.addRuntimeModule(chunk, new BrowserRuntimeModule());
2493
+ });
2494
+ });
2495
+ }
2496
+ }
2876
2497
  const getBrowserProviderOptions = (project)=>{
2877
2498
  const browserConfig = project.normalizedConfig.browser;
2878
2499
  return browserConfig.providerOptions ?? {};
2879
2500
  };
2880
- const getFileTaskId = (testPath)=>`file:${testPath}`;
2881
- const createDeferredPromise = ()=>{
2882
- let resolve;
2883
- let reject;
2884
- const promise = new Promise((res, rej)=>{
2885
- resolve = res;
2886
- reject = rej;
2501
+ const createBrowserWatchState = ()=>({
2502
+ lastTestFiles: [],
2503
+ hooksEnabled: false,
2504
+ headedFileSetVersion: 1,
2505
+ invalidation: new Map(),
2506
+ pendingAffectedTestFiles: new Map(),
2507
+ compileStartTimes: new Map(),
2508
+ pendingBuildTimeMs: 0
2887
2509
  });
2888
- return {
2889
- promise,
2890
- resolve,
2891
- reject
2510
+ const drainPendingBuildTime = (watchState)=>{
2511
+ const buildTime = watchState.pendingBuildTimeMs;
2512
+ watchState.pendingBuildTimeMs = 0;
2513
+ return buildTime;
2514
+ };
2515
+ const drainPendingAffectedTestFiles = (watchState)=>{
2516
+ const affected = new Set();
2517
+ for (const files of watchState.pendingAffectedTestFiles.values())for (const file of files)affected.add(file);
2518
+ watchState.pendingAffectedTestFiles.clear();
2519
+ return Array.from(affected);
2520
+ };
2521
+ const resolveViewport = (viewport)=>{
2522
+ if (!viewport) return null;
2523
+ if ('string' == typeof viewport) return resolveBrowserViewportPreset(viewport);
2524
+ if ('number' == typeof viewport.width && Number.isFinite(viewport.width) && viewport.width > 0 && 'number' == typeof viewport.height && Number.isFinite(viewport.height) && viewport.height > 0) return {
2525
+ width: viewport.width,
2526
+ height: viewport.height
2892
2527
  };
2528
+ return null;
2893
2529
  };
2894
- class ContainerRpcManager {
2895
- wss;
2896
- ws = null;
2897
- rpc = null;
2898
- methods;
2899
- onDisconnect;
2900
- detachActiveSocketListeners = null;
2901
- constructor(wss, methods, onDisconnect){
2902
- this.wss = wss;
2903
- this.methods = methods;
2904
- this.onDisconnect = onDisconnect;
2905
- this.setupConnectionHandler();
2906
- }
2907
- updateMethods(methods, onDisconnect) {
2908
- this.methods = methods;
2909
- this.onDisconnect = onDisconnect;
2910
- if (this.ws && this.ws.readyState === this.ws.OPEN) this.attachWebSocket(this.ws);
2911
- }
2912
- setupConnectionHandler() {
2913
- this.wss.on('connection', (ws)=>{
2914
- logger.debug('[Browser UI] Container WebSocket connected');
2915
- logger.debug(`[Browser UI] Current ws: ${this.ws ? 'exists' : 'null'}, new ws: ${ws ? 'exists' : 'null'}`);
2916
- this.attachWebSocket(ws);
2917
- });
2918
- }
2919
- attachWebSocket(ws) {
2920
- this.detachActiveSocketListeners?.();
2921
- if (this.rpc && !this.rpc.$closed) this.rpc.$close(new Error('Container RPC transport reattached'));
2922
- this.ws = ws;
2923
- const messageHandlers = new WeakMap();
2924
- this.rpc = createBirpc(this.methods, {
2925
- timeout: -1,
2926
- post: (data)=>{
2927
- if (ws.readyState === ws.OPEN) ws.send(JSON.stringify(data));
2928
- },
2929
- on: (fn)=>{
2930
- const handler = (message)=>{
2931
- try {
2932
- const data = JSON.parse(message.toString());
2933
- fn(data);
2934
- } catch {}
2935
- };
2936
- messageHandlers.set(fn, handler);
2937
- ws.on('message', handler);
2938
- },
2939
- off: (fn)=>{
2940
- const handler = messageHandlers.get(fn);
2941
- if (!handler) return;
2942
- ws.off('message', handler);
2943
- messageHandlers.delete(fn);
2944
- }
2945
- });
2946
- const handleClose = ()=>{
2947
- if (this.ws === ws) this.ws = null;
2948
- this.detachActiveSocketListeners?.();
2949
- this.detachActiveSocketListeners = null;
2950
- if (this.rpc && !this.rpc.$closed) {
2951
- const disconnectError = new Error('Browser UI WebSocket disconnected before reload completed');
2952
- this.rpc.$close(disconnectError);
2953
- this.onDisconnect?.(disconnectError);
2954
- }
2955
- this.rpc = null;
2956
- };
2957
- ws.on('close', handleClose);
2958
- this.detachActiveSocketListeners = ()=>{
2959
- ws.off('close', handleClose);
2960
- };
2961
- }
2962
- get isConnected() {
2963
- return null !== this.ws && this.ws.readyState === this.ws.OPEN;
2964
- }
2965
- get currentWebSocket() {
2966
- return this.ws;
2967
- }
2968
- reattach(ws) {
2969
- this.attachWebSocket(ws);
2970
- }
2971
- async notifyTestFileUpdate(files) {
2972
- await this.rpc?.onTestFileUpdate(files);
2973
- }
2974
- async updateHostConfig(config) {
2975
- await this.rpc?.onHostConfigUpdate(config);
2976
- }
2977
- async reloadTestFile(testFile, testNamePattern) {
2978
- logger.debug(`[Browser UI] reloadTestFile called, rpc: ${this.rpc ? 'exists' : 'null'}, ws: ${this.ws ? 'exists' : 'null'}`);
2979
- if (!this.rpc) throw new Error('Browser UI RPC not available for reloadTestFile');
2980
- logger.debug(`[Browser UI] Calling reloadTestFile: ${testFile}`);
2981
- return this.rpc.reloadTestFile(testFile, testNamePattern);
2982
- }
2983
- }
2984
- const createBrowserWatchState = ()=>({
2985
- lastTestFiles: [],
2986
- hooksEnabled: false,
2987
- invalidation: new Map(),
2988
- pendingAffectedTestFiles: new Map(),
2989
- compileStartTimes: new Map(),
2990
- pendingBuildTimeMs: 0
2991
- });
2992
- const drainPendingBuildTime = (watchState)=>{
2993
- const buildTime = watchState.pendingBuildTimeMs;
2994
- watchState.pendingBuildTimeMs = 0;
2995
- return buildTime;
2996
- };
2997
- const drainPendingAffectedTestFiles = (watchState)=>{
2998
- const affected = new Set();
2999
- for (const files of watchState.pendingAffectedTestFiles.values())for (const file of files)affected.add(file);
3000
- watchState.pendingAffectedTestFiles.clear();
3001
- return Array.from(affected);
3002
- };
3003
- const watchContext = {
3004
- runtime: null,
3005
- cleanupRegistered: false,
3006
- cleanupPromise: null
3007
- };
3008
- const resolveViewport = (viewport)=>{
3009
- if (!viewport) return null;
3010
- if ('string' == typeof viewport) return resolveBrowserViewportPreset(viewport);
3011
- if ('number' == typeof viewport.width && Number.isFinite(viewport.width) && viewport.width > 0 && 'number' == typeof viewport.height && Number.isFinite(viewport.height) && viewport.height > 0) return {
3012
- width: viewport.width,
3013
- height: viewport.height
3014
- };
3015
- return null;
3016
- };
3017
- const mapViewportByProject = (projects)=>{
3018
- const map = new Map();
3019
- for (const project of projects){
3020
- const viewport = resolveViewport(project.viewport);
3021
- if (viewport) map.set(project.name, viewport);
2530
+ const mapViewportByProject = (projects)=>{
2531
+ const map = new Map();
2532
+ for (const project of projects){
2533
+ const viewport = resolveViewport(project.viewport);
2534
+ if (viewport) map.set(project.name, viewport);
3022
2535
  }
3023
2536
  return map;
3024
2537
  };
3025
- const ensureProcessExitCode = (code)=>{
3026
- if (void 0 === process.exitCode || 0 === process.exitCode) process.exitCode = code;
3027
- };
3028
2538
  const castArray = (arr)=>{
3029
2539
  if (void 0 === arr) return [];
3030
2540
  return Array.isArray(arr) ? arr : [
@@ -3228,7 +2738,6 @@ const getBrowserRsbuildEnvironmentConfig = (project)=>({
3228
2738
  plugins: project.normalizedConfig.plugins,
3229
2739
  root: project.rootPath
3230
2740
  });
3231
- const getMaxTestTimeoutForRpc = (projects)=>Math.max(...projects.map((p)=>p.normalizedConfig.testTimeout ?? DEFAULT_TEST_TIMEOUT));
3232
2741
  const getBrowserLaunchOptions = (project)=>({
3233
2742
  provider: project.normalizedConfig.browser.provider,
3234
2743
  browser: project.normalizedConfig.browser.browser,
@@ -3247,15 +2756,7 @@ const ensureConsistentBrowserLaunchOptions = (projects)=>{
3247
2756
  }
3248
2757
  return firstOptions;
3249
2758
  };
3250
- const resolveProviderForTestPath = ({ testPath, browserProjects })=>{
3251
- const normalizedTestPath = normalize(testPath);
3252
- const sortedProjects = [
3253
- ...browserProjects
3254
- ].sort((a, b)=>b.rootPath.length - a.rootPath.length);
3255
- for (const project of sortedProjects)if (normalizedTestPath.startsWith(project.rootPath)) return project.provider;
3256
- throw new Error(`Cannot resolve browser provider for test path: ${JSON.stringify(testPath)}. Known project roots: ${JSON.stringify(sortedProjects.map((p)=>p.rootPath))}`);
3257
- };
3258
- const collectProjectEntries = async (context, browserProjects = getBrowserProjects(context))=>Promise.all(browserProjects.map(async (project)=>{
2759
+ const browserRsbuild_collectProjectEntries = async (context, browserProjects = getBrowserProjects(context))=>Promise.all(browserProjects.map(async (project)=>{
3259
2760
  const { normalizedConfig: { include, exclude, includeSource, setupFiles } } = project;
3260
2761
  const tests = await getTestEntries({
3261
2762
  include,
@@ -3273,16 +2774,16 @@ const collectProjectEntries = async (context, browserProjects = getBrowserProjec
3273
2774
  testFiles: Object.values(tests)
3274
2775
  };
3275
2776
  }));
3276
- const resolveBrowserFile = (relativePath)=>{
2777
+ const resolveBrowserDistFile = (relativePath)=>{
3277
2778
  const candidates = [
3278
- external_pathe_resolve(hostController_dirname, '../src', relativePath),
3279
- external_pathe_resolve(hostController_dirname, relativePath)
2779
+ external_pathe_resolve(browserRsbuild_dirname, relativePath),
2780
+ external_pathe_resolve(browserRsbuild_dirname, '../dist', relativePath)
3280
2781
  ];
3281
2782
  for (const candidate of candidates)if (existsSync(candidate)) return candidate;
3282
- throw new Error(`Unable to resolve browser client file: ${relativePath}`);
2783
+ throw new Error(`Unable to resolve browser dist file: ${relativePath}`);
3283
2784
  };
3284
2785
  const resolveContainerDist = ()=>{
3285
- const distPath = external_pathe_resolve(hostController_dirname, 'browser-container');
2786
+ const distPath = external_pathe_resolve(browserRsbuild_dirname, 'browser-container');
3286
2787
  if (existsSync(distPath)) return distPath;
3287
2788
  throw new Error(`Browser container build not found at ${distPath}. Please run "pnpm --filter @rstest/browser build".`);
3288
2789
  };
@@ -3424,27 +2925,7 @@ const destroyBrowserRuntime = async (runtime)=>{
3424
2925
  force: true
3425
2926
  }).catch(()=>{});
3426
2927
  };
3427
- const cleanupWatchRuntime = ()=>{
3428
- if (watchContext.cleanupPromise) return watchContext.cleanupPromise;
3429
- watchContext.cleanupPromise = (async ()=>{
3430
- if (!watchContext.runtime) return;
3431
- await destroyBrowserRuntime(watchContext.runtime);
3432
- watchContext.runtime = null;
3433
- })();
3434
- return watchContext.cleanupPromise;
3435
- };
3436
- const registerWatchCleanup = (embedded)=>{
3437
- if (watchContext.cleanupRegistered) return;
3438
- watchContext.cleanupRegistered = true;
3439
- if (embedded) return;
3440
- for (const signal of FATAL_SIGNALS)process.once(signal, ()=>{
3441
- cleanupWatchRuntime();
3442
- });
3443
- process.once('exit', ()=>{
3444
- cleanupWatchRuntime();
3445
- });
3446
- };
3447
- const createBrowserRuntime = async ({ context, projectEntries: initialProjectEntries, browserProjects, shardedEntries, freezeShardedEntries, tempDir, isWatchMode, onTriggerRerun, containerDistPath, containerDevServer, forceHeadless, skipProviderLaunch, appliedModifyRstestConfigEnvironments })=>{
2928
+ const createBrowserRuntime = async ({ context, projectEntries: initialProjectEntries, browserProjects, shardedEntries, freezeShardedEntries, tempDir, isWatchMode, containerDistPath, containerDevServer, forceHeadless, skipProviderLaunch, appliedModifyRstestConfigEnvironments })=>{
3448
2929
  const containerHtmlTemplate = containerDistPath ? await promises.readFile(join(containerDistPath, 'index.html'), 'utf-8') : null;
3449
2930
  let injectedContainerHtml = null;
3450
2931
  let serializedOptions = 'null';
@@ -3508,7 +2989,7 @@ const createBrowserRuntime = async ({ context, projectEntries: initialProjectEnt
3508
2989
  };
3509
2990
  const browserRuntimePath = fileURLToPath(import.meta.resolve('@rstest/core/internal/browser-runtime'));
3510
2991
  const staticRstestAliases = {
3511
- '@rstest/browser': resolveBrowserFile('browser.ts'),
2992
+ '@rstest/browser': resolveBrowserDistFile('client/index.js'),
3512
2993
  '@rstest/core/internal/browser-runtime': browserRuntimePath
3513
2994
  };
3514
2995
  const rstestEnvDefine = `globalThis[Symbol.for(${JSON.stringify(RSTEST_ENV_SYMBOL_KEY)})]`;
@@ -3596,7 +3077,7 @@ const createBrowserRuntime = async ({ context, projectEntries: initialProjectEnt
3596
3077
  modules: virtualManifestModules
3597
3078
  });
3598
3079
  const rstestInternalAliases = {
3599
- '@rstest/browser-manifest': manifestPath,
3080
+ __rstest_virtual_browser_manifest__: manifestPath,
3600
3081
  ...staticRstestAliases
3601
3082
  };
3602
3083
  const isHeadless = forceHeadless || project.normalizedConfig.browser.headless;
@@ -3634,6 +3115,17 @@ const createBrowserRuntime = async ({ context, projectEntries: initialProjectEnt
3634
3115
  {
3635
3116
  name: 'rstest:browser-user-config',
3636
3117
  setup (api) {
3118
+ if ('list' === context.command) {
3119
+ let testEntryPaths;
3120
+ const getTestEntryPaths = ()=>testEntryPaths ??= new Set(getProjectEntry(project)?.testFiles.map(normalize) ?? []);
3121
+ api.transform({
3122
+ test: (resourcePath)=>getTestEntryPaths().has(normalize(resourcePath))
3123
+ }, ({ code, resourcePath })=>({
3124
+ code: `${code}\nif (__webpack_require__.rstest_register_browser_test_entry && __webpack_module__.id != null) {
3125
+ __webpack_require__.rstest_register_browser_test_entry(${JSON.stringify(normalize(resourcePath))}, __webpack_module__.id);
3126
+ }`
3127
+ }));
3128
+ }
3637
3129
  api.expose?.('rstest:browser', {
3638
3130
  registerDispatchHandler: (namespace, handler)=>{
3639
3131
  dispatchHandlers.set(namespace, handler);
@@ -3661,8 +3153,7 @@ const createBrowserRuntime = async ({ context, projectEntries: initialProjectEnt
3661
3153
  source: {
3662
3154
  define: {
3663
3155
  'process.env': rstestEnvDefine,
3664
- 'import.meta.env': rstestEnvDefine,
3665
- 'import.meta.rstest': importMetaRstestDefine('web')
3156
+ 'import.meta.env': rstestEnvDefine
3666
3157
  }
3667
3158
  },
3668
3159
  output: {
@@ -3690,6 +3181,7 @@ const createBrowserRuntime = async ({ context, projectEntries: initialProjectEnt
3690
3181
  });
3691
3182
  rspackConfig.lazyCompilation = enableHmr ? createBrowserLazyCompilationConfig(setupFiles) : false;
3692
3183
  rspackConfig.plugins = rspackConfig.plugins || [];
3184
+ rspackConfig.plugins.push(new RstestBrowserRuntimePlugin());
3693
3185
  rspackConfig.plugins.push(virtualManifestPlugin);
3694
3186
  applyDefaultWatchOptions(rspackConfig, isWatchMode);
3695
3187
  const browserRuntimeDir = dirname(browserRuntimePath);
@@ -3706,7 +3198,7 @@ const createBrowserRuntime = async ({ context, projectEntries: initialProjectEnt
3706
3198
  });
3707
3199
  merged.source = merged.source || {};
3708
3200
  merged.source.entry = {
3709
- runner: resolveBrowserFile('client/entry.ts')
3201
+ runner: resolveBrowserDistFile('client/runner.js')
3710
3202
  };
3711
3203
  return merged;
3712
3204
  },
@@ -3715,7 +3207,7 @@ const createBrowserRuntime = async ({ context, projectEntries: initialProjectEnt
3715
3207
  }
3716
3208
  }
3717
3209
  ]);
3718
- if (isWatchMode && onTriggerRerun) rsbuildInstance.addPlugins([
3210
+ if (isWatchMode) rsbuildInstance.addPlugins([
3719
3211
  {
3720
3212
  name: 'rstest:browser-watch',
3721
3213
  setup (api) {
@@ -3731,7 +3223,7 @@ const createBrowserRuntime = async ({ context, projectEntries: initialProjectEnt
3731
3223
  if (watchState.hooksEnabled) watchState.pendingBuildTimeMs = Math.max(watchState.pendingBuildTimeMs, Date.now() - compileStart);
3732
3224
  }
3733
3225
  if (stats) {
3734
- const [projectEntry] = await collectProjectEntries(context, [
3226
+ const [projectEntry] = await browserRsbuild_collectProjectEntries(context, [
3735
3227
  project
3736
3228
  ]);
3737
3229
  const entryTestFiles = new Set(collectWatchTestFiles(projectEntry ? [
@@ -3760,7 +3252,7 @@ const createBrowserRuntime = async ({ context, projectEntries: initialProjectEnt
3760
3252
  }
3761
3253
  }
3762
3254
  if (!watchState.hooksEnabled) return;
3763
- await onTriggerRerun();
3255
+ await watchState.triggerRerun?.();
3764
3256
  });
3765
3257
  }
3766
3258
  }
@@ -3909,166 +3401,1661 @@ async function resolveProjectEntries(context, shardedEntries, browserProjects) {
3909
3401
  }
3910
3402
  return projectEntries;
3911
3403
  }
3912
- return collectProjectEntries(context, browserProjects);
3404
+ return browserRsbuild_collectProjectEntries(context, browserProjects);
3913
3405
  }
3914
- const runBrowserController = async (context, options)=>{
3915
- const { allowEmptyRun = false, filesOnly = false, onTraceEvents, env } = options ?? {};
3916
- const buildStart = Date.now();
3917
- const isWatchMode = 'watch' === context.command;
3918
- const phaseTrackers = onTraceEvents ? new Map() : void 0;
3919
- const trackerKey = (project, testPath)=>`${project}\u0000${testPath}`;
3920
- const browserProjects = options?.projects ?? getBrowserProjects(context);
3921
- const useHeadlessDirect = browserProjects.every((project)=>project.normalizedConfig.browser.headless);
3922
- const browserSourceMapCache = new Map();
3923
- const isHttpLikeFile = (file)=>/^https?:\/\//.test(file);
3924
- const resolveBrowserSourcemap = async (sourcePath)=>{
3925
- if (!isHttpLikeFile(sourcePath)) return {
3926
- handled: false,
3927
- sourcemap: null
3928
- };
3929
- const normalizedUrl = normalizeJavaScriptUrl(sourcePath);
3930
- if (!normalizedUrl) return {
3931
- handled: true,
3932
- sourcemap: null
3933
- };
3934
- if (browserSourceMapCache.has(normalizedUrl)) return {
3935
- handled: true,
3936
- sourcemap: browserSourceMapCache.get(normalizedUrl) ?? null
3937
- };
3938
- return {
3939
- handled: true,
3940
- sourcemap: await loadSourceMapWithCache({
3941
- jsUrl: normalizedUrl,
3942
- cache: browserSourceMapCache
3943
- })
3944
- };
3945
- };
3946
- const getBrowserSourcemap = async (sourcePath)=>{
3947
- const result = await resolveBrowserSourcemap(sourcePath);
3948
- return result.handled ? result.sourcemap : null;
3949
- };
3950
- const buildErrorResult = async (error, close)=>{
3951
- const elapsed = Math.max(0, Date.now() - buildStart);
3952
- const errorResult = {
3953
- results: [],
3954
- testResults: [],
3955
- duration: {
3956
- totalTime: elapsed,
3957
- buildTime: elapsed,
3958
- testTime: 0
3959
- },
3960
- hasFailure: true,
3961
- unhandledErrors: [
3962
- error
3963
- ],
3964
- getSourcemap: getBrowserSourcemap,
3965
- resolveSourcemap: resolveBrowserSourcemap,
3966
- close
3967
- };
3968
- if (isWatchMode) for (const reporter of context.reporters)await reporter.onTestRunEnd?.({
3969
- results: [],
3970
- testResults: [],
3971
- duration: errorResult.duration,
3972
- snapshotSummary: context.snapshotManager.summary,
3973
- getSourcemap: getBrowserSourcemap,
3974
- unhandledErrors: errorResult.unhandledErrors
3975
- });
3976
- return errorResult;
3977
- };
3978
- const toError = (error)=>error instanceof Error ? error : new Error(String(error));
3979
- const failWithError = async (error, cleanup)=>{
3980
- if (isWatchMode) ensureProcessExitCode(1);
3981
- const normalizedError = toError(error);
3982
- if (cleanup && !isWatchMode) return buildErrorResult(normalizedError, cleanup);
3983
- try {
3984
- return await buildErrorResult(normalizedError);
3985
- } finally{
3986
- await cleanup?.();
3987
- }
3988
- };
3989
- const collectDeletedTestPaths = (previous, current)=>{
3990
- const currentPathSet = new Set(current.map((file)=>file.testPath));
3991
- return previous.map((file)=>file.testPath).filter((testPath)=>!currentPathSet.has(testPath));
3992
- };
3993
- const notifyTestRunStart = async ()=>{
3994
- if (!isWatchMode) return;
3995
- for (const reporter of context.reporters)await reporter.onTestRunStart?.();
3996
- };
3997
- const coverageConfig = browserProjects.find((project)=>project.normalizedConfig.coverage?.enabled)?.normalizedConfig.coverage;
3998
- const coverageProvider = coverageConfig?.enabled ? await createCoverageProvider(coverageConfig, context.rootPath) : null;
3999
- const notifyTestRunEnd = async ({ duration, coverage })=>{
4000
- if (!isWatchMode) return;
4001
- for (const reporter of context.reporters)await reporter.onTestRunEnd?.({
4002
- results: context.reporterResults.results,
4003
- coverage,
4004
- testResults: context.reporterResults.testResults,
4005
- duration,
4006
- snapshotSummary: context.snapshotManager.summary,
4007
- getSourcemap: getBrowserSourcemap
4008
- });
4009
- };
4010
- const containerDevServerEnv = process.env.RSTEST_CONTAINER_DEV_SERVER;
4011
- let containerDevServer;
4012
- let containerDistPath;
4013
- if (!useHeadlessDirect) {
4014
- if (containerDevServerEnv) try {
4015
- containerDevServer = new URL(containerDevServerEnv).toString();
4016
- logger.debug(`[Browser UI] Using dev server for container: ${containerDevServer}`);
4017
- } catch (error) {
4018
- const originalError = toError(error);
4019
- originalError.message = `Invalid RSTEST_CONTAINER_DEV_SERVER value: ${originalError.message}`;
4020
- return failWithError(originalError);
4021
- }
4022
- if (!containerDevServer) try {
4023
- containerDistPath = resolveContainerDist();
4024
- } catch (error) {
4025
- return failWithError(error);
4026
- }
3406
+ const flattenSourceMap = (sourceMap, sourceMapUrl)=>encodedMap(new AnyMap(sourceMap, sourceMapUrl));
3407
+ const normalizeJavaScriptUrl = (value, options)=>{
3408
+ try {
3409
+ const url = options?.origin ? new URL(value, options.origin) : new URL(value);
3410
+ if ('http:' !== url.protocol && 'https:' !== url.protocol) return null;
3411
+ url.hash = '';
3412
+ return url.toString();
3413
+ } catch {
3414
+ return null;
4027
3415
  }
4028
- let projectEntries = await resolveProjectEntries(context, options?.shardedEntries, browserProjects);
4029
- let totalTests = projectEntries.reduce((total, item)=>total + item.testFiles.length, 0);
4030
- const shouldInitializeEmptyBrowserHooks = 0 === totalTests && hasUserRstestConfigPlugins(browserProjects);
4031
- const createEmptyRunResult = ()=>{
4032
- const elapsed = Math.max(0, Date.now() - buildStart);
4033
- return {
4034
- results: [],
4035
- testResults: [],
4036
- duration: {
4037
- totalTime: elapsed,
4038
- buildTime: elapsed,
4039
- testTime: 0
4040
- },
4041
- hasFailure: false,
4042
- getSourcemap: getBrowserSourcemap,
4043
- resolveSourcemap: resolveBrowserSourcemap
4044
- };
4045
- };
4046
- const reportEmptyTestSet = ()=>{
4047
- const code = context.normalizedConfig.passWithNoTests ? 0 : 1;
4048
- if (isWatchMode || !allowEmptyRun) {
4049
- const message = getNoTestFilesMessage({
4050
- context,
4051
- code,
4052
- defaultMessage: `No test files found, exiting with code ${code}.`
4053
- });
4054
- if (0 === code) logger.log(color.yellow(message));
4055
- else logger.error(color.red(message));
4056
- if (context.relatedFilters?.length) logger.log(color.gray('related: '), context.relatedFilters.join(color.gray(', ')));
4057
- else if (context.fileFilters?.length) logger.log(color.gray('filter: '), context.fileFilters.join(color.gray(', ')));
4058
- }
4059
- if (isWatchMode && 0 !== code && !allowEmptyRun) ensureProcessExitCode(code);
4060
- };
4061
- if (0 === totalTests && !shouldInitializeEmptyBrowserHooks) {
4062
- reportEmptyTestSet();
4063
- return allowEmptyRun ? createEmptyRunResult() : void 0;
3416
+ };
3417
+ const resolveInlineSourceMap = (code, sourceMapUrl)=>{
3418
+ try {
3419
+ const converter = convert_source_map.fromSource(code);
3420
+ return converter ? flattenSourceMap(converter.toObject(), sourceMapUrl) : null;
3421
+ } catch {
3422
+ return null;
4064
3423
  }
4065
- if (!filesOnly) await notifyTestRunStart();
4066
- const enableCliShortcuts = isWatchMode && isTTY('stdin');
4067
- const browserTempOutputRoot = context.normalizedConfig.output.distPath.root;
3424
+ };
3425
+ const loadSourceMapForSource = async ({ jsUrl, signal, source, fetcher = fetch })=>{
3426
+ const normalizedUrl = normalizeJavaScriptUrl(jsUrl);
3427
+ if (!normalizedUrl) return null;
3428
+ try {
3429
+ const matches = [
3430
+ ...source.matchAll(convert_source_map.mapFileCommentRegex)
3431
+ ];
3432
+ const finalMatch = matches.at(-1);
3433
+ const sourceMapUrl = finalMatch?.[1] ?? finalMatch?.[2]?.trim();
3434
+ if (!sourceMapUrl || sourceMapUrl.startsWith('data:')) return null;
3435
+ const resolvedSourceMapUrl = new URL(sourceMapUrl, normalizedUrl);
3436
+ if ('http:' !== resolvedSourceMapUrl.protocol && 'https:' !== resolvedSourceMapUrl.protocol) return null;
3437
+ const mapResponse = await fetcher(resolvedSourceMapUrl.href, signal ? {
3438
+ signal
3439
+ } : void 0);
3440
+ return mapResponse.ok ? {
3441
+ sourceMap: flattenSourceMap(await mapResponse.json(), resolvedSourceMapUrl.href),
3442
+ sourceMapUrl: resolvedSourceMapUrl.href
3443
+ } : null;
3444
+ } catch {
3445
+ return null;
3446
+ }
3447
+ };
3448
+ const fetchSourceMap = async (jsUrl, fetcher)=>{
3449
+ const jsResponse = await fetcher(jsUrl);
3450
+ if (!jsResponse.ok) return null;
3451
+ const code = await jsResponse.text();
3452
+ const inlineMap = resolveInlineSourceMap(code, jsUrl);
3453
+ if (inlineMap) return inlineMap;
3454
+ const mapUrl = new URL(jsUrl);
3455
+ mapUrl.pathname += '.map';
3456
+ const mapResponse = await fetcher(mapUrl.href);
3457
+ if (!mapResponse.ok) return null;
3458
+ return flattenSourceMap(await mapResponse.json(), mapUrl.href);
3459
+ };
3460
+ const loadSourceMapWithCache = async ({ jsUrl, cache, force = false, origin, fetcher = fetch })=>{
3461
+ const normalizedUrl = normalizeJavaScriptUrl(jsUrl, {
3462
+ origin
3463
+ });
3464
+ if (!normalizedUrl) return null;
3465
+ if (!force && cache.has(normalizedUrl)) return cache.get(normalizedUrl) ?? null;
3466
+ try {
3467
+ const sourceMap = await fetchSourceMap(normalizedUrl, fetcher);
3468
+ cache.set(normalizedUrl, sourceMap);
3469
+ return sourceMap;
3470
+ } catch {
3471
+ cache.set(normalizedUrl, null);
3472
+ return null;
3473
+ }
3474
+ };
3475
+ const resolveCoverageResourceUrl = (value)=>{
3476
+ try {
3477
+ const url = new URL(value);
3478
+ return 'http:' === url.protocol || 'https:' === url.protocol ? url.href : null;
3479
+ } catch {
3480
+ return null;
3481
+ }
3482
+ };
3483
+ const resolveProjectHttpSource = (source, rootPath, projectOrigin)=>{
3484
+ let sourceUrl;
3485
+ try {
3486
+ sourceUrl = new URL(source);
3487
+ } catch {
3488
+ return source;
3489
+ }
3490
+ if ('http:' !== sourceUrl.protocol && 'https:' !== sourceUrl.protocol || sourceUrl.origin !== projectOrigin) return source;
3491
+ let sourcePath = sourceUrl.pathname;
3492
+ try {
3493
+ sourcePath = decodeURIComponent(sourcePath);
3494
+ } catch {}
3495
+ return external_pathe_resolve(rootPath, sourcePath.replace(/^\/+/, ''));
3496
+ };
3497
+ const normalizeSourceMap = (sourceMap, rootPath, sourceMapUrl, scriptOrigin, projectOrigin)=>{
3498
+ const webpackSourceRoot = 'string' == typeof sourceMap.sourceRoot && sourceMap.sourceRoot.startsWith('webpack:') ? sourceMap.sourceRoot : void 0;
3499
+ const resolvedSources = new TraceMap(sourceMap, sourceMapUrl).resolvedSources;
3500
+ const { sourceRoot: _, ...sourceMapWithoutRoot } = sourceMap;
3501
+ return {
3502
+ ...sourceMapWithoutRoot,
3503
+ sources: sourceMap.sources.map((source, index)=>{
3504
+ if ('string' != typeof source) return source;
3505
+ if ('rstest runtime' === source || source.startsWith('webpack/runtime/') || source.startsWith('data:') || source.startsWith('blob:')) return source;
3506
+ if (external_pathe_isAbsolute(source)) return source;
3507
+ let sourceUrl;
3508
+ try {
3509
+ sourceUrl = webpackSourceRoot ? new URL(source, webpackSourceRoot) : new URL(source);
3510
+ } catch {
3511
+ const resolvedSource = resolvedSources[index] ?? source;
3512
+ return resolveProjectHttpSource(resolvedSource, rootPath, projectOrigin);
3513
+ }
3514
+ if ('webpack:' !== sourceUrl.protocol) {
3515
+ const resolvedSource = resolvedSources[index] ?? source;
3516
+ return resolveProjectHttpSource(resolvedSource, rootPath, projectOrigin);
3517
+ }
3518
+ let sourcePath = sourceUrl.pathname;
3519
+ try {
3520
+ sourcePath = decodeURIComponent(sourcePath);
3521
+ } catch {}
3522
+ sourcePath = sourcePath.replace(/^\/+/, '');
3523
+ if ('rstest runtime' === sourcePath || sourcePath.startsWith('data:') || sourcePath.startsWith('blob:')) return sourcePath;
3524
+ if (sourcePath.startsWith('webpack/runtime/')) return source;
3525
+ return scriptOrigin === projectOrigin ? external_pathe_resolve(rootPath, sourcePath) : new URL(sourcePath, `${scriptOrigin}/`).href;
3526
+ })
3527
+ };
3528
+ };
3529
+ const takeBrowserV8Coverage = async ({ collector, page, projectUrl, rootPath, fetchTimeout, sourceMapCache, resourceStore })=>{
3530
+ const rawEntries = await collector.take(page);
3531
+ const projectOrigin = new URL(projectUrl).origin;
3532
+ const entries = [];
3533
+ const assetFiles = {};
3534
+ const sourceMaps = {};
3535
+ await Promise.all(rawEntries.map(async (entry)=>{
3536
+ const url = resolveCoverageResourceUrl(entry.url);
3537
+ const cacheUrl = normalizeJavaScriptUrl(entry.url);
3538
+ if (!url || !cacheUrl || !entry.source) return;
3539
+ const inlineSourceMap = resolveInlineSourceMap(entry.source, url);
3540
+ const loadedSourceMap = inlineSourceMap ? null : await loadSourceMapForSource({
3541
+ jsUrl: url,
3542
+ signal: fetchTimeout > 0 ? AbortSignal.timeout(fetchTimeout) : void 0,
3543
+ source: entry.source
3544
+ });
3545
+ const sourceMap = inlineSourceMap ?? loadedSourceMap?.sourceMap;
3546
+ const normalizedSourceMap = sourceMap ? normalizeSourceMap(sourceMap, rootPath, loadedSourceMap?.sourceMapUrl ?? url, new URL(url).origin, projectOrigin) : void 0;
3547
+ const filePath = url;
3548
+ entries.push({
3549
+ url,
3550
+ scriptId: entry.scriptId,
3551
+ filePath,
3552
+ functions: entry.functions
3553
+ });
3554
+ if (resourceStore?.assetFiles.get(filePath) !== entry.source) {
3555
+ assetFiles[filePath] = entry.source;
3556
+ resourceStore?.assetFiles.set(filePath, entry.source);
3557
+ }
3558
+ if (normalizedSourceMap) {
3559
+ sourceMapCache.set(cacheUrl, normalizedSourceMap);
3560
+ const serializedSourceMap = JSON.stringify(normalizedSourceMap);
3561
+ if (resourceStore?.sourceMaps.get(filePath) !== serializedSourceMap) {
3562
+ sourceMaps[filePath] = serializedSourceMap;
3563
+ resourceStore?.sourceMaps.set(filePath, serializedSourceMap);
3564
+ }
3565
+ } else {
3566
+ sourceMapCache.delete(cacheUrl);
3567
+ resourceStore?.sourceMaps.delete(filePath);
3568
+ }
3569
+ }));
3570
+ if (!entries.length) return null;
3571
+ return {
3572
+ entries,
3573
+ ...Object.keys(assetFiles).length || Object.keys(sourceMaps).length ? {
3574
+ options: {
3575
+ assetFiles,
3576
+ sourceMaps
3577
+ }
3578
+ } : {},
3579
+ root: rootPath
3580
+ };
3581
+ };
3582
+ function createPromiseWithResolvers() {
3583
+ let resolve;
3584
+ let reject;
3585
+ return {
3586
+ promise: new Promise((res, rej)=>{
3587
+ resolve = res;
3588
+ reject = rej;
3589
+ }),
3590
+ resolve,
3591
+ reject
3592
+ };
3593
+ }
3594
+ const random = Math.random.bind(Math);
3595
+ const urlAlphabet = "useandom-26T198340PX75pxJACKVERYMINDBUSHWOLF_GQZbfghjklqvwyzrict";
3596
+ function nanoid(size = 21) {
3597
+ let id = "";
3598
+ let i = size;
3599
+ while(i--)id += urlAlphabet[64 * random() | 0];
3600
+ return id;
3601
+ }
3602
+ const DEFAULT_TIMEOUT = 6e4;
3603
+ const defaultSerialize = (i)=>i;
3604
+ const defaultDeserialize = defaultSerialize;
3605
+ const { clearTimeout: dist_clearTimeout, setTimeout: dist_setTimeout } = globalThis;
3606
+ function createBirpc($functions, options) {
3607
+ const { post, on, off = ()=>{}, eventNames = [], serialize = defaultSerialize, deserialize = defaultDeserialize, resolver, bind = "rpc", timeout = DEFAULT_TIMEOUT, proxify = true } = options;
3608
+ let $closed = false;
3609
+ const _rpcPromiseMap = /* @__PURE__ */ new Map();
3610
+ let _promiseInit;
3611
+ let rpc;
3612
+ async function _call(method, args, event, optional) {
3613
+ if ($closed) throw new Error(`[birpc] rpc is closed, cannot call "${method}"`);
3614
+ const req = {
3615
+ m: method,
3616
+ a: args,
3617
+ t: "q"
3618
+ };
3619
+ if (optional) req.o = true;
3620
+ const send = async (_req)=>post(serialize(_req));
3621
+ if (event) return void await send(req);
3622
+ if (_promiseInit) try {
3623
+ await _promiseInit;
3624
+ } finally{
3625
+ _promiseInit = void 0;
3626
+ }
3627
+ let { promise, resolve, reject } = createPromiseWithResolvers();
3628
+ const id = nanoid();
3629
+ req.i = id;
3630
+ let timeoutId;
3631
+ async function handler(newReq = req) {
3632
+ if (timeout >= 0) {
3633
+ timeoutId = dist_setTimeout(()=>{
3634
+ try {
3635
+ if (options.onTimeoutError?.call(rpc, method, args) !== true) throw new Error(`[birpc] timeout on calling "${method}"`);
3636
+ } catch (e) {
3637
+ reject(e);
3638
+ }
3639
+ _rpcPromiseMap.delete(id);
3640
+ }, timeout);
3641
+ if ("object" == typeof timeoutId) timeoutId = timeoutId.unref?.();
3642
+ }
3643
+ _rpcPromiseMap.set(id, {
3644
+ resolve,
3645
+ reject,
3646
+ timeoutId,
3647
+ method
3648
+ });
3649
+ await send(newReq);
3650
+ return promise;
3651
+ }
3652
+ try {
3653
+ if (options.onRequest) await options.onRequest.call(rpc, req, handler, resolve);
3654
+ else await handler();
3655
+ } catch (e) {
3656
+ if (options.onGeneralError?.call(rpc, e) !== true) throw e;
3657
+ return;
3658
+ } finally{
3659
+ dist_clearTimeout(timeoutId);
3660
+ _rpcPromiseMap.delete(id);
3661
+ }
3662
+ return promise;
3663
+ }
3664
+ const builtinMethods = {
3665
+ $call: (method, ...args)=>_call(method, args, false),
3666
+ $callOptional: (method, ...args)=>_call(method, args, false, true),
3667
+ $callEvent: (method, ...args)=>_call(method, args, true),
3668
+ $callRaw: (options)=>_call(options.method, options.args, options.event, options.optional),
3669
+ $rejectPendingCalls,
3670
+ get $closed () {
3671
+ return $closed;
3672
+ },
3673
+ get $meta () {
3674
+ return options.meta;
3675
+ },
3676
+ $close,
3677
+ $functions
3678
+ };
3679
+ rpc = proxify ? new Proxy({}, {
3680
+ get (_, method) {
3681
+ if (Object.hasOwn(builtinMethods, method)) return builtinMethods[method];
3682
+ if ("then" === method && !eventNames.includes("then") && !("then" in $functions)) return;
3683
+ const sendEvent = (...args)=>_call(method, args, true);
3684
+ if (eventNames.includes(method)) {
3685
+ sendEvent.asEvent = sendEvent;
3686
+ return sendEvent;
3687
+ }
3688
+ const sendCall = (...args)=>_call(method, args, false);
3689
+ sendCall.asEvent = sendEvent;
3690
+ return sendCall;
3691
+ }
3692
+ }) : builtinMethods;
3693
+ function $close(customError) {
3694
+ $closed = true;
3695
+ _rpcPromiseMap.forEach(({ reject, method })=>{
3696
+ const error = /* @__PURE__ */ new Error(`[birpc] rpc is closed, cannot call "${method}"`);
3697
+ if (customError) {
3698
+ customError.cause ??= error;
3699
+ return reject(customError);
3700
+ }
3701
+ reject(error);
3702
+ });
3703
+ _rpcPromiseMap.clear();
3704
+ off(onMessage);
3705
+ }
3706
+ function $rejectPendingCalls(handler) {
3707
+ const handlerResults = Array.from(_rpcPromiseMap.values()).map(({ method, reject })=>{
3708
+ if (!handler) return reject(/* @__PURE__ */ new Error(`[birpc]: rejected pending call "${method}".`));
3709
+ return handler({
3710
+ method,
3711
+ reject
3712
+ });
3713
+ });
3714
+ _rpcPromiseMap.clear();
3715
+ return handlerResults;
3716
+ }
3717
+ async function onMessage(data, ...extra) {
3718
+ let msg;
3719
+ try {
3720
+ msg = deserialize(data);
3721
+ } catch (e) {
3722
+ if (options.onGeneralError?.call(rpc, e) !== true) throw e;
3723
+ return;
3724
+ }
3725
+ if ("q" === msg.t) {
3726
+ const { m: method, a: args, o: optional } = msg;
3727
+ let result, error;
3728
+ let fn = await (resolver ? resolver.call(rpc, method, $functions[method]) : $functions[method]);
3729
+ if (optional) fn ||= ()=>void 0;
3730
+ if (fn) try {
3731
+ result = await fn.apply("rpc" === bind ? rpc : $functions, args);
3732
+ } catch (e) {
3733
+ error = e;
3734
+ }
3735
+ else error = /* @__PURE__ */ new Error(`[birpc] function "${method}" not found`);
3736
+ if (msg.i) {
3737
+ if (error && options.onFunctionError) {
3738
+ if (true === options.onFunctionError.call(rpc, error, method, args)) return;
3739
+ }
3740
+ if (!error) try {
3741
+ await post(serialize({
3742
+ t: "s",
3743
+ i: msg.i,
3744
+ r: result
3745
+ }), ...extra);
3746
+ return;
3747
+ } catch (e) {
3748
+ error = e;
3749
+ if (options.onGeneralError?.call(rpc, e, method, args) !== true) throw e;
3750
+ }
3751
+ try {
3752
+ await post(serialize({
3753
+ t: "s",
3754
+ i: msg.i,
3755
+ e: error
3756
+ }), ...extra);
3757
+ } catch (e) {
3758
+ if (options.onGeneralError?.call(rpc, e, method, args) !== true) throw e;
3759
+ }
3760
+ }
3761
+ } else {
3762
+ const { i: ack, r: result, e: error } = msg;
3763
+ const promise = _rpcPromiseMap.get(ack);
3764
+ if (promise) {
3765
+ dist_clearTimeout(promise.timeoutId);
3766
+ if (error) promise.reject(error);
3767
+ else promise.resolve(result);
3768
+ }
3769
+ _rpcPromiseMap.delete(ack);
3770
+ }
3771
+ }
3772
+ _promiseInit = on(onMessage);
3773
+ return rpc;
3774
+ }
3775
+ class ContainerRpcManager {
3776
+ wss;
3777
+ ws = null;
3778
+ rpc = null;
3779
+ methods;
3780
+ onDisconnect;
3781
+ onAttach;
3782
+ detachActiveSocketListeners = null;
3783
+ transportEpoch = 0;
3784
+ constructor(wss, methods, onDisconnect, onAttach){
3785
+ this.wss = wss;
3786
+ this.methods = methods;
3787
+ this.onDisconnect = onDisconnect;
3788
+ this.onAttach = onAttach;
3789
+ this.setupConnectionHandler();
3790
+ }
3791
+ updateMethods(methods, onDisconnect, onAttach) {
3792
+ this.methods = methods;
3793
+ this.onDisconnect = onDisconnect;
3794
+ this.onAttach = onAttach;
3795
+ if (this.ws && this.ws.readyState === this.ws.OPEN) this.attachWebSocket(this.ws);
3796
+ }
3797
+ setupConnectionHandler() {
3798
+ this.wss.on('connection', (ws)=>{
3799
+ logger.debug('[Browser UI] Container WebSocket connected');
3800
+ logger.debug(`[Browser UI] Current ws: ${this.ws ? 'exists' : 'null'}, new ws: ${ws ? 'exists' : 'null'}`);
3801
+ this.attachWebSocket(ws);
3802
+ });
3803
+ }
3804
+ attachWebSocket(ws) {
3805
+ this.detachActiveSocketListeners?.();
3806
+ if (this.rpc && !this.rpc.$closed) this.rpc.$close(new Error('Container RPC transport reattached'));
3807
+ this.transportEpoch += 1;
3808
+ this.onAttach?.(this.transportEpoch);
3809
+ this.ws = ws;
3810
+ const messageHandlers = new WeakMap();
3811
+ this.rpc = createBirpc(this.methods, {
3812
+ timeout: -1,
3813
+ post: (data)=>{
3814
+ if (ws.readyState === ws.OPEN) ws.send(JSON.stringify(data));
3815
+ },
3816
+ on: (fn)=>{
3817
+ const handler = (message)=>{
3818
+ try {
3819
+ const data = JSON.parse(message.toString());
3820
+ fn(data);
3821
+ } catch {}
3822
+ };
3823
+ messageHandlers.set(fn, handler);
3824
+ ws.on('message', handler);
3825
+ },
3826
+ off: (fn)=>{
3827
+ const handler = messageHandlers.get(fn);
3828
+ if (!handler) return;
3829
+ ws.off('message', handler);
3830
+ messageHandlers.delete(fn);
3831
+ }
3832
+ });
3833
+ const handleClose = ()=>{
3834
+ if (this.ws === ws) this.ws = null;
3835
+ this.detachActiveSocketListeners?.();
3836
+ this.detachActiveSocketListeners = null;
3837
+ if (this.rpc && !this.rpc.$closed) {
3838
+ const disconnectError = new Error('Browser UI WebSocket disconnected before reload completed');
3839
+ this.rpc.$close(disconnectError);
3840
+ this.onDisconnect?.(disconnectError);
3841
+ }
3842
+ this.rpc = null;
3843
+ };
3844
+ ws.on('close', handleClose);
3845
+ this.detachActiveSocketListeners = ()=>{
3846
+ ws.off('close', handleClose);
3847
+ };
3848
+ }
3849
+ get isConnected() {
3850
+ return null !== this.ws && this.ws.readyState === this.ws.OPEN;
3851
+ }
3852
+ get currentWebSocket() {
3853
+ return this.ws;
3854
+ }
3855
+ reattach(ws) {
3856
+ this.attachWebSocket(ws);
3857
+ }
3858
+ async notifyTestFileUpdate(files, version) {
3859
+ await this.rpc?.onTestFileUpdate(files, version);
3860
+ }
3861
+ async updateHostConfig(config) {
3862
+ await this.rpc?.onHostConfigUpdate(config);
3863
+ }
3864
+ async reloadTestFile(testFile, runId, testNamePattern) {
3865
+ logger.debug(`[Browser UI] reloadTestFile called, rpc: ${this.rpc ? 'exists' : 'null'}, ws: ${this.ws ? 'exists' : 'null'}`);
3866
+ if (!this.rpc) throw new Error('Browser UI RPC not available for reloadTestFile');
3867
+ logger.debug(`[Browser UI] Calling reloadTestFile: ${testFile} (run ${runId})`);
3868
+ await this.rpc.reloadTestFile(testFile, runId, testNamePattern);
3869
+ }
3870
+ }
3871
+ const getFileTaskId = (testPath)=>`file:${testPath}`;
3872
+ const toError = (error)=>error instanceof Error ? error : new Error(String(error));
3873
+ const createDeferredPromise = ()=>{
3874
+ let resolve;
3875
+ let reject;
3876
+ const promise = new Promise((res, rej)=>{
3877
+ resolve = res;
3878
+ reject = rej;
3879
+ });
3880
+ return {
3881
+ promise,
3882
+ resolve,
3883
+ reject
3884
+ };
3885
+ };
3886
+ const RUN_BOOT_DEADLINE_MS = 45000;
3887
+ const createHeadedRunRegistry = ()=>{
3888
+ const runsById = new Map();
3889
+ let transportEpoch = 0;
3890
+ const settle = (record, error)=>{
3891
+ if (!runsById.delete(record.runId)) return;
3892
+ clearTimeout(record.bootTimer);
3893
+ clearTimeout(record.cleanupTimer);
3894
+ if (error) record.deferred.reject(error);
3895
+ else record.deferred.resolve();
3896
+ };
3897
+ const sweepOpen = (match, error)=>{
3898
+ for (const record of runsById.values())if ('open' === record.phase && match(record)) settle(record, error);
3899
+ };
3900
+ return {
3901
+ mint (testPath) {
3902
+ sweepOpen((record)=>record.testPath === testPath);
3903
+ const runId = randomUUID();
3904
+ const bootTimer = setTimeout(()=>{
3905
+ const record = runsById.get(runId);
3906
+ if (record) settle(record, new Error(`Headed run for ${testPath} produced no message within ${RUN_BOOT_DEADLINE_MS / 1000}s; its document never booted`));
3907
+ }, RUN_BOOT_DEADLINE_MS);
3908
+ bootTimer.unref?.();
3909
+ const record = {
3910
+ runId,
3911
+ testPath,
3912
+ epoch: transportEpoch,
3913
+ phase: 'open',
3914
+ deferred: createDeferredPromise(),
3915
+ bootTimer
3916
+ };
3917
+ runsById.set(runId, record);
3918
+ return {
3919
+ runId,
3920
+ settled: record.deferred.promise
3921
+ };
3922
+ },
3923
+ admit (runId, terminal) {
3924
+ const record = runsById.get(runId);
3925
+ if (!record) return false;
3926
+ clearTimeout(record.bootTimer);
3927
+ if (!terminal) return true;
3928
+ if ('open' !== record.phase) return false;
3929
+ record.phase = 'claimed';
3930
+ return true;
3931
+ },
3932
+ armCleanupDeadline (runId, ms, onExpire) {
3933
+ const record = runsById.get(runId);
3934
+ if (!record || 'open' !== record.phase) return;
3935
+ clearTimeout(record.cleanupTimer);
3936
+ const cleanupTimer = setTimeout(()=>{
3937
+ const current = runsById.get(runId);
3938
+ if (!current || 'open' !== current.phase) return;
3939
+ current.cleanupTimer = void 0;
3940
+ current.phase = 'claimed';
3941
+ onExpire();
3942
+ }, ms);
3943
+ cleanupTimer.unref?.();
3944
+ record.cleanupTimer = cleanupTimer;
3945
+ },
3946
+ disarmCleanupDeadline (runId) {
3947
+ const record = runsById.get(runId);
3948
+ if (record) {
3949
+ clearTimeout(record.cleanupTimer);
3950
+ record.cleanupTimer = void 0;
3951
+ }
3952
+ },
3953
+ resolve (runId) {
3954
+ const record = runsById.get(runId);
3955
+ if (record) settle(record);
3956
+ },
3957
+ reject (runId, error) {
3958
+ const record = runsById.get(runId);
3959
+ if (record) settle(record, toError(error));
3960
+ },
3961
+ retainPaths (paths) {
3962
+ const retained = new Set(paths);
3963
+ sweepOpen((record)=>!retained.has(record.testPath));
3964
+ },
3965
+ rejectAll (error) {
3966
+ sweepOpen(()=>true, error);
3967
+ },
3968
+ setTransportEpoch (epoch) {
3969
+ transportEpoch = epoch;
3970
+ sweepOpen((record)=>record.epoch < epoch, new Error('Container transport was replaced before the run completed'));
3971
+ }
3972
+ };
3973
+ };
3974
+ const createHeadedSerialTaskQueue = ()=>{
3975
+ let queue = Promise.resolve();
3976
+ const enqueue = (task)=>{
3977
+ const next = queue.then(task);
3978
+ queue = next.catch(()=>{});
3979
+ return next;
3980
+ };
3981
+ return {
3982
+ enqueue
3983
+ };
3984
+ };
3985
+ const claimHeadedCycleScope = (testPaths, currentTestFiles, pendingTestNamePatterns)=>{
3986
+ const scope = [];
3987
+ const filesByPath = new Map(currentTestFiles.map((file)=>[
3988
+ file.testPath,
3989
+ file
3990
+ ]));
3991
+ for (const testPath of testPaths){
3992
+ const normalizedTestPath = normalize(testPath);
3993
+ const file = filesByPath.get(normalizedTestPath);
3994
+ if (file) {
3995
+ scope.push({
3996
+ file,
3997
+ testNamePattern: pendingTestNamePatterns.get(normalizedTestPath)
3998
+ });
3999
+ pendingTestNamePatterns.delete(normalizedTestPath);
4000
+ }
4001
+ }
4002
+ return scope;
4003
+ };
4004
+ const createHeadedScheduler = async ({ context, runtime, allTestFiles, hostOptions, v8Coverage, projectRoots, isWatchMode, createDispatchRouter, handlers: { handleTestFileComplete }, fatalErrorRef, watchSignals, setDispatchPageResolver, createWatchSession, collectProjectEntries, logWatchReady, destroyRuntime })=>{
4005
+ const { browser, browserLaunchOptions, watchState, wss } = runtime;
4006
+ const containerUrl = `http://localhost:${runtime.containerServer.port}/`;
4007
+ let currentTestFiles = allTestFiles;
4008
+ const RUNNER_FRAMES_READY_TIMEOUT_MS = 30000;
4009
+ let ackedFrameSetVersion = 0;
4010
+ const frameSetWaiters = new Set();
4011
+ const markFrameSetReady = (version)=>{
4012
+ if (version > watchState.headedFileSetVersion) return;
4013
+ if (version > ackedFrameSetVersion) ackedFrameSetVersion = version;
4014
+ for (const waiter of frameSetWaiters)if (waiter.version <= ackedFrameSetVersion) {
4015
+ clearTimeout(waiter.timeoutId);
4016
+ frameSetWaiters.delete(waiter);
4017
+ waiter.resolve();
4018
+ }
4019
+ };
4020
+ const waitForFrameSet = async (version)=>{
4021
+ if (ackedFrameSetVersion >= version) return;
4022
+ await new Promise((resolve, reject)=>{
4023
+ const waiter = {
4024
+ version,
4025
+ resolve,
4026
+ timeoutId: setTimeout(()=>{
4027
+ frameSetWaiters.delete(waiter);
4028
+ reject(new Error(`Timed out waiting for headed runner frames to be ready (file-set version ${version}).`));
4029
+ }, RUNNER_FRAMES_READY_TIMEOUT_MS)
4030
+ };
4031
+ frameSetWaiters.add(waiter);
4032
+ });
4033
+ };
4034
+ const findTestFileInfo = (testFile)=>{
4035
+ const normalizedTestFile = normalize(testFile);
4036
+ return currentTestFiles.find((file)=>file.testPath === normalizedTestFile);
4037
+ };
4038
+ const getTestFileInfo = (testFile)=>{
4039
+ const fileInfo = findTestFileInfo(testFile);
4040
+ if (!fileInfo) throw new Error(`Unknown browser test file: ${JSON.stringify(testFile)}`);
4041
+ return fileInfo;
4042
+ };
4043
+ let containerContext;
4044
+ let containerPage;
4045
+ let isNewPage = false;
4046
+ const attachContainerPageHandlers = (activeContext, activePage)=>{
4047
+ activePage.on('popup', async (popup)=>{
4048
+ await popup.close().catch(()=>{});
4049
+ });
4050
+ activeContext.on('page', async (page)=>{
4051
+ if (page !== activePage) await page.close().catch(()=>{});
4052
+ });
4053
+ activePage.on('console', (msg)=>{
4054
+ const text = msg.text();
4055
+ if (text.startsWith('[Container]') || text.startsWith('[Runner]')) return void logger.log(color.gray(`[Browser Console] ${text}`));
4056
+ logger.debug(`[Browser Console] ${text}`);
4057
+ });
4058
+ };
4059
+ const createContainerPage = async ()=>{
4060
+ const nextContext = await browser.newContext({
4061
+ providerOptions: browserLaunchOptions.providerOptions,
4062
+ viewport: null
4063
+ });
4064
+ const nextPage = await nextContext.newPage();
4065
+ attachContainerPageHandlers(nextContext, nextPage);
4066
+ return {
4067
+ context: nextContext,
4068
+ page: nextPage
4069
+ };
4070
+ };
4071
+ if (isWatchMode && runtime.containerPage && runtime.containerContext) {
4072
+ containerContext = runtime.containerContext;
4073
+ containerPage = runtime.containerPage;
4074
+ logger.log(color.gray('\n[Watch] Reusing existing container page\n'));
4075
+ } else {
4076
+ isNewPage = true;
4077
+ const created = await createContainerPage();
4078
+ containerContext = created.context;
4079
+ containerPage = created.page;
4080
+ if (isWatchMode) {
4081
+ runtime.containerPage = containerPage;
4082
+ runtime.containerContext = containerContext;
4083
+ }
4084
+ }
4085
+ setDispatchPageResolver(()=>({
4086
+ containerPage
4087
+ }));
4088
+ const dispatchRouter = createDispatchRouter();
4089
+ const headedReloadQueue = createHeadedSerialTaskQueue();
4090
+ let rawCoverage = [];
4091
+ let coverageProject;
4092
+ let coverageStarted = false;
4093
+ const startCoverage = async (projectName)=>{
4094
+ if (!v8Coverage) return;
4095
+ await v8Coverage.start(containerPage);
4096
+ coverageStarted = true;
4097
+ coverageProject = projectName;
4098
+ };
4099
+ const collectCoverage = async ()=>{
4100
+ if (!coverageStarted || !v8Coverage) return;
4101
+ coverageStarted = false;
4102
+ const coverage = await v8Coverage.take(containerPage, projectRoots.get(coverageProject ?? '') ?? context.rootPath, coverageProject);
4103
+ if (coverage) rawCoverage.push(coverage);
4104
+ };
4105
+ let enqueueHeadedReload = async (_file, _testNamePattern)=>{
4106
+ throw new Error('Headed reload queue is not initialized');
4107
+ };
4108
+ const isCurrentTestFile = (testPath)=>void 0 !== findTestFileInfo(testPath);
4109
+ const runs = watchState.headedRuns ??= createHeadedRunRegistry();
4110
+ const recoverHeadedContainer = async ()=>{
4111
+ ackedFrameSetVersion = 0;
4112
+ await containerContext.close().catch(()=>{});
4113
+ const created = await createContainerPage();
4114
+ containerContext = created.context;
4115
+ containerPage = created.page;
4116
+ if (isWatchMode) {
4117
+ runtime.containerPage = containerPage;
4118
+ runtime.containerContext = containerContext;
4119
+ }
4120
+ await containerPage.goto(containerUrl, {
4121
+ waitUntil: 'load'
4122
+ });
4123
+ await waitForFrameSet(watchState.headedFileSetVersion);
4124
+ };
4125
+ dispatchRouter.register(DISPATCH_NAMESPACE_FILE_CLEANUP, async (request)=>{
4126
+ const { runId } = request;
4127
+ if (!runId) return;
4128
+ if ('start' === request.method) {
4129
+ const payload = request.args;
4130
+ runs.armCleanupDeadline(runId, FIXTURE_CLEANUP_TIMEOUT_MS, ()=>{
4131
+ (async ()=>{
4132
+ try {
4133
+ await collectCoverage();
4134
+ await handleTestFileComplete(createFileCleanupTimeoutResult({
4135
+ message: `File fixture cleanup did not finish within ${FIXTURE_CLEANUP_TIMEOUT_MS}ms`,
4136
+ projectName: payload.projectName,
4137
+ result: payload.result,
4138
+ testPath: payload.testPath
4139
+ }));
4140
+ await recoverHeadedContainer();
4141
+ runs.resolve(runId);
4142
+ } catch (error) {
4143
+ runs.reject(runId, toError(error));
4144
+ }
4145
+ })();
4146
+ });
4147
+ } else if ('end' === request.method) runs.disarmCleanupDeadline(runId);
4148
+ });
4149
+ const reloadTestFileAndWait = async (file, testNamePattern)=>{
4150
+ await startCoverage(file.projectName);
4151
+ const { runId, settled } = runs.mint(file.testPath);
4152
+ rpcManager.reloadTestFile(file.testPath, runId, testNamePattern).catch((error)=>runs.reject(runId, error));
4153
+ try {
4154
+ await settled;
4155
+ } finally{
4156
+ await collectCoverage().catch((error)=>{
4157
+ logger.debug(`[Headed] Failed to flush coverage: ${error}`);
4158
+ });
4159
+ }
4160
+ };
4161
+ let runUiRequestedRerun = async (file, testNamePattern)=>{
4162
+ await enqueueHeadedReload(file, testNamePattern);
4163
+ };
4164
+ const createRpcMethods = ()=>({
4165
+ async rerunTest (testFile, testNamePattern) {
4166
+ const projectName = context.normalizedConfig.name || 'project';
4167
+ const relativePath = relative(context.rootPath, testFile);
4168
+ const displayPath = `<${projectName}>/${relativePath}`;
4169
+ logger.log(color.cyan(`\nRe-running test: ${displayPath}${testNamePattern ? ` (pattern: ${testNamePattern})` : ''}\n`));
4170
+ await runUiRequestedRerun(getTestFileInfo(testFile), testNamePattern);
4171
+ },
4172
+ async getTestFiles () {
4173
+ return {
4174
+ files: currentTestFiles,
4175
+ version: watchState.headedFileSetVersion
4176
+ };
4177
+ },
4178
+ async onFrameSetReady (version) {
4179
+ markFrameSetReady(version);
4180
+ },
4181
+ async dispatch (request) {
4182
+ const { method, runId } = request;
4183
+ const isTerminal = "runner" === request.namespace && ('file-complete' === method || 'fatal' === method);
4184
+ if (!runId || !runs.admit(runId, isTerminal)) {
4185
+ logger.debug(`[Headed] Dropped stale "${method}" from run ${runId ?? '(none)'}`);
4186
+ return {
4187
+ requestId: request.requestId,
4188
+ stale: true
4189
+ };
4190
+ }
4191
+ let coverageError;
4192
+ if (isTerminal) try {
4193
+ await collectCoverage();
4194
+ } catch (error) {
4195
+ coverageError = toError(error);
4196
+ }
4197
+ const response = await dispatchRouter.dispatch(request);
4198
+ if (!isTerminal) return response;
4199
+ if ('file-complete' === method) {
4200
+ const failure = coverageError ?? (response.error ? new Error(response.error) : void 0);
4201
+ if (failure) runs.reject(runId, failure);
4202
+ else runs.resolve(runId);
4203
+ } else {
4204
+ if (coverageError) logger.error(`[Headed] Failed to take coverage during fatal handling: ${coverageError.message}`);
4205
+ const payload = request.args;
4206
+ const error = new Error(payload?.message ?? 'Fatal browser error');
4207
+ if (payload?.stack) error.stack = payload.stack;
4208
+ runs.reject(runId, error);
4209
+ }
4210
+ return response;
4211
+ }
4212
+ });
4213
+ let rpcManager;
4214
+ if (isWatchMode && runtime.rpcManager) {
4215
+ rpcManager = runtime.rpcManager;
4216
+ rpcManager.updateMethods(createRpcMethods(), runs.rejectAll, runs.setTransportEpoch);
4217
+ const existingWs = rpcManager.currentWebSocket;
4218
+ if (existingWs) rpcManager.reattach(existingWs);
4219
+ } else {
4220
+ rpcManager = new ContainerRpcManager(wss, createRpcMethods(), runs.rejectAll, runs.setTransportEpoch);
4221
+ if (isWatchMode) runtime.rpcManager = rpcManager;
4222
+ }
4223
+ if (isNewPage) {
4224
+ await containerPage.goto(containerUrl, {
4225
+ waitUntil: 'load'
4226
+ });
4227
+ logger.log(color.cyan(`\nBrowser mode opened at ${containerUrl}\n`));
4228
+ }
4229
+ enqueueHeadedReload = async (file, testNamePattern)=>headedReloadQueue.enqueue(async ()=>{
4230
+ if (fatalErrorRef.current) return;
4231
+ if (!isCurrentTestFile(file.testPath)) return void logger.debug(`[Browser UI] Skipping reload for removed test file: ${file.testPath}`);
4232
+ await reloadTestFileAndWait(file, testNamePattern);
4233
+ });
4234
+ let testTime = 0;
4235
+ if (currentTestFiles.length > 0) {
4236
+ const testStart = Date.now();
4237
+ try {
4238
+ if (rpcManager.isConnected) await rpcManager.notifyTestFileUpdate(currentTestFiles, watchState.headedFileSetVersion);
4239
+ await waitForFrameSet(watchState.headedFileSetVersion);
4240
+ for (const file of currentTestFiles){
4241
+ await enqueueHeadedReload(file);
4242
+ if (fatalErrorRef.current) break;
4243
+ }
4244
+ } catch (error) {
4245
+ fatalErrorRef.current = fatalErrorRef.current ?? toError(error);
4246
+ }
4247
+ testTime = Date.now() - testStart;
4248
+ }
4249
+ let watchSession;
4250
+ if (isWatchMode) {
4251
+ const pendingTestNamePatterns = new Map();
4252
+ const runScope = async (testPaths)=>{
4253
+ rawCoverage = [];
4254
+ const cycleScope = claimHeadedCycleScope(testPaths, currentTestFiles, pendingTestNamePatterns);
4255
+ for (const { file, testNamePattern } of cycleScope)await enqueueHeadedReload(file, testNamePattern);
4256
+ return rawCoverage;
4257
+ };
4258
+ const refreshHostConfig = async ()=>{
4259
+ const refreshedHostOptions = {
4260
+ ...hostOptions,
4261
+ snapshot: {
4262
+ updateSnapshot: context.snapshotManager.options.updateSnapshot
4263
+ }
4264
+ };
4265
+ runtime.setContainerOptions(refreshedHostOptions);
4266
+ await rpcManager.updateHostConfig(refreshedHostOptions);
4267
+ };
4268
+ const runDispatchRerunOnce = async ()=>{
4269
+ try {
4270
+ const [, newProjectEntries] = await Promise.all([
4271
+ refreshHostConfig(),
4272
+ collectProjectEntries()
4273
+ ]);
4274
+ const rerunPlan = planWatchRerun({
4275
+ projectEntries: newProjectEntries,
4276
+ previousTestFiles: watchState.lastTestFiles,
4277
+ affectedTestFiles: drainPendingAffectedTestFiles(watchState)
4278
+ });
4279
+ commitWatchFileSetUpdate(rerunPlan.fileSetUpdate, watchState, (deletedTestPaths)=>context.updateReporterResultState([], [], deletedTestPaths));
4280
+ if (rerunPlan.fileSetUpdate) {
4281
+ currentTestFiles = rerunPlan.fileSetUpdate.currentTestFiles;
4282
+ const version = watchState.headedFileSetVersion += 1;
4283
+ runs.retainPaths(currentTestFiles.map((file)=>file.testPath));
4284
+ await rpcManager.notifyTestFileUpdate(currentTestFiles, version);
4285
+ if (currentTestFiles.length > 0) await waitForFrameSet(version);
4286
+ }
4287
+ logger.log(color.cyan(rerunPlan.decision.message));
4288
+ if ('idle' === rerunPlan.decision.kind) return void logWatchReady();
4289
+ await watchSignals.signalInvalidation('empty' === rerunPlan.decision.kind ? [] : rerunPlan.decision.testPaths);
4290
+ } catch (error) {
4291
+ logWatchReady();
4292
+ throw error;
4293
+ }
4294
+ };
4295
+ const dispatchRerunQueue = createHeadedSerialTaskQueue();
4296
+ watchSignals.setDispatchRerun(()=>dispatchRerunQueue.enqueue(runDispatchRerunOnce));
4297
+ runUiRequestedRerun = async (file, testNamePattern)=>{
4298
+ await refreshHostConfig();
4299
+ const { cycle } = await watchSignals.signalInvalidation([
4300
+ file.testPath
4301
+ ], ()=>{
4302
+ if (void 0 === testNamePattern) pendingTestNamePatterns.delete(normalize(file.testPath));
4303
+ else pendingTestNamePatterns.set(normalize(file.testPath), testNamePattern);
4304
+ });
4305
+ await cycle;
4306
+ };
4307
+ watchSession = createWatchSession(runScope);
4308
+ }
4309
+ const closeContainerRuntime = isWatchMode ? void 0 : async ()=>{
4310
+ try {
4311
+ await containerPage.close();
4312
+ } catch {}
4313
+ try {
4314
+ await containerContext.close();
4315
+ } catch {}
4316
+ await destroyRuntime();
4317
+ };
4318
+ return {
4319
+ testTime,
4320
+ rawCoverage,
4321
+ watchSession,
4322
+ close: closeContainerRuntime
4323
+ };
4324
+ };
4325
+ const DEFAULT_MAX_HEADLESS_WORKERS = 12;
4326
+ const resolveHeadlessWorkerCount = ({ command, maxWorkers, totalTasks, numCpus = browser_getNumCpus() })=>{
4327
+ const base = Math.max(Math.min(DEFAULT_MAX_HEADLESS_WORKERS, numCpus - 1), 1);
4328
+ return resolveWorkerCount({
4329
+ command,
4330
+ maxWorkers,
4331
+ totalTasks,
4332
+ recommended: base,
4333
+ watchRecommended: Math.max(Math.floor(base / 2), 1),
4334
+ numCpus
4335
+ });
4336
+ };
4337
+ const getHeadlessConcurrency = (context, totalTests)=>resolveHeadlessWorkerCount({
4338
+ command: context.command,
4339
+ maxWorkers: context.normalizedConfig.pool.maxWorkers,
4340
+ totalTasks: totalTests
4341
+ });
4342
+ const attachHeadlessRunnerTransport = async (page, handlers)=>{
4343
+ await page.exposeFunction(DISPATCH_MESSAGE_TYPE, handlers.onDispatchMessage);
4344
+ await page.exposeFunction(DISPATCH_RPC_BRIDGE_NAME, handlers.onDispatchRpc);
4345
+ };
4346
+ const createCancelSignal = ()=>{
4347
+ let settled = false;
4348
+ let resolveSignal = ()=>{};
4349
+ const signal = new Promise((resolve)=>{
4350
+ resolveSignal = ()=>{
4351
+ if (!settled) {
4352
+ settled = true;
4353
+ resolve();
4354
+ }
4355
+ };
4356
+ });
4357
+ return {
4358
+ signal,
4359
+ resolve: resolveSignal
4360
+ };
4361
+ };
4362
+ const createRunSession = (token)=>{
4363
+ const { signal, resolve } = createCancelSignal();
4364
+ return {
4365
+ token,
4366
+ cancelled: false,
4367
+ cancelSignal: signal,
4368
+ signalCancel: resolve
4369
+ };
4370
+ };
4371
+ class RunSessionLifecycle {
4372
+ currentToken = 0;
4373
+ active = null;
4374
+ get activeSession() {
4375
+ return this.active;
4376
+ }
4377
+ get activeToken() {
4378
+ return this.currentToken;
4379
+ }
4380
+ createSession(factory) {
4381
+ const session = factory(++this.currentToken);
4382
+ this.active = session;
4383
+ return session;
4384
+ }
4385
+ isTokenActive(token) {
4386
+ return token === this.currentToken;
4387
+ }
4388
+ isTokenStale(token) {
4389
+ return !this.isTokenActive(token);
4390
+ }
4391
+ invalidateActiveToken() {
4392
+ this.currentToken += 1;
4393
+ return this.currentToken;
4394
+ }
4395
+ clearIfActive(session) {
4396
+ if (this.active === session) this.active = null;
4397
+ }
4398
+ async cancel(session, options) {
4399
+ const waitForDone = options?.waitForDone ?? true;
4400
+ if (!session.cancelled) {
4401
+ session.cancelled = true;
4402
+ session.signalCancel();
4403
+ await options?.onCancel?.(session);
4404
+ }
4405
+ if (waitForDone) await session.done?.catch(()=>{});
4406
+ }
4407
+ async cancelActive(options) {
4408
+ if (!this.active) return;
4409
+ await this.cancel(this.active, options);
4410
+ }
4411
+ }
4412
+ class RunnerSessionRegistry {
4413
+ nextId = 0;
4414
+ sessionsById = new Map();
4415
+ sessionIdByTestFile = new Map();
4416
+ register(input) {
4417
+ const id = input.id ?? `runner-session-${++this.nextId}`;
4418
+ const createdAt = input.createdAt ?? Date.now();
4419
+ const record = {
4420
+ ...input,
4421
+ id,
4422
+ createdAt
4423
+ };
4424
+ this.sessionsById.set(id, record);
4425
+ this.sessionIdByTestFile.set(record.testFile, id);
4426
+ return record;
4427
+ }
4428
+ getById(id) {
4429
+ return this.sessionsById.get(id);
4430
+ }
4431
+ getByTestFile(testFile) {
4432
+ const id = this.sessionIdByTestFile.get(testFile);
4433
+ if (!id) return;
4434
+ return this.sessionsById.get(id);
4435
+ }
4436
+ list() {
4437
+ return Array.from(this.sessionsById.values());
4438
+ }
4439
+ listByRunToken(runToken) {
4440
+ return this.list().filter((session)=>session.runToken === runToken);
4441
+ }
4442
+ deleteById(id) {
4443
+ const record = this.sessionsById.get(id);
4444
+ if (!record) return false;
4445
+ this.sessionsById.delete(id);
4446
+ if (this.sessionIdByTestFile.get(record.testFile) === id) this.sessionIdByTestFile.delete(record.testFile);
4447
+ return true;
4448
+ }
4449
+ deleteByTestFile(testFile) {
4450
+ const id = this.sessionIdByTestFile.get(testFile);
4451
+ if (!id) return false;
4452
+ return this.deleteById(id);
4453
+ }
4454
+ clear() {
4455
+ this.sessionsById.clear();
4456
+ this.sessionIdByTestFile.clear();
4457
+ }
4458
+ }
4459
+ const createHeadlessScheduler = async ({ context, browser, browserLaunchOptions, projectServers, v8Coverage, allTestFiles, projectRuntimeConfigs, hostOptions, watchState, isWatchMode, createDispatchRouter, handlers: { handleFatal, handleTestFileComplete }, watchSignals, setDispatchPageResolver, createWatchSession, collectProjectEntries, logWatchReady, destroyRuntime })=>{
4460
+ const viewportByProject = mapViewportByProject(projectRuntimeConfigs);
4461
+ const projectRootByName = new Map(projectRuntimeConfigs.map((project)=>[
4462
+ project.name,
4463
+ project.projectRoot
4464
+ ]));
4465
+ const runLifecycle = new RunSessionLifecycle();
4466
+ const sessionRegistry = new RunnerSessionRegistry();
4467
+ setDispatchPageResolver((target)=>({
4468
+ runnerPage: target?.sessionId ? sessionRegistry.getById(target.sessionId)?.page : void 0
4469
+ }));
4470
+ let dispatchRequestCounter = 0;
4471
+ const nextDispatchRequestId = (namespace)=>`${namespace}-${++dispatchRequestCounter}`;
4472
+ const closeContextSafely = async (browserContext)=>{
4473
+ try {
4474
+ await browserContext.close();
4475
+ } catch {}
4476
+ };
4477
+ const cancelRun = async (run, waitForDone = true)=>{
4478
+ await runLifecycle.cancel(run, {
4479
+ waitForDone,
4480
+ onCancel: async (session)=>{
4481
+ await Promise.all(Array.from(session.contexts).map((browserContext)=>closeContextSafely(browserContext)));
4482
+ }
4483
+ });
4484
+ };
4485
+ const collectRunCoverage = async (run)=>{
4486
+ const results = await Promise.allSettled(Array.from(run.coverageCollectors, (collect)=>collect()));
4487
+ for (const result of results)if ('rejected' === result.status) {
4488
+ const error = toError(result.reason);
4489
+ await handleFatal({
4490
+ message: error.message,
4491
+ stack: error.stack
4492
+ });
4493
+ }
4494
+ };
4495
+ const dispatchRouter = createDispatchRouter({
4496
+ isRunTokenStale: (runToken)=>runLifecycle.isTokenStale(runToken),
4497
+ onStale: (request)=>{
4498
+ if ("runner" === request.namespace) logger.debug(`[Headless] Dropped stale message "${request.method}" for ${request.target?.testFile ?? 'unknown'}`);
4499
+ }
4500
+ });
4501
+ const fileCleanupHandlers = new Map();
4502
+ dispatchRouter.register(DISPATCH_NAMESPACE_FILE_CLEANUP, async (request)=>{
4503
+ const sessionId = request.target?.sessionId;
4504
+ if (!sessionId) throw new Error('File cleanup dispatch is missing a browser session.');
4505
+ const handler = fileCleanupHandlers.get(sessionId);
4506
+ if (!handler) return;
4507
+ const payload = request.args;
4508
+ if ('start' === request.method) handler.start(payload.result);
4509
+ else if ('end' === request.method) handler.end();
4510
+ });
4511
+ const dispatchRunnerMessage = async (run, file, sessionId, message)=>{
4512
+ const response = await dispatchRouter.dispatch({
4513
+ requestId: nextDispatchRequestId("runner"),
4514
+ runToken: run.token,
4515
+ namespace: "runner",
4516
+ method: message.type,
4517
+ args: 'payload' in message ? message.payload : void 0,
4518
+ target: {
4519
+ sessionId,
4520
+ testFile: file.testPath,
4521
+ projectName: file.projectName
4522
+ }
4523
+ });
4524
+ if (response.stale) return;
4525
+ if (response.error) throw new Error(response.error);
4526
+ };
4527
+ const runSingleFile = async (run, file, rawCoverage)=>{
4528
+ if (run.cancelled || runLifecycle.isTokenStale(run.token)) return;
4529
+ const viewport = viewportByProject.get(file.projectName);
4530
+ const browserContext = await browser.newContext({
4531
+ providerOptions: browserLaunchOptions.providerOptions,
4532
+ viewport: viewport ?? null
4533
+ });
4534
+ run.contexts.add(browserContext);
4535
+ let page = null;
4536
+ let sessionId = null;
4537
+ let coverageCollection;
4538
+ let settled = false;
4539
+ let resolveDone = null;
4540
+ let cleanupTimer;
4541
+ let fileCleanupFinished = false;
4542
+ let provisionalResult;
4543
+ const markDone = ()=>{
4544
+ if (!settled) {
4545
+ settled = true;
4546
+ resolveDone?.();
4547
+ }
4548
+ };
4549
+ const donePromise = new Promise((resolve)=>{
4550
+ resolveDone = resolve;
4551
+ });
4552
+ const crashDeferred = createDeferredPromise();
4553
+ const onPageDead = (reason)=>{
4554
+ if (settled || run.cancelled || !runLifecycle.isTokenActive(run.token)) return;
4555
+ settled = true;
4556
+ crashDeferred.resolve(reason);
4557
+ };
4558
+ const collectCoverage = async ()=>{
4559
+ if (!page || !v8Coverage) return;
4560
+ if (!coverageCollection) coverageCollection = (async ()=>{
4561
+ const coverage = await v8Coverage.take(page, projectRootByName.get(file.projectName) ?? context.rootPath, file.projectName);
4562
+ if (coverage) rawCoverage.push(coverage);
4563
+ })();
4564
+ await coverageCollection;
4565
+ };
4566
+ try {
4567
+ page = await browserContext.newPage();
4568
+ await v8Coverage?.start(page);
4569
+ run.coverageCollectors.add(collectCoverage);
4570
+ page.on('crash', ()=>onPageDead(`Browser page crashed while running ${file.testPath}.`));
4571
+ page.on('close', ()=>onPageDead(`Browser page closed unexpectedly while running ${file.testPath}.`));
4572
+ const session = sessionRegistry.register({
4573
+ testFile: file.testPath,
4574
+ projectName: file.projectName,
4575
+ runToken: run.token,
4576
+ mode: 'headless-page',
4577
+ context: browserContext,
4578
+ page
4579
+ });
4580
+ sessionId = session.id;
4581
+ const clearFileCleanupTimeout = ()=>{
4582
+ if (cleanupTimer) {
4583
+ clearTimeout(cleanupTimer);
4584
+ cleanupTimer = void 0;
4585
+ }
4586
+ };
4587
+ const finishFileCleanup = ()=>{
4588
+ fileCleanupFinished = true;
4589
+ clearFileCleanupTimeout();
4590
+ };
4591
+ fileCleanupHandlers.set(session.id, {
4592
+ end: finishFileCleanup,
4593
+ start: (result)=>{
4594
+ if (fileCleanupFinished) return;
4595
+ provisionalResult = result;
4596
+ clearFileCleanupTimeout();
4597
+ cleanupTimer = setTimeout(()=>{
4598
+ (async ()=>{
4599
+ if (settled) return;
4600
+ settled = true;
4601
+ const message = `File fixture cleanup did not finish within ${FIXTURE_CLEANUP_TIMEOUT_MS}ms`;
4602
+ try {
4603
+ await collectCoverage();
4604
+ await handleTestFileComplete(createFileCleanupTimeoutResult({
4605
+ message,
4606
+ projectName: file.projectName,
4607
+ result: provisionalResult,
4608
+ testPath: file.testPath
4609
+ }));
4610
+ } catch (error) {
4611
+ const formatted = toError(error);
4612
+ await handleFatal({
4613
+ message: formatted.message,
4614
+ stack: formatted.stack
4615
+ });
4616
+ await collectRunCoverage(run);
4617
+ await cancelRun(run, false);
4618
+ } finally{
4619
+ resolveDone?.();
4620
+ }
4621
+ })();
4622
+ }, FIXTURE_CLEANUP_TIMEOUT_MS);
4623
+ }
4624
+ });
4625
+ await attachHeadlessRunnerTransport(page, {
4626
+ onDispatchMessage: async (envelope)=>{
4627
+ const message = envelope.message;
4628
+ try {
4629
+ if (settled) return;
4630
+ if ('file-complete' === message.type || 'fatal' === message.type) await collectCoverage();
4631
+ await dispatchRunnerMessage(run, file, session.id, message);
4632
+ if ('file-complete' === message.type) markDone();
4633
+ else if ('fatal' === message.type) {
4634
+ markDone();
4635
+ await collectRunCoverage(run);
4636
+ await cancelRun(run, false);
4637
+ }
4638
+ } catch (error) {
4639
+ const formatted = toError(error);
4640
+ await handleFatal({
4641
+ message: formatted.message,
4642
+ stack: formatted.stack
4643
+ });
4644
+ markDone();
4645
+ await collectRunCoverage(run);
4646
+ await cancelRun(run, false);
4647
+ }
4648
+ },
4649
+ onDispatchRpc: async (request)=>dispatchRouter.dispatch({
4650
+ ...request,
4651
+ runToken: run.token,
4652
+ target: {
4653
+ sessionId: session.id,
4654
+ testFile: file.testPath,
4655
+ projectName: file.projectName,
4656
+ ...request.target
4657
+ }
4658
+ })
4659
+ });
4660
+ const inlineOptions = {
4661
+ ...hostOptions,
4662
+ snapshot: {
4663
+ updateSnapshot: context.snapshotManager.options.updateSnapshot
4664
+ },
4665
+ testFile: file.testPath,
4666
+ runId: `${run.token}:${session.id}`
4667
+ };
4668
+ const serializedOptions = serializeForInlineScript(inlineOptions);
4669
+ await page.addInitScript(`window.__RSTEST_BROWSER_OPTIONS__ = ${serializedOptions};`);
4670
+ const projectServer = projectServers.get(file.projectName);
4671
+ if (!projectServer) throw new Error(`No browser dev server for project "${file.projectName}" (test file: ${file.testPath}).`);
4672
+ await page.goto(`http://localhost:${projectServer.port}/runner.html`, {
4673
+ waitUntil: 'load'
4674
+ });
4675
+ const state = await Promise.race([
4676
+ donePromise.then(()=>({
4677
+ type: 'done'
4678
+ })),
4679
+ crashDeferred.promise.then((reason)=>({
4680
+ type: 'crash',
4681
+ reason
4682
+ })),
4683
+ run.cancelSignal.then(()=>({
4684
+ type: 'cancelled'
4685
+ }))
4686
+ ]);
4687
+ if ('cancelled' === state.type) return;
4688
+ if ('crash' === state.type && runLifecycle.isTokenActive(run.token) && !run.cancelled) {
4689
+ await collectRunCoverage(run);
4690
+ await handleFatal({
4691
+ message: state.reason
4692
+ });
4693
+ await cancelRun(run, false);
4694
+ }
4695
+ } catch (error) {
4696
+ if (runLifecycle.isTokenActive(run.token) && !run.cancelled) {
4697
+ await collectRunCoverage(run);
4698
+ const formatted = toError(error);
4699
+ await handleFatal({
4700
+ message: formatted.message,
4701
+ stack: formatted.stack
4702
+ });
4703
+ await cancelRun(run, false);
4704
+ }
4705
+ } finally{
4706
+ if (cleanupTimer) clearTimeout(cleanupTimer);
4707
+ if (sessionId) fileCleanupHandlers.delete(sessionId);
4708
+ run.coverageCollectors.delete(collectCoverage);
4709
+ const abandoned = run.cancelled || runLifecycle.isTokenStale(run.token);
4710
+ const teardown = async ()=>{
4711
+ if (page) try {
4712
+ await page.close();
4713
+ } catch {}
4714
+ await closeContextSafely(browserContext);
4715
+ };
4716
+ if (sessionId) sessionRegistry.deleteById(sessionId);
4717
+ run.contexts.delete(browserContext);
4718
+ if (abandoned) teardown();
4719
+ else await teardown();
4720
+ }
4721
+ };
4722
+ const makeSkippedFileResult = (file)=>({
4723
+ testId: getFileTaskId(file.testPath),
4724
+ status: 'skip',
4725
+ name: '',
4726
+ testPath: file.testPath,
4727
+ project: file.projectName,
4728
+ results: []
4729
+ });
4730
+ const runFilesWithPool = async (files)=>{
4731
+ const rawCoverage = [];
4732
+ if (0 === files.length) return rawCoverage;
4733
+ const previous = runLifecycle.activeSession;
4734
+ if (previous) await cancelRun(previous);
4735
+ const run = runLifecycle.createSession((token)=>({
4736
+ ...createRunSession(token),
4737
+ contexts: new Set(),
4738
+ coverageCollectors: new Set()
4739
+ }));
4740
+ const queue = [
4741
+ ...files
4742
+ ];
4743
+ const concurrency = getHeadlessConcurrency(context, queue.length);
4744
+ const bail = context.normalizedConfig.bail;
4745
+ const worker = async ()=>{
4746
+ while(queue.length > 0 && !run.cancelled && runLifecycle.isTokenActive(run.token)){
4747
+ if (bail && context.stateManager.getCountOfFailedTests() >= bail) {
4748
+ let skipped = queue.shift();
4749
+ while(skipped){
4750
+ await handleTestFileComplete(makeSkippedFileResult(skipped));
4751
+ skipped = queue.shift();
4752
+ }
4753
+ return;
4754
+ }
4755
+ const next = queue.shift();
4756
+ if (!next) return;
4757
+ await runSingleFile(run, next, rawCoverage);
4758
+ }
4759
+ };
4760
+ run.done = Promise.all(Array.from({
4761
+ length: Math.min(queue.length, Math.max(concurrency, 1))
4762
+ }, ()=>worker())).then(()=>{});
4763
+ await run.done;
4764
+ runLifecycle.clearIfActive(run);
4765
+ return rawCoverage;
4766
+ };
4767
+ const testStart = Date.now();
4768
+ const rawCoverage = await runFilesWithPool(allTestFiles);
4769
+ const testTime = Date.now() - testStart;
4770
+ let watchSession;
4771
+ if (isWatchMode) {
4772
+ const runScope = async (testPaths)=>{
4773
+ const pathSet = new Set(testPaths.map((testPath)=>normalize(testPath)));
4774
+ return runFilesWithPool(watchState.lastTestFiles.filter((file)=>pathSet.has(file.testPath)));
4775
+ };
4776
+ watchSignals.setInterrupt(async ()=>{
4777
+ const active = runLifecycle.activeSession;
4778
+ if (!active || active.cancelled) return;
4779
+ runLifecycle.invalidateActiveToken();
4780
+ await runLifecycle.cancel(active, {
4781
+ waitForDone: false
4782
+ });
4783
+ });
4784
+ watchSignals.setDispatchRerun(async ()=>{
4785
+ const newProjectEntries = await collectProjectEntries();
4786
+ const rerunPlan = planWatchRerun({
4787
+ projectEntries: newProjectEntries,
4788
+ previousTestFiles: watchState.lastTestFiles,
4789
+ affectedTestFiles: drainPendingAffectedTestFiles(watchState)
4790
+ });
4791
+ commitWatchFileSetUpdate(rerunPlan.fileSetUpdate, watchState, (deletedTestPaths)=>context.updateReporterResultState([], [], deletedTestPaths));
4792
+ logger.log(color.cyan(rerunPlan.decision.message));
4793
+ if ('idle' === rerunPlan.decision.kind) return void logWatchReady();
4794
+ await watchSignals.signalInvalidation('empty' === rerunPlan.decision.kind ? [] : rerunPlan.decision.testPaths);
4795
+ });
4796
+ watchSession = createWatchSession(runScope);
4797
+ }
4798
+ const closeHeadlessRuntime = isWatchMode ? void 0 : async ()=>{
4799
+ sessionRegistry.clear();
4800
+ await destroyRuntime();
4801
+ };
4802
+ return {
4803
+ testTime,
4804
+ rawCoverage,
4805
+ watchSession,
4806
+ close: closeHeadlessRuntime
4807
+ };
4808
+ };
4809
+ const isRecord = (value)=>'object' == typeof value && null !== value;
4810
+ const readString = (value, key, label)=>{
4811
+ const result = value[key];
4812
+ if ('string' != typeof result) throw new Error(`Invalid browser RPC request: ${label} must be a string`);
4813
+ return result;
4814
+ };
4815
+ const readUnknownArray = (value, key, label)=>{
4816
+ const result = value[key];
4817
+ if (!Array.isArray(result)) throw new Error(`Invalid browser RPC request: ${label} must be an array`);
4818
+ return result;
4819
+ };
4820
+ const parseBrowserLocatorIR = (value, label)=>{
4821
+ if (!isRecord(value)) throw new Error(`Invalid browser RPC request: ${label} must be an object`);
4822
+ const steps = value.steps;
4823
+ if (!Array.isArray(steps)) throw new Error(`Invalid browser RPC request: ${label}.steps must be an array`);
4824
+ return value;
4825
+ };
4826
+ const validateBrowserRpcRequest = (payload)=>{
4827
+ if (!isRecord(payload)) throw new Error('Invalid browser RPC request: payload must be an object');
4828
+ const kind = readString(payload, 'kind', 'kind');
4829
+ if ('locator' !== kind && 'expect' !== kind && 'config' !== kind) throw new Error(`Invalid browser RPC request: unsupported kind ${JSON.stringify(kind)}`);
4830
+ const request = {
4831
+ id: readString(payload, 'id', 'id'),
4832
+ testPath: readString(payload, 'testPath', 'testPath'),
4833
+ kind,
4834
+ locator: parseBrowserLocatorIR(payload.locator, 'locator'),
4835
+ method: readString(payload, 'method', 'method'),
4836
+ args: readUnknownArray(payload, 'args', 'args')
4837
+ };
4838
+ const isNot = payload.isNot;
4839
+ if (void 0 !== isNot) {
4840
+ if ('boolean' != typeof isNot) throw new Error('Invalid browser RPC request: isNot must be a boolean');
4841
+ request.isNot = isNot;
4842
+ }
4843
+ const timeout = payload.timeout;
4844
+ if (void 0 !== timeout) {
4845
+ if ('number' != typeof timeout) throw new Error('Invalid browser RPC request: timeout must be a number');
4846
+ request.timeout = timeout;
4847
+ }
4848
+ return request;
4849
+ };
4850
+ const watchContext = {
4851
+ runtime: null,
4852
+ cleanupRegistered: false,
4853
+ cleanupPromise: null
4854
+ };
4855
+ const runWatchRuntimeTeardown = (state, destroy)=>{
4856
+ if (state.cleanupPromise) return state.cleanupPromise;
4857
+ state.cleanupPromise = (async ()=>{
4858
+ if (!state.runtime) return;
4859
+ await destroy(state.runtime);
4860
+ state.runtime = null;
4861
+ })();
4862
+ return state.cleanupPromise.finally(()=>{
4863
+ state.cleanupPromise = null;
4864
+ });
4865
+ };
4866
+ const cleanupWatchRuntime = ()=>runWatchRuntimeTeardown(watchContext, destroyBrowserRuntime);
4867
+ const registerWatchCleanup = (embedded)=>{
4868
+ if (watchContext.cleanupRegistered) return;
4869
+ watchContext.cleanupRegistered = true;
4870
+ if (embedded) return;
4871
+ for (const signal of FATAL_SIGNALS)process.once(signal, ()=>{
4872
+ cleanupWatchRuntime();
4873
+ });
4874
+ process.once('exit', ()=>{
4875
+ cleanupWatchRuntime();
4876
+ });
4877
+ };
4878
+ const createWatchSignals = (onInvalidate)=>{
4879
+ let dispatchRerun;
4880
+ let interruptInFlightRun;
4881
+ let signalledCycle;
4882
+ return {
4883
+ setDispatchRerun (fn) {
4884
+ dispatchRerun = fn;
4885
+ },
4886
+ async runDispatchRerun () {
4887
+ await dispatchRerun?.();
4888
+ },
4889
+ setInterrupt (fn) {
4890
+ interruptInFlightRun = fn;
4891
+ },
4892
+ async signalInvalidation (fileFilters, claimScope) {
4893
+ await interruptInFlightRun?.();
4894
+ claimScope?.();
4895
+ const cycle = Promise.resolve(onInvalidate?.({
4896
+ isFirstBuild: false,
4897
+ fileFilters
4898
+ })).then(()=>{}, (error)=>{
4899
+ logger.error(color.red('Browser Mode watch cycle failed:'), error);
4900
+ });
4901
+ signalledCycle = cycle;
4902
+ return {
4903
+ cycle
4904
+ };
4905
+ },
4906
+ async awaitSignalledCycle () {
4907
+ await signalledCycle;
4908
+ }
4909
+ };
4910
+ };
4911
+ let nextBrowserFilePid = 1000000000;
4912
+ const resolveEmptyLaunchExitCode = (current, { allowEmptyRun, isWatchMode, passWithNoTests })=>{
4913
+ if (allowEmptyRun || !isWatchMode || passWithNoTests) return current;
4914
+ return void 0 === current || 0 === current ? 1 : current;
4915
+ };
4916
+ const getMaxTestTimeoutForRpc = (projects)=>Math.max(...projects.map((p)=>p.normalizedConfig.testTimeout ?? DEFAULT_TEST_TIMEOUT));
4917
+ const resolveProviderForTestPath = ({ testPath, browserProjects })=>{
4918
+ const normalizedTestPath = normalize(testPath);
4919
+ const sortedProjects = [
4920
+ ...browserProjects
4921
+ ].sort((a, b)=>b.rootPath.length - a.rootPath.length);
4922
+ for (const project of sortedProjects)if (normalizedTestPath.startsWith(project.rootPath)) return project.provider;
4923
+ throw new Error(`Cannot resolve browser provider for test path: ${JSON.stringify(testPath)}. Known project roots: ${JSON.stringify(sortedProjects.map((p)=>p.rootPath))}`);
4924
+ };
4925
+ const runBrowserController = async (context, options)=>{
4926
+ const { allowEmptyRun = false, filesOnly = false, onTraceEvents, env, onInvalidate } = options ?? {};
4927
+ const buildStart = Date.now();
4928
+ const isWatchMode = 'watch' === context.command;
4929
+ const phaseTrackers = onTraceEvents ? new Map() : void 0;
4930
+ const trackerKey = (project, testPath)=>`${project}\u0000${testPath}`;
4931
+ const browserProjects = options?.projects ?? getBrowserProjects(context);
4932
+ const useHeadlessDirect = browserProjects.every((project)=>project.normalizedConfig.browser.headless);
4933
+ const browserSourceMapCache = new Map();
4934
+ const browserCoverageResources = {
4935
+ assetFiles: new Map(),
4936
+ sourceMaps: new Map()
4937
+ };
4938
+ const loadBrowserCoverageResources = async (filenames, resource)=>{
4939
+ const store = browserCoverageResources[resource];
4940
+ const loaded = {};
4941
+ for (const filename of filenames){
4942
+ const value = store.get(filename);
4943
+ if (void 0 !== value) loaded[filename] = value;
4944
+ }
4945
+ return loaded;
4946
+ };
4947
+ const loadBrowserCoverageAssetFiles = (filenames)=>loadBrowserCoverageResources(filenames, 'assetFiles');
4948
+ const loadBrowserCoverageSourceMaps = (filenames)=>loadBrowserCoverageResources(filenames, 'sourceMaps');
4949
+ const isHttpLikeFile = (file)=>/^https?:\/\//.test(file);
4950
+ const resolveBrowserSourcemap = async (sourcePath)=>{
4951
+ if (!isHttpLikeFile(sourcePath)) return {
4952
+ handled: false,
4953
+ sourcemap: null
4954
+ };
4955
+ const normalizedUrl = normalizeJavaScriptUrl(sourcePath);
4956
+ if (!normalizedUrl) return {
4957
+ handled: true,
4958
+ sourcemap: null
4959
+ };
4960
+ if (browserSourceMapCache.has(normalizedUrl)) return {
4961
+ handled: true,
4962
+ sourcemap: browserSourceMapCache.get(normalizedUrl) ?? null
4963
+ };
4964
+ return {
4965
+ handled: true,
4966
+ sourcemap: await loadSourceMapWithCache({
4967
+ jsUrl: normalizedUrl,
4968
+ cache: browserSourceMapCache
4969
+ })
4970
+ };
4971
+ };
4972
+ const getBrowserSourcemap = async (sourcePath)=>{
4973
+ const result = await resolveBrowserSourcemap(sourcePath);
4974
+ return result.handled ? result.sourcemap : null;
4975
+ };
4976
+ const buildErrorResult = (error, close)=>{
4977
+ const elapsed = Math.max(0, Date.now() - buildStart);
4978
+ return {
4979
+ results: [],
4980
+ testResults: [],
4981
+ duration: {
4982
+ totalTime: elapsed,
4983
+ buildTime: elapsed,
4984
+ testTime: 0
4985
+ },
4986
+ hasFailure: true,
4987
+ unhandledErrors: [
4988
+ error
4989
+ ],
4990
+ getSourcemap: getBrowserSourcemap,
4991
+ resolveSourcemap: resolveBrowserSourcemap,
4992
+ close
4993
+ };
4994
+ };
4995
+ const failWithError = async (error, cleanup)=>{
4996
+ const normalizedError = toError(error);
4997
+ if (cleanup && !isWatchMode) return buildErrorResult(normalizedError, cleanup);
4998
+ try {
4999
+ return buildErrorResult(normalizedError);
5000
+ } finally{
5001
+ await cleanup?.();
5002
+ }
5003
+ };
5004
+ const coverageConfig = browserProjects.find((project)=>project.normalizedConfig.coverage?.enabled)?.normalizedConfig.coverage;
5005
+ const coverageProvider = !filesOnly && 'list' !== context.command && coverageConfig?.enabled ? await createCoverageProvider(coverageConfig, context.rootPath) : null;
5006
+ const browserCoverageCapabilityError = filesOnly || 'list' === context.command || coverageConfig?.provider !== 'v8' || (browserProjects[0]?.normalizedConfig.browser.browser ?? 'chromium') !== 'chromium' || coverageProvider?.supportsBrowserCoverage === true ? void 0 : new Error("The installed @rstest/coverage-v8 provider does not support Chromium Browser Mode coverage. Upgrade @rstest/coverage-v8 to the version matching @rstest/core, or use coverage.provider: 'istanbul'.");
5007
+ const containerDevServerEnv = process.env.RSTEST_CONTAINER_DEV_SERVER;
5008
+ let containerDevServer;
5009
+ let containerDistPath;
5010
+ if (!useHeadlessDirect) {
5011
+ if (containerDevServerEnv) try {
5012
+ containerDevServer = new URL(containerDevServerEnv).toString();
5013
+ logger.debug(`[Browser UI] Using dev server for container: ${containerDevServer}`);
5014
+ } catch (error) {
5015
+ const originalError = toError(error);
5016
+ originalError.message = `Invalid RSTEST_CONTAINER_DEV_SERVER value: ${originalError.message}`;
5017
+ return failWithError(originalError);
5018
+ }
5019
+ if (!containerDevServer) try {
5020
+ containerDistPath = resolveContainerDist();
5021
+ } catch (error) {
5022
+ return failWithError(error);
5023
+ }
5024
+ }
5025
+ let projectEntries = await resolveProjectEntries(context, options?.shardedEntries, browserProjects);
5026
+ let totalTests = projectEntries.reduce((total, item)=>total + item.testFiles.length, 0);
5027
+ const shouldInitializeEmptyBrowserHooks = 0 === totalTests && hasUserRstestConfigPlugins(browserProjects);
5028
+ const createEmptyRunResult = ()=>{
5029
+ const elapsed = Math.max(0, Date.now() - buildStart);
5030
+ return {
5031
+ results: [],
5032
+ testResults: [],
5033
+ duration: {
5034
+ totalTime: elapsed,
5035
+ buildTime: elapsed,
5036
+ testTime: 0
5037
+ },
5038
+ hasFailure: false,
5039
+ getSourcemap: getBrowserSourcemap,
5040
+ resolveSourcemap: resolveBrowserSourcemap
5041
+ };
5042
+ };
5043
+ const writeEmptyLaunchExitCode = ()=>{
5044
+ process.exitCode = resolveEmptyLaunchExitCode(process.exitCode, {
5045
+ allowEmptyRun,
5046
+ isWatchMode,
5047
+ passWithNoTests: context.normalizedConfig.passWithNoTests
5048
+ });
5049
+ };
5050
+ if (0 === totalTests && !shouldInitializeEmptyBrowserHooks) {
5051
+ writeEmptyLaunchExitCode();
5052
+ return allowEmptyRun ? createEmptyRunResult() : void 0;
5053
+ }
5054
+ const enableCliShortcuts = isWatchMode && isTTY('stdin');
5055
+ const browserTempOutputRoot = context.normalizedConfig.output.distPath.root;
4068
5056
  const tempDir = isWatchMode && watchContext.runtime ? watchContext.runtime.tempDir : isWatchMode ? join(context.rootPath, browserTempOutputRoot, 'browser', 'watch') : join(context.rootPath, browserTempOutputRoot, 'browser', Date.now().toString());
4069
5057
  let runtime = isWatchMode ? watchContext.runtime : null;
4070
- let triggerRerun;
4071
- let awaitHeadlessRerunIdle;
5058
+ const watchSignals = createWatchSignals(onInvalidate);
4072
5059
  if (!runtime) {
4073
5060
  try {
4074
5061
  runtime = await createBrowserRuntime({
@@ -4079,12 +5066,9 @@ const runBrowserController = async (context, options)=>{
4079
5066
  freezeShardedEntries: options?.freezeShardedEntries,
4080
5067
  tempDir,
4081
5068
  isWatchMode,
4082
- onTriggerRerun: isWatchMode ? async ()=>{
4083
- await triggerRerun?.();
4084
- } : void 0,
4085
5069
  containerDistPath,
4086
5070
  containerDevServer,
4087
- skipProviderLaunch: filesOnly,
5071
+ skipProviderLaunch: filesOnly || Boolean(browserCoverageCapabilityError),
4088
5072
  appliedModifyRstestConfigEnvironments: options?.appliedModifyRstestConfigEnvironments
4089
5073
  });
4090
5074
  } catch (error) {
@@ -4095,13 +5079,18 @@ const runBrowserController = async (context, options)=>{
4095
5079
  }).catch(()=>{});
4096
5080
  });
4097
5081
  }
4098
- if (isWatchMode) {
5082
+ if (isWatchMode && !filesOnly && !browserCoverageCapabilityError) {
4099
5083
  watchContext.runtime = runtime;
4100
5084
  registerWatchCleanup(context.embedded);
4101
5085
  }
4102
5086
  }
4103
5087
  const watchState = runtime.watchState;
4104
- if (isWatchMode) watchState.lastTestFiles = collectWatchTestFiles(projectEntries);
5088
+ if (isWatchMode) {
5089
+ watchState.lastTestFiles = collectWatchTestFiles(projectEntries);
5090
+ watchState.triggerRerun = async ()=>{
5091
+ await watchSignals.runDispatchRerun();
5092
+ };
5093
+ }
4105
5094
  const seedPendingRerun = (testPaths)=>{
4106
5095
  const wanted = testPaths ? new Set(testPaths.map((testPath)=>normalize(testPath))) : null;
4107
5096
  let seeded = 0;
@@ -4114,22 +5103,11 @@ const runBrowserController = async (context, options)=>{
4114
5103
  }
4115
5104
  return seeded;
4116
5105
  };
4117
- const watchHandles = isWatchMode ? {
4118
- rerun: async (testPaths)=>{
4119
- const seeded = seedPendingRerun(testPaths);
4120
- if (testPaths && 0 === seeded) return;
4121
- await triggerRerun?.();
4122
- await awaitHeadlessRerunIdle?.();
4123
- },
4124
- close: cleanupWatchRuntime
4125
- } : void 0;
4126
- const finalizeWatchRerun = async ({ rerunTestPaths, testTime, unhandledErrors })=>{
5106
+ const buildRerunOutcome = ({ rerunTestPaths, testTime, rawCoverage, unhandledErrors })=>{
4127
5107
  const rerunPathSet = new Set(rerunTestPaths);
4128
5108
  const rerunResults = context.reporterResults.results.filter((result)=>rerunPathSet.has(result.testPath));
4129
- let rerunCoverage;
4130
5109
  const coverageMap = buildBrowserCoverageMap(rerunResults, coverageProvider);
4131
- if (coverageMap && coverageMap.files().length > 0) rerunCoverage = coverageMap.toJSON();
4132
- const outcome = {
5110
+ return {
4133
5111
  results: rerunResults,
4134
5112
  testResults: context.reporterResults.testResults.filter((result)=>rerunPathSet.has(result.testPath)),
4135
5113
  errors: unhandledErrors ?? [],
@@ -4138,21 +5116,45 @@ const runBrowserController = async (context, options)=>{
4138
5116
  buildTime: drainPendingBuildTime(watchState),
4139
5117
  testTime
4140
5118
  },
4141
- coverage: rerunCoverage ? {
4142
- map: rerunCoverage
5119
+ coverage: coverageMap?.files().length || rawCoverage.length ? {
5120
+ map: coverageMap?.toJSON(),
5121
+ raw: rawCoverage,
5122
+ loadAssetFiles: loadBrowserCoverageAssetFiles,
5123
+ loadSourceMaps: loadBrowserCoverageSourceMaps
4143
5124
  } : void 0,
4144
5125
  resolveSourcemap: resolveBrowserSourcemap
4145
5126
  };
4146
- await finalizeRunCycle(context, {
4147
- outcomes: [
4148
- outcome
4149
- ],
4150
- mode: 'on-demand',
4151
- isWatchMode: true,
4152
- coverageProvider,
4153
- reportOnFailure: coverageConfig?.reportOnFailure ?? false
4154
- });
4155
5127
  };
5128
+ const createWatchSession = (execute)=>({
5129
+ runCycle: async (testPaths)=>{
5130
+ const rerunStartTime = Date.now();
5131
+ fatalErrorRef.current = null;
5132
+ let rerunError;
5133
+ let rawCoverage = [];
5134
+ try {
5135
+ rawCoverage = await execute(testPaths);
5136
+ } catch (error) {
5137
+ rerunError = toError(error);
5138
+ }
5139
+ const rerunFatalError = fatalErrorRef.current ?? void 0;
5140
+ return buildRerunOutcome({
5141
+ rerunTestPaths: testPaths,
5142
+ testTime: Math.max(0, Date.now() - rerunStartTime),
5143
+ rawCoverage,
5144
+ unhandledErrors: rerunError ? [
5145
+ rerunError
5146
+ ] : rerunFatalError ? [
5147
+ rerunFatalError
5148
+ ] : void 0
5149
+ });
5150
+ },
5151
+ requestRerun: async (testPaths)=>{
5152
+ const seeded = seedPendingRerun(testPaths);
5153
+ if (testPaths && 0 === seeded) return;
5154
+ await watchSignals.runDispatchRerun();
5155
+ await watchSignals.awaitSignalledCycle();
5156
+ }
5157
+ });
4156
5158
  projectEntries = runtime.projectEntries;
4157
5159
  totalTests = projectEntries.reduce((total, item)=>total + item.testFiles.length, 0);
4158
5160
  const buildTime = Date.now() - buildStart;
@@ -4170,11 +5172,15 @@ const runBrowserController = async (context, options)=>{
4170
5172
  close: ()=>destroyBrowserRuntime(runtime)
4171
5173
  };
4172
5174
  if (0 === totalTests) {
4173
- reportEmptyTestSet();
5175
+ writeEmptyLaunchExitCode();
4174
5176
  await destroyBrowserRuntime(runtime);
4175
5177
  return allowEmptyRun ? createEmptyRunResult() : void 0;
4176
5178
  }
4177
- const { browser, browserLaunchOptions, wsPort, wss } = runtime;
5179
+ if (browserCoverageCapabilityError) {
5180
+ await destroyBrowserRuntime(runtime);
5181
+ return failWithError(browserCoverageCapabilityError);
5182
+ }
5183
+ const { browser, browserLaunchOptions, wsPort } = runtime;
4178
5184
  const allTestFiles = projectEntries.flatMap((entry)=>entry.testFiles.map((testPath)=>({
4179
5185
  testPath: normalize(testPath),
4180
5186
  projectName: entry.project.name
@@ -4196,17 +5202,18 @@ const runBrowserController = async (context, options)=>{
4196
5202
  name,
4197
5203
  `http://localhost:${server.port}`
4198
5204
  ]));
5205
+ const containerRunnerUrl = `http://localhost:${runtime.containerServer.port}`;
4199
5206
  const hostOptions = {
4200
5207
  rootPath: normalize(context.rootPath),
4201
5208
  projects: projectRuntimeConfigs,
4202
5209
  snapshot: {
4203
5210
  updateSnapshot: options?.updateSnapshot ?? context.snapshotManager.options.updateSnapshot
4204
5211
  },
4205
- runnerUrl: `http://localhost:${runtime.containerServer.port}`,
5212
+ runnerUrl: containerRunnerUrl,
4206
5213
  projectRunnerUrls,
4207
5214
  wsPort,
4208
5215
  debug: isDebug(),
4209
- rpcTimeout: maxTestTimeoutForRpc
5216
+ rpcTimeout: -1
4210
5217
  };
4211
5218
  const browserProviderProjects = browserProjects.map((project)=>({
4212
5219
  rootPath: normalize(project.rootPath),
@@ -4214,8 +5221,26 @@ const runBrowserController = async (context, options)=>{
4214
5221
  }));
4215
5222
  const implementationByProvider = new Map();
4216
5223
  for (const browserProject of browserProviderProjects)if (!implementationByProvider.has(browserProject.provider)) implementationByProvider.set(browserProject.provider, getBrowserProviderImplementation(browserProject.provider));
4217
- let activeContainerPage = null;
4218
- let getHeadlessRunnerPageBySessionId;
5224
+ const browserName = browserLaunchOptions.browser ?? 'chromium';
5225
+ const v8CoverageCollector = coverageConfig?.provider === 'v8' ? implementationByProvider.get(browserLaunchOptions.provider)?.createV8CoverageCollector?.({
5226
+ browserName
5227
+ }) ?? null : null;
5228
+ const v8Coverage = v8CoverageCollector ? {
5229
+ start: v8CoverageCollector.start,
5230
+ take: (page, projectRoot, projectName)=>takeBrowserV8Coverage({
5231
+ collector: v8CoverageCollector,
5232
+ fetchTimeout: maxTestTimeoutForRpc,
5233
+ page,
5234
+ projectUrl: projectRunnerUrls[projectName ?? ''] ?? containerRunnerUrl,
5235
+ rootPath: normalize(projectRoot),
5236
+ sourceMapCache: browserSourceMapCache,
5237
+ resourceStore: browserCoverageResources
5238
+ })
5239
+ } : void 0;
5240
+ let resolveDispatchPages = ()=>({});
5241
+ const setDispatchPageResolver = (resolver)=>{
5242
+ resolveDispatchPages = resolver;
5243
+ };
4219
5244
  const dispatchBrowserRpcRequest = async ({ request, target })=>{
4220
5245
  const timeoutFallbackMs = maxTestTimeoutForRpc;
4221
5246
  const provider = resolveProviderForTestPath({
@@ -4224,12 +5249,12 @@ const runBrowserController = async (context, options)=>{
4224
5249
  });
4225
5250
  const implementation = implementationByProvider.get(provider);
4226
5251
  if (!implementation) throw new Error(`Browser provider implementation not found: ${provider}`);
4227
- const runnerPage = target?.sessionId ? getHeadlessRunnerPageBySessionId?.(target.sessionId) : void 0;
5252
+ const { runnerPage, containerPage } = resolveDispatchPages(target);
4228
5253
  if (target?.sessionId && !runnerPage) throw new Error(`Runner page session not found for browser dispatch: ${target.sessionId}`);
4229
- if (!runnerPage && !activeContainerPage) throw new Error('Browser container page is not initialized');
5254
+ if (!runnerPage && !containerPage) throw new Error('Browser container page is not initialized');
4230
5255
  try {
4231
5256
  return await implementation.dispatchRpc({
4232
- containerPage: runnerPage ? void 0 : activeContainerPage ?? void 0,
5257
+ containerPage: runnerPage ? void 0 : containerPage,
4233
5258
  runnerPage,
4234
5259
  request,
4235
5260
  timeoutFallbackMs
@@ -4249,7 +5274,9 @@ const runBrowserController = async (context, options)=>{
4249
5274
  runtime.setContainerOptions(hostOptions);
4250
5275
  const reporterResults = [];
4251
5276
  const caseResults = [];
4252
- let fatalError = null;
5277
+ const fatalErrorRef = {
5278
+ current: null
5279
+ };
4253
5280
  const runnerSinks = new Map(browserProjects.map((project)=>[
4254
5281
  project.name,
4255
5282
  createRunnerEventSink(context, project.normalizedConfig)
@@ -4259,14 +5286,22 @@ const runBrowserController = async (context, options)=>{
4259
5286
  if (!sink) throw new Error(`No runner event sink for project "${projectName}"`);
4260
5287
  return sink;
4261
5288
  };
4262
- const silentConsoleController = createSilentConsoleController({
4263
- runtimeConfig: {
4264
- silent: context.normalizedConfig.silent,
4265
- disableConsoleIntercept: context.normalizedConfig.disableConsoleIntercept
4266
- },
4267
- emitInterceptedLog: (log)=>sinkForProjectName(log.project).onConsoleLog(log),
4268
- writeOriginalLog: ()=>{}
4269
- });
5289
+ const silentConsoleControllers = new Map(browserProjects.map((project)=>[
5290
+ project.name,
5291
+ createSilentConsoleController({
5292
+ runtimeConfig: {
5293
+ silent: project.normalizedConfig.silent,
5294
+ disableConsoleIntercept: project.normalizedConfig.disableConsoleIntercept
5295
+ },
5296
+ emitInterceptedLog: (log)=>sinkForProjectName(log.project).onConsoleLog(log),
5297
+ writeOriginalLog: ()=>{}
5298
+ })
5299
+ ]));
5300
+ const silentConsoleControllerForProjectName = (projectName)=>{
5301
+ const controller = silentConsoleControllers.get(projectName);
5302
+ if (!controller) throw new Error(`No silent console controller for project "${projectName}"`);
5303
+ return controller;
5304
+ };
4270
5305
  const snapshotRpcMethods = {
4271
5306
  async resolveSnapshotPath (testPath) {
4272
5307
  return resolveSnapshotPathDefault(testPath, context.normalizedConfig.resolveSnapshotPath);
@@ -4321,7 +5356,7 @@ const runBrowserController = async (context, options)=>{
4321
5356
  const handleTestSuiteResult = async (payload)=>{
4322
5357
  phaseTrackers?.get(trackerKey(payload.project, payload.testPath))?.recordSuiteResult(payload);
4323
5358
  await sinkForProjectName(payload.project).onTestSuiteResult(payload);
4324
- if ('passed-only' === context.normalizedConfig.silent) silentConsoleController.flushBufferedLogsForTask({
5359
+ silentConsoleControllerForProjectName(payload.project).flushBufferedLogsForTask({
4325
5360
  taskId: payload.testId,
4326
5361
  status: payload.status,
4327
5362
  taskParentNames: payload.parentNames,
@@ -4337,7 +5372,7 @@ const runBrowserController = async (context, options)=>{
4337
5372
  caseResults.push(payload);
4338
5373
  phaseTrackers?.get(trackerKey(payload.project, payload.testPath))?.recordCaseResult(payload);
4339
5374
  await sinkForProjectName(payload.project).onTestCaseResult(payload);
4340
- if ('passed-only' === context.normalizedConfig.silent) silentConsoleController.flushBufferedLogsForTask({
5375
+ silentConsoleControllerForProjectName(payload.project).flushBufferedLogsForTask({
4341
5376
  taskId: payload.testId,
4342
5377
  status: payload.status,
4343
5378
  taskParentNames: payload.parentNames,
@@ -4360,7 +5395,7 @@ const runBrowserController = async (context, options)=>{
4360
5395
  phaseTrackers.delete(key);
4361
5396
  }
4362
5397
  }
4363
- if ('passed-only' === context.normalizedConfig.silent) silentConsoleController.flushBufferedLogsForTask({
5398
+ silentConsoleControllerForProjectName(payload.project).flushBufferedLogsForTask({
4364
5399
  taskId: payload.testId,
4365
5400
  status: payload.status,
4366
5401
  taskParentNames: payload.parentNames,
@@ -4368,7 +5403,6 @@ const runBrowserController = async (context, options)=>{
4368
5403
  testPath: payload.testPath
4369
5404
  });
4370
5405
  await sinkForProjectName(payload.project).onTestFileResult(payload);
4371
- if (isWatchMode && 'fail' === payload.status) ensureProcessExitCode(1);
4372
5406
  };
4373
5407
  const handleLog = async (payload)=>{
4374
5408
  const log = {
@@ -4383,13 +5417,12 @@ const runBrowserController = async (context, options)=>{
4383
5417
  type: payload.type,
4384
5418
  trace: payload.trace
4385
5419
  };
4386
- silentConsoleController.onConsoleLog(log);
5420
+ silentConsoleControllerForProjectName(payload.projectName).onConsoleLog(log);
4387
5421
  };
4388
5422
  const handleFatal = async (payload)=>{
4389
5423
  const error = new Error(payload.message);
4390
5424
  error.stack = payload.stack;
4391
- fatalError = error;
4392
- if (isWatchMode) ensureProcessExitCode(1);
5425
+ fatalErrorRef.current = error;
4393
5426
  };
4394
5427
  const runSnapshotRpc = async (request)=>{
4395
5428
  switch(request.method){
@@ -4427,637 +5460,73 @@ const runBrowserController = async (context, options)=>{
4427
5460
  logger.debug(`[Dispatch] Skip registering dispatch namespace "${namespace}" because it is already reserved`);
4428
5461
  }
4429
5462
  });
4430
- if (useHeadlessDirect) {
4431
- const viewportByProject = mapViewportByProject(projectRuntimeConfigs);
4432
- const runLifecycle = new RunSessionLifecycle();
4433
- const sessionRegistry = new RunnerSessionRegistry();
4434
- getHeadlessRunnerPageBySessionId = (sessionId)=>sessionRegistry.getById(sessionId)?.page;
4435
- let dispatchRequestCounter = 0;
4436
- const nextDispatchRequestId = (namespace)=>`${namespace}-${++dispatchRequestCounter}`;
4437
- const closeContextSafely = async (browserContext)=>{
4438
- try {
4439
- await browserContext.close();
4440
- } catch {}
4441
- };
4442
- const cancelRun = async (run, waitForDone = true)=>{
4443
- await runLifecycle.cancel(run, {
4444
- waitForDone,
4445
- onCancel: async (session)=>{
4446
- await Promise.all(Array.from(session.contexts).map((browserContext)=>closeContextSafely(browserContext)));
4447
- }
4448
- });
4449
- };
4450
- const dispatchRouter = createDispatchRouter({
4451
- isRunTokenStale: (runToken)=>runLifecycle.isTokenStale(runToken),
4452
- onStale: (request)=>{
4453
- if ("runner" === request.namespace) logger.debug(`[Headless] Dropped stale message "${request.method}" for ${request.target?.testFile ?? 'unknown'}`);
4454
- }
4455
- });
4456
- const dispatchRunnerMessage = async (run, file, sessionId, message)=>{
4457
- const response = await dispatchRouter.dispatch({
4458
- requestId: nextDispatchRequestId("runner"),
4459
- runToken: run.token,
4460
- namespace: "runner",
4461
- method: message.type,
4462
- args: 'payload' in message ? message.payload : void 0,
4463
- target: {
4464
- sessionId,
4465
- testFile: file.testPath,
4466
- projectName: file.projectName
4467
- }
4468
- });
4469
- if (response.stale) return;
4470
- if (response.error) throw new Error(response.error);
4471
- };
4472
- const runSingleFile = async (run, file)=>{
4473
- if (run.cancelled || runLifecycle.isTokenStale(run.token)) return;
4474
- const viewport = viewportByProject.get(file.projectName);
4475
- const browserContext = await browser.newContext({
4476
- providerOptions: browserLaunchOptions.providerOptions,
4477
- viewport: viewport ?? null
4478
- });
4479
- run.contexts.add(browserContext);
4480
- let page = null;
4481
- let sessionId = null;
4482
- let settled = false;
4483
- let resolveDone = null;
4484
- const markDone = ()=>{
4485
- if (!settled) {
4486
- settled = true;
4487
- resolveDone?.();
4488
- }
4489
- };
4490
- const donePromise = new Promise((resolve)=>{
4491
- resolveDone = resolve;
4492
- });
4493
- const crashDeferred = createDeferredPromise();
4494
- const onPageDead = (reason)=>{
4495
- if (settled || run.cancelled || !runLifecycle.isTokenActive(run.token)) return;
4496
- settled = true;
4497
- crashDeferred.resolve(reason);
4498
- };
4499
- try {
4500
- page = await browserContext.newPage();
4501
- page.on('crash', ()=>onPageDead(`Browser page crashed while running ${file.testPath}.`));
4502
- page.on('close', ()=>onPageDead(`Browser page closed unexpectedly while running ${file.testPath}.`));
4503
- const session = sessionRegistry.register({
4504
- testFile: file.testPath,
4505
- projectName: file.projectName,
4506
- runToken: run.token,
4507
- mode: 'headless-page',
4508
- context: browserContext,
4509
- page
4510
- });
4511
- sessionId = session.id;
4512
- await attachHeadlessRunnerTransport(page, {
4513
- onDispatchMessage: async (message)=>{
4514
- try {
4515
- await dispatchRunnerMessage(run, file, session.id, message);
4516
- if ('file-complete' === message.type || 'complete' === message.type) markDone();
4517
- else if ('fatal' === message.type) {
4518
- markDone();
4519
- await cancelRun(run, false);
4520
- }
4521
- } catch (error) {
4522
- const formatted = toError(error);
4523
- await handleFatal({
4524
- message: formatted.message,
4525
- stack: formatted.stack
4526
- });
4527
- markDone();
4528
- await cancelRun(run, false);
4529
- }
4530
- },
4531
- onDispatchRpc: async (request)=>dispatchRouter.dispatch({
4532
- ...request,
4533
- runToken: run.token,
4534
- target: {
4535
- sessionId: session.id,
4536
- testFile: file.testPath,
4537
- projectName: file.projectName,
4538
- ...request.target
4539
- }
4540
- })
4541
- });
4542
- const inlineOptions = {
4543
- ...hostOptions,
4544
- snapshot: {
4545
- updateSnapshot: context.snapshotManager.options.updateSnapshot
4546
- },
4547
- testFile: file.testPath,
4548
- runId: `${run.token}:${session.id}`
4549
- };
4550
- const serializedOptions = serializeForInlineScript(inlineOptions);
4551
- await page.addInitScript(`window.__RSTEST_BROWSER_OPTIONS__ = ${serializedOptions};`);
4552
- const projectServer = runtime.projectServers.get(file.projectName);
4553
- if (!projectServer) throw new Error(`No browser dev server for project "${file.projectName}" (test file: ${file.testPath}).`);
4554
- await page.goto(`http://localhost:${projectServer.port}/runner.html`, {
4555
- waitUntil: 'load'
4556
- });
4557
- const state = await Promise.race([
4558
- donePromise.then(()=>({
4559
- type: 'done'
4560
- })),
4561
- crashDeferred.promise.then((reason)=>({
4562
- type: 'crash',
4563
- reason
4564
- })),
4565
- run.cancelSignal.then(()=>({
4566
- type: 'cancelled'
4567
- }))
4568
- ]);
4569
- if ('cancelled' === state.type) return;
4570
- if ('crash' === state.type && runLifecycle.isTokenActive(run.token) && !run.cancelled) {
4571
- await handleFatal({
4572
- message: state.reason
4573
- });
4574
- await cancelRun(run, false);
4575
- }
4576
- } catch (error) {
4577
- if (runLifecycle.isTokenActive(run.token) && !run.cancelled) {
4578
- const formatted = toError(error);
4579
- await handleFatal({
4580
- message: formatted.message,
4581
- stack: formatted.stack
4582
- });
4583
- await cancelRun(run, false);
4584
- }
4585
- } finally{
4586
- if (page) try {
4587
- await page.close();
4588
- } catch {}
4589
- if (sessionId) sessionRegistry.deleteById(sessionId);
4590
- run.contexts.delete(browserContext);
4591
- await closeContextSafely(browserContext);
4592
- }
4593
- };
4594
- const makeSkippedFileResult = (file)=>({
4595
- testId: getFileTaskId(file.testPath),
4596
- status: 'skip',
4597
- name: '',
4598
- testPath: file.testPath,
4599
- project: file.projectName,
4600
- results: []
4601
- });
4602
- const runFilesWithPool = async (files)=>{
4603
- if (0 === files.length) return;
4604
- const previous = runLifecycle.activeSession;
4605
- if (previous) await cancelRun(previous);
4606
- const run = runLifecycle.createSession((token)=>({
4607
- ...createRunSession(token),
4608
- contexts: new Set()
4609
- }));
4610
- const queue = [
4611
- ...files
4612
- ];
4613
- const concurrency = getHeadlessConcurrency(context, queue.length);
4614
- const bail = context.normalizedConfig.bail;
4615
- const worker = async ()=>{
4616
- while(queue.length > 0 && !run.cancelled && runLifecycle.isTokenActive(run.token)){
4617
- if (bail && context.stateManager.getCountOfFailedTests() >= bail) {
4618
- let skipped = queue.shift();
4619
- while(skipped){
4620
- await handleTestFileComplete(makeSkippedFileResult(skipped));
4621
- skipped = queue.shift();
4622
- }
4623
- return;
4624
- }
4625
- const next = queue.shift();
4626
- if (!next) return;
4627
- await runSingleFile(run, next);
4628
- }
4629
- };
4630
- run.done = Promise.all(Array.from({
4631
- length: Math.min(queue.length, Math.max(concurrency, 1))
4632
- }, ()=>worker())).then(()=>{});
4633
- await run.done;
4634
- runLifecycle.clearIfActive(run);
4635
- };
4636
- const latestRerunScheduler = createHeadlessLatestRerunScheduler({
4637
- getActiveRun: ()=>runLifecycle.activeSession,
4638
- isRunCancelled: (run)=>run.cancelled,
4639
- invalidateActiveRun: ()=>{
4640
- runLifecycle.invalidateActiveToken();
4641
- },
4642
- interruptActiveRun: async (run)=>{
4643
- await cancelRun(run, false);
4644
- },
4645
- runFiles: async (files)=>{
4646
- prepareWatchRerunState(context);
4647
- await notifyTestRunStart();
4648
- const rerunStartTime = Date.now();
4649
- const fatalErrorBeforeRun = fatalError;
4650
- let rerunError;
4651
- try {
4652
- await runFilesWithPool(files);
4653
- } catch (error) {
4654
- rerunError = toError(error);
4655
- throw error;
4656
- } finally{
4657
- const testTime = Math.max(0, Date.now() - rerunStartTime);
4658
- const rerunFatalError = fatalError && fatalError !== fatalErrorBeforeRun ? fatalError : void 0;
4659
- await finalizeWatchRerun({
4660
- rerunTestPaths: files.map((file)=>file.testPath),
4661
- testTime,
4662
- unhandledErrors: rerunError ? [
4663
- rerunError
4664
- ] : rerunFatalError ? [
4665
- rerunFatalError
4666
- ] : void 0
4667
- });
4668
- logWatchReadyMessage(context, enableCliShortcuts);
4669
- }
4670
- },
4671
- onError: async (error)=>{
4672
- const formatted = toError(error);
4673
- await handleFatal({
4674
- message: formatted.message,
4675
- stack: formatted.stack
4676
- });
4677
- },
4678
- onInterrupt: (run)=>{
4679
- logger.debug(`[Headless] Interrupting active run token ${run.token} before scheduling latest rerun`);
4680
- }
4681
- });
4682
- awaitHeadlessRerunIdle = ()=>latestRerunScheduler.whenIdle();
4683
- const testStart = Date.now();
4684
- await runFilesWithPool(allTestFiles);
4685
- const testTime = Date.now() - testStart;
4686
- if (isWatchMode) triggerRerun = async ()=>{
4687
- const newProjectEntries = await collectProjectEntries(context);
4688
- const rerunPlan = planWatchRerun({
4689
- projectEntries: newProjectEntries,
4690
- previousTestFiles: watchState.lastTestFiles,
4691
- affectedTestFiles: drainPendingAffectedTestFiles(watchState)
4692
- });
4693
- if (rerunPlan.filesChanged) {
4694
- const deletedTestPaths = collectDeletedTestPaths(watchState.lastTestFiles, rerunPlan.currentTestFiles);
4695
- if (deletedTestPaths.length > 0) context.updateReporterResultState([], [], deletedTestPaths);
4696
- watchState.lastTestFiles = rerunPlan.currentTestFiles;
4697
- if (0 === rerunPlan.currentTestFiles.length) {
4698
- await latestRerunScheduler.enqueueLatest([]);
4699
- logger.log(color.cyan('No browser test files remain after update.\n'));
4700
- logWatchReadyMessage(context, enableCliShortcuts);
4701
- return;
4702
- }
4703
- logger.log(color.cyan(`Test file set changed, re-running ${rerunPlan.currentTestFiles.length} file(s)...\n`));
4704
- await latestRerunScheduler.enqueueLatest(rerunPlan.currentTestFiles);
4705
- return;
4706
- }
4707
- if (0 === rerunPlan.affectedTestFiles.length) {
4708
- logger.log(color.cyan('No affected browser test files detected, skipping re-run.\n'));
4709
- logWatchReadyMessage(context, enableCliShortcuts);
4710
- return;
4711
- }
4712
- logger.log(color.cyan(`Re-running ${rerunPlan.affectedTestFiles.length} affected test file(s)...\n`));
4713
- await latestRerunScheduler.enqueueLatest(rerunPlan.affectedTestFiles);
4714
- };
4715
- const closeHeadlessRuntime = isWatchMode ? void 0 : async ()=>{
4716
- sessionRegistry.clear();
4717
- await destroyBrowserRuntime(runtime);
4718
- };
4719
- if (fatalError) return failWithError(fatalError, closeHeadlessRuntime);
4720
- const duration = {
4721
- totalTime: buildTime + testTime,
4722
- buildTime,
4723
- testTime
4724
- };
4725
- context.updateReporterResultState(reporterResults, caseResults);
4726
- const isFailure = reporterResults.some((result)=>'fail' === result.status);
4727
- if (isWatchMode && isFailure) ensureProcessExitCode(1);
4728
- const cycleCoverageMap = isWatchMode && coverageProvider ? buildBrowserCoverageMap(reporterResults, coverageProvider) : void 0;
4729
- const result = {
4730
- results: reporterResults,
4731
- testResults: caseResults,
4732
- duration,
4733
- hasFailure: isFailure,
4734
- getSourcemap: getBrowserSourcemap,
4735
- resolveSourcemap: resolveBrowserSourcemap,
4736
- close: closeHeadlessRuntime,
4737
- coverage: cycleCoverageMap,
4738
- watch: watchHandles
4739
- };
4740
- if (isWatchMode) try {
4741
- await notifyTestRunEnd({
4742
- duration,
4743
- coverage: cycleCoverageMap?.files().length ? cycleCoverageMap.toJSON() : void 0
4744
- });
4745
- } finally{
4746
- await closeHeadlessRuntime?.();
4747
- }
4748
- if (isWatchMode && triggerRerun) {
4749
- watchState.hooksEnabled = true;
4750
- logWatchReadyMessage(context, enableCliShortcuts);
4751
- }
4752
- return result;
4753
- }
4754
- let currentTestFiles = allTestFiles;
4755
- const RUNNER_FRAMES_READY_TIMEOUT_MS = 30000;
4756
- let currentRunnerFramesSignature = null;
4757
- const runnerFramesWaiters = new Map();
4758
- const createTestFilesSignature = (testFiles)=>JSON.stringify(testFiles.map((testFile)=>normalize(testFile)));
4759
- const markRunnerFramesReady = (testFiles)=>{
4760
- const signature = createTestFilesSignature(testFiles);
4761
- currentRunnerFramesSignature = signature;
4762
- const waiters = runnerFramesWaiters.get(signature);
4763
- if (!waiters) return;
4764
- runnerFramesWaiters.delete(signature);
4765
- for (const waiter of waiters)waiter();
4766
- };
4767
- const waitForRunnerFramesReady = async (testFiles)=>{
4768
- const signature = createTestFilesSignature(testFiles);
4769
- if (currentRunnerFramesSignature === signature) return;
4770
- await new Promise((resolve, reject)=>{
4771
- const waiters = runnerFramesWaiters.get(signature) ?? new Set();
4772
- const cleanup = ()=>{
4773
- const currentWaiters = runnerFramesWaiters.get(signature);
4774
- if (!currentWaiters) return;
4775
- currentWaiters.delete(onReady);
4776
- if (0 === currentWaiters.size) runnerFramesWaiters.delete(signature);
4777
- };
4778
- const onReady = ()=>{
4779
- if (timeoutId) clearTimeout(timeoutId);
4780
- cleanup();
4781
- resolve();
4782
- };
4783
- const timeoutId = setTimeout(()=>{
4784
- cleanup();
4785
- reject(new Error(`Timed out waiting for headed runner frames to be ready for ${testFiles.length} file(s).`));
4786
- }, RUNNER_FRAMES_READY_TIMEOUT_MS);
4787
- waiters.add(onReady);
4788
- runnerFramesWaiters.set(signature, waiters);
4789
- if (currentRunnerFramesSignature === signature) onReady();
4790
- });
4791
- };
4792
- const getTestFileInfo = (testFile)=>{
4793
- const normalizedTestFile = normalize(testFile);
4794
- const fileInfo = currentTestFiles.find((file)=>file.testPath === normalizedTestFile);
4795
- if (!fileInfo) throw new Error(`Unknown browser test file: ${JSON.stringify(testFile)}`);
4796
- return fileInfo;
4797
- };
4798
- let containerContext;
4799
- let containerPage;
4800
- let isNewPage = false;
4801
- if (isWatchMode && runtime.containerPage && runtime.containerContext) {
4802
- containerContext = runtime.containerContext;
4803
- containerPage = runtime.containerPage;
4804
- logger.log(color.gray('\n[Watch] Reusing existing container page\n'));
4805
- } else {
4806
- isNewPage = true;
4807
- containerContext = await browser.newContext({
4808
- providerOptions: browserLaunchOptions.providerOptions,
4809
- viewport: null
4810
- });
4811
- containerPage = await containerContext.newPage();
4812
- containerPage.on('popup', async (popup)=>{
4813
- await popup.close().catch(()=>{});
4814
- });
4815
- containerContext.on('page', async (page)=>{
4816
- if (page !== containerPage) await page.close().catch(()=>{});
4817
- });
4818
- if (isWatchMode) {
4819
- runtime.containerPage = containerPage;
4820
- runtime.containerContext = containerContext;
4821
- }
4822
- containerPage.on('console', (msg)=>{
4823
- const text = msg.text();
4824
- if (text.startsWith('[Container]') || text.startsWith('[Runner]')) logger.log(color.gray(`[Browser Console] ${text}`));
4825
- });
4826
- }
4827
- activeContainerPage = containerPage;
4828
- const dispatchRouter = createDispatchRouter();
4829
- const headedReloadQueue = createHeadedSerialTaskQueue();
4830
- const pendingHeadedReloads = new Map();
4831
- let enqueueHeadedReload = async (_file, _testNamePattern)=>{
4832
- throw new Error('Headed reload queue is not initialized');
4833
- };
4834
- const rejectPendingHeadedReload = (testPath, error, runId)=>{
4835
- const pending = pendingHeadedReloads.get(testPath);
4836
- if (!pending) return;
4837
- if (runId && pending.runId !== runId) return;
4838
- pendingHeadedReloads.delete(testPath);
4839
- pending.deferred.reject(error);
4840
- };
4841
- const rejectAllPendingHeadedReloads = (error)=>{
4842
- for (const [testPath, pending] of pendingHeadedReloads){
4843
- pendingHeadedReloads.delete(testPath);
4844
- pending.deferred.reject(error);
4845
- }
4846
- };
4847
- const registerPendingHeadedReload = (testPath, runId)=>{
4848
- const previousPending = pendingHeadedReloads.get(testPath);
4849
- if (previousPending) {
4850
- previousPending.deferred.reject(new Error(`Reload for "${testPath}" was superseded by a newer request.`));
4851
- pendingHeadedReloads.delete(testPath);
4852
- }
4853
- const deferred = createDeferredPromise();
4854
- pendingHeadedReloads.set(testPath, {
4855
- runId,
4856
- deferred
4857
- });
4858
- return deferred.promise;
5463
+ const schedulerDeps = {
5464
+ context,
5465
+ allTestFiles,
5466
+ hostOptions,
5467
+ projectRoots: new Map(projectRuntimeConfigs.map((project)=>[
5468
+ project.name,
5469
+ project.projectRoot
5470
+ ])),
5471
+ isWatchMode,
5472
+ createDispatchRouter,
5473
+ fatalErrorRef,
5474
+ watchSignals,
5475
+ setDispatchPageResolver,
5476
+ createWatchSession,
5477
+ collectProjectEntries: ()=>browserRsbuild_collectProjectEntries(context),
5478
+ logWatchReady: ()=>logWatchReadyMessage(context, enableCliShortcuts),
5479
+ destroyRuntime: ()=>destroyBrowserRuntime(runtime)
4859
5480
  };
4860
- const resolvePendingHeadedReload = (testPath, runId)=>{
4861
- const pending = pendingHeadedReloads.get(testPath);
4862
- if (!pending) return;
4863
- if (runId && pending.runId !== runId) return void logger.debug(`[Browser UI] Ignoring stale file-complete for ${testPath}. current=${pending.runId}, incoming=${runId}`);
4864
- pendingHeadedReloads.delete(testPath);
4865
- pending.deferred.resolve();
4866
- };
4867
- const reloadTestFileAndWait = async (file, testNamePattern)=>{
4868
- let reloadAck;
4869
- try {
4870
- reloadAck = await rpcManager.reloadTestFile(file.testPath, testNamePattern);
4871
- await registerPendingHeadedReload(file.testPath, reloadAck.runId);
4872
- } catch (error) {
4873
- if (reloadAck?.runId) rejectPendingHeadedReload(file.testPath, toError(error), reloadAck.runId);
4874
- throw error;
5481
+ const { testTime, rawCoverage, watchSession, close } = useHeadlessDirect ? await createHeadlessScheduler({
5482
+ ...schedulerDeps,
5483
+ browser,
5484
+ browserLaunchOptions,
5485
+ projectServers: runtime.projectServers,
5486
+ v8Coverage,
5487
+ projectRuntimeConfigs,
5488
+ watchState,
5489
+ handlers: {
5490
+ handleFatal,
5491
+ handleTestFileComplete
4875
5492
  }
4876
- };
4877
- const createRpcMethods = ()=>({
4878
- async rerunTest (testFile, testNamePattern) {
4879
- const projectName = context.normalizedConfig.name || 'project';
4880
- const relativePath = relative(context.rootPath, testFile);
4881
- const displayPath = `<${projectName}>/${relativePath}`;
4882
- logger.log(color.cyan(`\nRe-running test: ${displayPath}${testNamePattern ? ` (pattern: ${testNamePattern})` : ''}\n`));
4883
- await enqueueHeadedReload(getTestFileInfo(testFile), testNamePattern);
4884
- },
4885
- async getTestFiles () {
4886
- return currentTestFiles;
4887
- },
4888
- async onRunnerFramesReady (testFiles) {
4889
- markRunnerFramesReady(testFiles);
4890
- },
4891
- async onTestFileStart (payload) {
4892
- await handleTestFileStart(payload);
4893
- },
4894
- async onTestCaseResult (payload) {
4895
- await handleTestCaseResult(payload);
4896
- },
4897
- async onTestFileComplete (payload) {
4898
- try {
4899
- await handleTestFileComplete(payload);
4900
- resolvePendingHeadedReload(payload.testPath, payload.runId);
4901
- } catch (error) {
4902
- rejectPendingHeadedReload(payload.testPath, toError(error), payload.runId);
4903
- throw error;
4904
- }
4905
- },
4906
- async onLog (payload) {
4907
- await handleLog(payload);
4908
- },
4909
- async onFatal (payload) {
4910
- const error = new Error(payload.message);
4911
- error.stack = payload.stack;
4912
- rejectAllPendingHeadedReloads(error);
4913
- await handleFatal(payload);
4914
- },
4915
- async dispatch (request) {
4916
- return dispatchRouter.dispatch(request);
4917
- }
4918
- });
4919
- let rpcManager;
4920
- if (isWatchMode && runtime.rpcManager) {
4921
- rpcManager = runtime.rpcManager;
4922
- rpcManager.updateMethods(createRpcMethods(), rejectAllPendingHeadedReloads);
4923
- const existingWs = rpcManager.currentWebSocket;
4924
- if (existingWs) rpcManager.reattach(existingWs);
4925
- } else {
4926
- rpcManager = new ContainerRpcManager(wss, createRpcMethods(), rejectAllPendingHeadedReloads);
4927
- if (isWatchMode) runtime.rpcManager = rpcManager;
4928
- }
4929
- if (isNewPage) {
4930
- const pagePath = '/';
4931
- const containerPort = runtime.containerServer.port;
4932
- await containerPage.goto(`http://localhost:${containerPort}${pagePath}`, {
4933
- waitUntil: 'load'
4934
- });
4935
- logger.log(color.cyan(`\nBrowser mode opened at http://localhost:${containerPort}${pagePath}\n`));
4936
- }
4937
- enqueueHeadedReload = async (file, testNamePattern)=>headedReloadQueue.enqueue(async ()=>{
4938
- if (fatalError) return;
4939
- await reloadTestFileAndWait(file, testNamePattern);
4940
- });
4941
- let testTime = 0;
4942
- if (currentTestFiles.length > 0) {
4943
- const testStart = Date.now();
4944
- try {
4945
- await waitForRunnerFramesReady(currentTestFiles.map((file)=>file.testPath));
4946
- for (const file of currentTestFiles){
4947
- await enqueueHeadedReload(file);
4948
- if (fatalError) break;
4949
- }
4950
- } catch (error) {
4951
- fatalError = fatalError ?? toError(error);
4952
- if (isWatchMode) ensureProcessExitCode(1);
5493
+ }) : await createHeadedScheduler({
5494
+ ...schedulerDeps,
5495
+ runtime,
5496
+ v8Coverage,
5497
+ handlers: {
5498
+ handleTestFileComplete
4953
5499
  }
4954
- testTime = Date.now() - testStart;
4955
- }
4956
- if (isWatchMode) triggerRerun = async ()=>{
4957
- const refreshedHostOptions = {
4958
- ...hostOptions,
4959
- snapshot: {
4960
- updateSnapshot: context.snapshotManager.options.updateSnapshot
4961
- }
5500
+ });
5501
+ watchState.hooksEnabled = void 0 !== watchSession;
5502
+ if (fatalErrorRef.current) {
5503
+ const errorResult = await failWithError(fatalErrorRef.current, close);
5504
+ return {
5505
+ ...errorResult,
5506
+ rawCoverage,
5507
+ loadAssetFiles: loadBrowserCoverageAssetFiles,
5508
+ loadSourceMaps: loadBrowserCoverageSourceMaps,
5509
+ watchSession
4962
5510
  };
4963
- runtime.setContainerOptions(refreshedHostOptions);
4964
- const [, newProjectEntries] = await Promise.all([
4965
- rpcManager.updateHostConfig(refreshedHostOptions),
4966
- collectProjectEntries(context)
4967
- ]);
4968
- const rerunPlan = planWatchRerun({
4969
- projectEntries: newProjectEntries,
4970
- previousTestFiles: watchState.lastTestFiles,
4971
- affectedTestFiles: drainPendingAffectedTestFiles(watchState)
4972
- });
4973
- if (rerunPlan.filesChanged) {
4974
- const deletedTestPaths = collectDeletedTestPaths(watchState.lastTestFiles, rerunPlan.currentTestFiles);
4975
- if (deletedTestPaths.length > 0) context.updateReporterResultState([], [], deletedTestPaths);
4976
- watchState.lastTestFiles = rerunPlan.currentTestFiles;
4977
- currentTestFiles = rerunPlan.currentTestFiles;
4978
- await rpcManager.notifyTestFileUpdate(currentTestFiles);
4979
- if (0 === currentTestFiles.length) {
4980
- logger.log(color.cyan('No browser test files remain after update.\n'));
4981
- logWatchReadyMessage(context, enableCliShortcuts);
4982
- return;
4983
- }
4984
- await waitForRunnerFramesReady(currentTestFiles.map((file)=>file.testPath));
4985
- }
4986
- if (rerunPlan.normalizedAffectedTestFiles.length > 0) {
4987
- logger.log(color.cyan(`Re-running ${rerunPlan.normalizedAffectedTestFiles.length} affected test file(s)...\n`));
4988
- prepareWatchRerunState(context);
4989
- await notifyTestRunStart();
4990
- const rerunStartTime = Date.now();
4991
- const fatalErrorBeforeRun = fatalError;
4992
- let rerunError;
4993
- try {
4994
- for (const testFile of rerunPlan.normalizedAffectedTestFiles)await enqueueHeadedReload(getTestFileInfo(testFile));
4995
- } catch (error) {
4996
- rerunError = toError(error);
4997
- throw error;
4998
- } finally{
4999
- const testTime = Math.max(0, Date.now() - rerunStartTime);
5000
- const rerunFatalError = fatalError && fatalError !== fatalErrorBeforeRun ? fatalError : void 0;
5001
- await finalizeWatchRerun({
5002
- rerunTestPaths: rerunPlan.normalizedAffectedTestFiles,
5003
- testTime,
5004
- unhandledErrors: rerunError ? [
5005
- rerunError
5006
- ] : rerunFatalError ? [
5007
- rerunFatalError
5008
- ] : void 0
5009
- });
5010
- logWatchReadyMessage(context, enableCliShortcuts);
5011
- }
5012
- } else if (rerunPlan.filesChanged) logWatchReadyMessage(context, enableCliShortcuts);
5013
- else {
5014
- logger.log(color.cyan('Tests will be re-executed automatically\n'));
5015
- logWatchReadyMessage(context, enableCliShortcuts);
5016
- }
5017
- };
5018
- const closeContainerRuntime = isWatchMode ? void 0 : async ()=>{
5019
- try {
5020
- await containerPage.close();
5021
- } catch {}
5022
- try {
5023
- await containerContext.close();
5024
- } catch {}
5025
- await destroyBrowserRuntime(runtime);
5026
- };
5027
- if (fatalError) return failWithError(fatalError, closeContainerRuntime);
5028
- const duration = {
5029
- totalTime: buildTime + testTime,
5030
- buildTime,
5031
- testTime
5032
- };
5511
+ }
5033
5512
  context.updateReporterResultState(reporterResults, caseResults);
5034
- const isFailure = reporterResults.some((result)=>'fail' === result.status);
5035
- if (isWatchMode && isFailure) ensureProcessExitCode(1);
5036
- const cycleCoverageMap = isWatchMode && coverageProvider ? buildBrowserCoverageMap(reporterResults, coverageProvider) : void 0;
5037
- const result = {
5513
+ return {
5038
5514
  results: reporterResults,
5039
5515
  testResults: caseResults,
5040
- duration,
5041
- hasFailure: isFailure,
5516
+ duration: {
5517
+ totalTime: buildTime + testTime,
5518
+ buildTime,
5519
+ testTime
5520
+ },
5521
+ hasFailure: reporterResults.some((result)=>'fail' === result.status),
5522
+ rawCoverage,
5523
+ loadAssetFiles: loadBrowserCoverageAssetFiles,
5524
+ loadSourceMaps: loadBrowserCoverageSourceMaps,
5042
5525
  getSourcemap: getBrowserSourcemap,
5043
5526
  resolveSourcemap: resolveBrowserSourcemap,
5044
- close: closeContainerRuntime,
5045
- coverage: cycleCoverageMap,
5046
- watch: watchHandles
5527
+ close,
5528
+ watchSession
5047
5529
  };
5048
- if (isWatchMode) try {
5049
- await notifyTestRunEnd({
5050
- duration,
5051
- coverage: cycleCoverageMap?.files().length ? cycleCoverageMap.toJSON() : void 0
5052
- });
5053
- } finally{
5054
- await closeContainerRuntime?.();
5055
- }
5056
- if (isWatchMode && triggerRerun) {
5057
- watchState.hooksEnabled = true;
5058
- logWatchReadyMessage(context, enableCliShortcuts);
5059
- }
5060
- return result;
5061
5530
  };
5062
5531
  const listBrowserTests = async (context, options)=>{
5063
5532
  const browserProjects = options?.projects ?? getBrowserProjects(context);
@@ -5116,11 +5585,11 @@ const listBrowserTests = async (context, options)=>{
5116
5585
  environmentName: project.environmentName,
5117
5586
  projectRoot: normalize(project.rootPath),
5118
5587
  runtimeConfig: serializableConfig(projectRuntimeConfig(project, {
5119
- envMode: 'static'
5588
+ envMode: 'static',
5589
+ envOverlay: options?.env
5120
5590
  })),
5121
5591
  viewport: project.normalizedConfig.browser.viewport
5122
5592
  }));
5123
- const maxTestTimeoutForRpc = getMaxTestTimeoutForRpc(browserProjects);
5124
5593
  const hostOptions = {
5125
5594
  rootPath: normalize(context.rootPath),
5126
5595
  projects: projectRuntimeConfigs,
@@ -5129,7 +5598,7 @@ const listBrowserTests = async (context, options)=>{
5129
5598
  },
5130
5599
  mode: 'collect',
5131
5600
  debug: isDebug(),
5132
- rpcTimeout: maxTestTimeoutForRpc
5601
+ rpcTimeout: -1
5133
5602
  };
5134
5603
  runtime.setContainerOptions(hostOptions);
5135
5604
  const browserContext = await browser.newContext({
@@ -5147,7 +5616,8 @@ const listBrowserTests = async (context, options)=>{
5147
5616
  resolveCollect = resolve;
5148
5617
  });
5149
5618
  const page = await browserContext.newPage();
5150
- await page.exposeFunction(DISPATCH_MESSAGE_TYPE, (message)=>{
5619
+ await page.exposeFunction(DISPATCH_MESSAGE_TYPE, (envelope)=>{
5620
+ const message = envelope.message;
5151
5621
  switch(message.type){
5152
5622
  case 'collect-result':
5153
5623
  {
@@ -5249,8 +5719,11 @@ const emptyOutcome = ()=>({
5249
5719
  });
5250
5720
  async function createBrowserExecutor(context, options) {
5251
5721
  const { projects, coverageProvider, shardedEntries, freezeShardedEntries, filesOnly, allowEmptyRun, appliedModifyRstestConfigEnvironments } = options;
5722
+ const isWatchMode = 'watch' === context.command;
5252
5723
  let deferredClose;
5253
5724
  let inFlightCycle;
5725
+ let invalidationCallback;
5726
+ let watchSession;
5254
5727
  const foldOutcome = (result)=>{
5255
5728
  if (!result) return emptyOutcome();
5256
5729
  const map = buildBrowserCoverageMap(result.results, coverageProvider);
@@ -5263,9 +5736,12 @@ async function createBrowserExecutor(context, options) {
5263
5736
  buildTime: result.duration.buildTime,
5264
5737
  testTime: result.duration.testTime
5265
5738
  },
5266
- coverage: {
5267
- map: map?.toJSON()
5268
- },
5739
+ coverage: map?.files().length || result.rawCoverage?.length ? {
5740
+ map: map?.toJSON(),
5741
+ raw: result.rawCoverage,
5742
+ loadAssetFiles: result.loadAssetFiles,
5743
+ loadSourceMaps: result.loadSourceMaps
5744
+ } : void 0,
5269
5745
  resolveSourcemap: result.resolveSourcemap
5270
5746
  };
5271
5747
  };
@@ -5274,6 +5750,15 @@ async function createBrowserExecutor(context, options) {
5274
5750
  projects,
5275
5751
  async init () {},
5276
5752
  async runCycle (opts) {
5753
+ if (watchSession) {
5754
+ const cycle = watchSession.runCycle(opts.fileFilters ?? []);
5755
+ inFlightCycle = cycle;
5756
+ try {
5757
+ return await cycle;
5758
+ } finally{
5759
+ inFlightCycle = void 0;
5760
+ }
5761
+ }
5277
5762
  const cycle = runBrowserController(context, {
5278
5763
  projects,
5279
5764
  shardedEntries,
@@ -5282,24 +5767,37 @@ async function createBrowserExecutor(context, options) {
5282
5767
  appliedModifyRstestConfigEnvironments,
5283
5768
  onTraceEvents: opts.onTraceEvents,
5284
5769
  env: opts.env,
5285
- updateSnapshot: opts.updateSnapshot
5770
+ updateSnapshot: opts.updateSnapshot,
5771
+ onInvalidate: isWatchMode ? (hint)=>invalidationCallback?.(hint) : void 0
5286
5772
  });
5287
5773
  inFlightCycle = cycle;
5288
5774
  try {
5289
5775
  const result = await cycle;
5290
5776
  deferredClose = result?.close;
5777
+ watchSession = result?.watchSession;
5291
5778
  return foldOutcome(result);
5292
5779
  } finally{
5293
5780
  inFlightCycle = void 0;
5294
5781
  }
5295
5782
  },
5296
- async collect () {
5783
+ onInvalidate (cb) {
5784
+ invalidationCallback = cb;
5785
+ },
5786
+ hasWatchSession () {
5787
+ return void 0 !== watchSession;
5788
+ },
5789
+ async requestRerun (testPaths) {
5790
+ if (!watchSession) return void logger.log(color.yellow('\nBrowser Mode has no live watch session, so this rerun skipped it.'));
5791
+ await watchSession.requestRerun(testPaths);
5792
+ },
5793
+ async collect (opts) {
5297
5794
  const pending = listBrowserTests(context, {
5298
5795
  projects,
5299
5796
  shardedEntries,
5300
5797
  freezeShardedEntries,
5301
5798
  filesOnly,
5302
- appliedModifyRstestConfigEnvironments
5799
+ appliedModifyRstestConfigEnvironments,
5800
+ env: opts.env
5303
5801
  });
5304
5802
  inFlightCycle = pending;
5305
5803
  try {
@@ -5314,6 +5812,11 @@ async function createBrowserExecutor(context, options) {
5314
5812
  },
5315
5813
  async close () {
5316
5814
  if (inFlightCycle) await inFlightCycle.catch(()=>void 0);
5815
+ if (isWatchMode) {
5816
+ watchSession = void 0;
5817
+ await cleanupWatchRuntime();
5818
+ return;
5819
+ }
5317
5820
  const close = deferredClose;
5318
5821
  deferredClose = void 0;
5319
5822
  await close?.();