@rstest/browser 0.11.4 → 0.11.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/browser-container/container-static/js/683.4f821ce8f1.js +194 -0
- package/dist/browser-container/container-static/js/index.568aa7cb35.js +1 -0
- package/dist/browser-container/container-static/js/lib-react.e24a1d366b.js +2 -0
- package/dist/browser-container/index.html +1 -1
- package/dist/browserExecutor.d.ts +7 -5
- package/dist/browserRsbuild.d.ts +119 -0
- package/dist/containerRpc.d.ts +52 -0
- package/dist/dispatchCapabilities.d.ts +1 -2
- package/dist/headedScheduler.d.ts +58 -0
- package/dist/headlessScheduler.d.ts +37 -0
- package/dist/hostController.d.ts +12 -47
- package/dist/hostPayloads.d.ts +30 -0
- package/dist/index.js +1738 -1793
- package/dist/protocol.d.ts +5 -0
- package/dist/schedulerSeam.d.ts +37 -0
- package/dist/watchRerunPlanner.d.ts +5 -0
- package/dist/watchRuntime.d.ts +21 -0
- package/dist/watchSignals.d.ts +22 -0
- package/package.json +5 -5
- package/src/browserExecutor.ts +90 -8
- package/src/browserRsbuild.ts +1927 -0
- package/src/client/entry.ts +3 -0
- package/src/containerRpc.ts +206 -0
- package/src/dispatchCapabilities.ts +1 -6
- package/src/headedScheduler.ts +664 -0
- package/src/headlessScheduler.ts +566 -0
- package/src/hostController.ts +855 -4163
- package/src/hostPayloads.ts +62 -0
- package/src/protocol.ts +6 -0
- package/src/schedulerSeam.ts +49 -0
- package/src/watchRerunPlanner.ts +18 -7
- package/src/watchRuntime.ts +83 -0
- package/src/watchSignals.ts +93 -0
- package/dist/browser-container/container-static/js/243.a8eed2b9e7.js +0 -27406
- package/dist/browser-container/container-static/js/243.a8eed2b9e7.js.LICENSE.txt +0 -1
- package/dist/browser-container/container-static/js/index.84aaafaf21.js +0 -3058
- package/dist/browser-container/container-static/js/lib-react.62b27a21db.js +0 -8454
- package/dist/browser-container/container-static/js/lib-react.62b27a21db.js.LICENSE.txt +0 -1
- package/dist/headlessLatestRerunScheduler.d.ts +0 -18
- package/src/headlessLatestRerunScheduler.ts +0 -76
package/dist/index.js
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
import { DEFAULT_TEST_TIMEOUT, FATAL_SIGNALS, PhaseTracker, RSTEST_ENV_SYMBOL_KEY, applyWatchInvalidation, applyWebMockRspackConfig, browserIgnoredRuntimeConfigKeys, buildBrowserCoverageMap, color, createCoverageProvider, createRunnerEventSink, createSilentConsoleController,
|
|
2
|
-
import { existsSync } from "node:fs";
|
|
1
|
+
import { DEFAULT_TEST_TIMEOUT, FATAL_SIGNALS, PhaseTracker, RSTEST_ENV_SYMBOL_KEY, applyWatchInvalidation, applyWebMockRspackConfig, browserIgnoredRuntimeConfigKeys, buildBrowserCoverageMap, color, createCoverageProvider, createRunnerEventSink, createSilentConsoleController, getNumCpus as browser_getNumCpus, getPrettyConsoleName, getSetupFiles, getTestEntries, hasUserRstestConfigPlugins, importMetaRstestDefine, 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, 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
10
|
import convert_source_map from "convert-source-map";
|
|
@@ -1654,218 +1654,6 @@ __webpack_require__.add({
|
|
|
1654
1654
|
};
|
|
1655
1655
|
}
|
|
1656
1656
|
});
|
|
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
1657
|
const toErrorMessage = (error)=>error instanceof Error ? error.message : String(error);
|
|
1870
1658
|
class HostDispatchRouter {
|
|
1871
1659
|
handlers = new Map();
|
|
@@ -2141,104 +1929,6 @@ const validateBrowserConfig = (context)=>{
|
|
|
2141
1929
|
}
|
|
2142
1930
|
reportUnsupportedBrowserOptions(context);
|
|
2143
1931
|
};
|
|
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
1932
|
const supportedLocatorActions = new Set([
|
|
2243
1933
|
'click',
|
|
2244
1934
|
'dblclick',
|
|
@@ -2686,159 +2376,9 @@ function getBrowserProviderImplementation(provider) {
|
|
|
2686
2376
|
if (!implementation) throw new Error(`Unsupported browser provider: ${String(provider)}`);
|
|
2687
2377
|
return implementation;
|
|
2688
2378
|
}
|
|
2689
|
-
const
|
|
2690
|
-
|
|
2691
|
-
|
|
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
|
-
}
|
|
2379
|
+
const collectDeletedTestPaths = (previous, current)=>{
|
|
2380
|
+
const currentPathSet = new Set(current.map((file)=>file.testPath));
|
|
2381
|
+
return previous.map((file)=>file.testPath).filter((testPath)=>!currentPathSet.has(testPath));
|
|
2842
2382
|
};
|
|
2843
2383
|
const serializeTestFiles = (files)=>JSON.stringify(files.map((f)=>`${f.projectName}:${f.testPath}`).sort());
|
|
2844
2384
|
const normalizeTestFiles = (files)=>files.map((file)=>({
|
|
@@ -2854,11 +2394,8 @@ const planWatchRerun = ({ projectEntries, previousTestFiles, affectedTestFiles }
|
|
|
2854
2394
|
const normalizedPrevious = normalizeTestFiles(previousTestFiles);
|
|
2855
2395
|
const filesChanged = serializeTestFiles(currentTestFiles) !== serializeTestFiles(normalizedPrevious);
|
|
2856
2396
|
const normalizedAffectedTestFiles = affectedTestFiles.map((testFile)=>normalize(testFile));
|
|
2857
|
-
const
|
|
2858
|
-
|
|
2859
|
-
file
|
|
2860
|
-
]));
|
|
2861
|
-
const matchedAffectedFiles = normalizedAffectedTestFiles.map((testFile)=>currentFileMap.get(testFile)).filter((file)=>Boolean(file));
|
|
2397
|
+
const affectedPathSet = new Set(normalizedAffectedTestFiles);
|
|
2398
|
+
const matchedAffectedFiles = currentTestFiles.filter((file)=>affectedPathSet.has(file.testPath));
|
|
2862
2399
|
return {
|
|
2863
2400
|
currentTestFiles,
|
|
2864
2401
|
filesChanged,
|
|
@@ -2869,162 +2406,49 @@ const planWatchRerun = ({ projectEntries, previousTestFiles, affectedTestFiles }
|
|
|
2869
2406
|
const picomatch = __webpack_require__("../../node_modules/.pnpm/picomatch@4.0.5/node_modules/picomatch/index.js");
|
|
2870
2407
|
var picomatch_default = /*#__PURE__*/ __webpack_require__.n(picomatch);
|
|
2871
2408
|
const { createRsbuild: createRsbuild, rspack: rspack } = rsbuild;
|
|
2872
|
-
const
|
|
2409
|
+
const browserRsbuild_dirname = dirname(fileURLToPath(import.meta.url));
|
|
2873
2410
|
const OPTIONS_PLACEHOLDER = '__RSTEST_OPTIONS_PLACEHOLDER__';
|
|
2874
|
-
let nextBrowserFilePid = 1000000000;
|
|
2875
2411
|
const serializeForInlineScript = (value)=>JSON.stringify(value).replace(/</g, '\\u003c').replace(/\u2028/g, '\\u2028').replace(/\u2029/g, '\\u2029');
|
|
2876
2412
|
const getBrowserProviderOptions = (project)=>{
|
|
2877
2413
|
const browserConfig = project.normalizedConfig.browser;
|
|
2878
2414
|
return browserConfig.providerOptions ?? {};
|
|
2879
2415
|
};
|
|
2880
|
-
const
|
|
2881
|
-
|
|
2882
|
-
|
|
2883
|
-
|
|
2884
|
-
|
|
2885
|
-
|
|
2886
|
-
|
|
2416
|
+
const createBrowserWatchState = ()=>({
|
|
2417
|
+
lastTestFiles: [],
|
|
2418
|
+
hooksEnabled: false,
|
|
2419
|
+
invalidation: new Map(),
|
|
2420
|
+
pendingAffectedTestFiles: new Map(),
|
|
2421
|
+
compileStartTimes: new Map(),
|
|
2422
|
+
pendingBuildTimeMs: 0
|
|
2887
2423
|
});
|
|
2888
|
-
|
|
2889
|
-
|
|
2890
|
-
|
|
2891
|
-
|
|
2424
|
+
const drainPendingBuildTime = (watchState)=>{
|
|
2425
|
+
const buildTime = watchState.pendingBuildTimeMs;
|
|
2426
|
+
watchState.pendingBuildTimeMs = 0;
|
|
2427
|
+
return buildTime;
|
|
2428
|
+
};
|
|
2429
|
+
const drainPendingAffectedTestFiles = (watchState)=>{
|
|
2430
|
+
const affected = new Set();
|
|
2431
|
+
for (const files of watchState.pendingAffectedTestFiles.values())for (const file of files)affected.add(file);
|
|
2432
|
+
watchState.pendingAffectedTestFiles.clear();
|
|
2433
|
+
return Array.from(affected);
|
|
2434
|
+
};
|
|
2435
|
+
const resolveViewport = (viewport)=>{
|
|
2436
|
+
if (!viewport) return null;
|
|
2437
|
+
if ('string' == typeof viewport) return resolveBrowserViewportPreset(viewport);
|
|
2438
|
+
if ('number' == typeof viewport.width && Number.isFinite(viewport.width) && viewport.width > 0 && 'number' == typeof viewport.height && Number.isFinite(viewport.height) && viewport.height > 0) return {
|
|
2439
|
+
width: viewport.width,
|
|
2440
|
+
height: viewport.height
|
|
2892
2441
|
};
|
|
2442
|
+
return null;
|
|
2893
2443
|
};
|
|
2894
|
-
|
|
2895
|
-
|
|
2896
|
-
|
|
2897
|
-
|
|
2898
|
-
|
|
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);
|
|
2444
|
+
const mapViewportByProject = (projects)=>{
|
|
2445
|
+
const map = new Map();
|
|
2446
|
+
for (const project of projects){
|
|
2447
|
+
const viewport = resolveViewport(project.viewport);
|
|
2448
|
+
if (viewport) map.set(project.name, viewport);
|
|
3022
2449
|
}
|
|
3023
2450
|
return map;
|
|
3024
2451
|
};
|
|
3025
|
-
const ensureProcessExitCode = (code)=>{
|
|
3026
|
-
if (void 0 === process.exitCode || 0 === process.exitCode) process.exitCode = code;
|
|
3027
|
-
};
|
|
3028
2452
|
const castArray = (arr)=>{
|
|
3029
2453
|
if (void 0 === arr) return [];
|
|
3030
2454
|
return Array.isArray(arr) ? arr : [
|
|
@@ -3228,7 +2652,6 @@ const getBrowserRsbuildEnvironmentConfig = (project)=>({
|
|
|
3228
2652
|
plugins: project.normalizedConfig.plugins,
|
|
3229
2653
|
root: project.rootPath
|
|
3230
2654
|
});
|
|
3231
|
-
const getMaxTestTimeoutForRpc = (projects)=>Math.max(...projects.map((p)=>p.normalizedConfig.testTimeout ?? DEFAULT_TEST_TIMEOUT));
|
|
3232
2655
|
const getBrowserLaunchOptions = (project)=>({
|
|
3233
2656
|
provider: project.normalizedConfig.browser.provider,
|
|
3234
2657
|
browser: project.normalizedConfig.browser.browser,
|
|
@@ -3247,15 +2670,7 @@ const ensureConsistentBrowserLaunchOptions = (projects)=>{
|
|
|
3247
2670
|
}
|
|
3248
2671
|
return firstOptions;
|
|
3249
2672
|
};
|
|
3250
|
-
const
|
|
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)=>{
|
|
2673
|
+
const browserRsbuild_collectProjectEntries = async (context, browserProjects = getBrowserProjects(context))=>Promise.all(browserProjects.map(async (project)=>{
|
|
3259
2674
|
const { normalizedConfig: { include, exclude, includeSource, setupFiles } } = project;
|
|
3260
2675
|
const tests = await getTestEntries({
|
|
3261
2676
|
include,
|
|
@@ -3275,14 +2690,14 @@ const collectProjectEntries = async (context, browserProjects = getBrowserProjec
|
|
|
3275
2690
|
}));
|
|
3276
2691
|
const resolveBrowserFile = (relativePath)=>{
|
|
3277
2692
|
const candidates = [
|
|
3278
|
-
external_pathe_resolve(
|
|
3279
|
-
external_pathe_resolve(
|
|
2693
|
+
external_pathe_resolve(browserRsbuild_dirname, '../src', relativePath),
|
|
2694
|
+
external_pathe_resolve(browserRsbuild_dirname, relativePath)
|
|
3280
2695
|
];
|
|
3281
2696
|
for (const candidate of candidates)if (existsSync(candidate)) return candidate;
|
|
3282
2697
|
throw new Error(`Unable to resolve browser client file: ${relativePath}`);
|
|
3283
2698
|
};
|
|
3284
2699
|
const resolveContainerDist = ()=>{
|
|
3285
|
-
const distPath = external_pathe_resolve(
|
|
2700
|
+
const distPath = external_pathe_resolve(browserRsbuild_dirname, 'browser-container');
|
|
3286
2701
|
if (existsSync(distPath)) return distPath;
|
|
3287
2702
|
throw new Error(`Browser container build not found at ${distPath}. Please run "pnpm --filter @rstest/browser build".`);
|
|
3288
2703
|
};
|
|
@@ -3424,26 +2839,6 @@ const destroyBrowserRuntime = async (runtime)=>{
|
|
|
3424
2839
|
force: true
|
|
3425
2840
|
}).catch(()=>{});
|
|
3426
2841
|
};
|
|
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
2842
|
const createBrowserRuntime = async ({ context, projectEntries: initialProjectEntries, browserProjects, shardedEntries, freezeShardedEntries, tempDir, isWatchMode, onTriggerRerun, containerDistPath, containerDevServer, forceHeadless, skipProviderLaunch, appliedModifyRstestConfigEnvironments })=>{
|
|
3448
2843
|
const containerHtmlTemplate = containerDistPath ? await promises.readFile(join(containerDistPath, 'index.html'), 'utf-8') : null;
|
|
3449
2844
|
let injectedContainerHtml = null;
|
|
@@ -3549,6 +2944,30 @@ const createBrowserRuntime = async ({ context, projectEntries: initialProjectEnt
|
|
|
3549
2944
|
return false;
|
|
3550
2945
|
}
|
|
3551
2946
|
};
|
|
2947
|
+
const serveContainerRoute = async (req, res, next)=>{
|
|
2948
|
+
if (!req.url) return void next();
|
|
2949
|
+
const url = new URL(req.url, 'http://localhost');
|
|
2950
|
+
if ('/' === url.pathname) {
|
|
2951
|
+
if (await respondWithDevServerHtml(url, res)) return;
|
|
2952
|
+
const html = injectedContainerHtml || containerHtmlTemplate?.replace(OPTIONS_PLACEHOLDER, 'null');
|
|
2953
|
+
if (html) {
|
|
2954
|
+
res.setHeader('Content-Type', 'text/html');
|
|
2955
|
+
res.end(html);
|
|
2956
|
+
return;
|
|
2957
|
+
}
|
|
2958
|
+
res.statusCode = 502;
|
|
2959
|
+
res.end('Container UI is not available.');
|
|
2960
|
+
return;
|
|
2961
|
+
}
|
|
2962
|
+
if (url.pathname.startsWith('/container-static/')) {
|
|
2963
|
+
if (await proxyDevServerAsset(req, res)) return;
|
|
2964
|
+
if (serveContainer) return void serveContainer(req, res, next);
|
|
2965
|
+
res.statusCode = 502;
|
|
2966
|
+
res.end('Container assets are not available.');
|
|
2967
|
+
return;
|
|
2968
|
+
}
|
|
2969
|
+
next();
|
|
2970
|
+
};
|
|
3552
2971
|
const buildProjectServer = async (project, isContainerServer)=>{
|
|
3553
2972
|
const manifestPath = join(tempDir, toSafeVarName(project.environmentName), VIRTUAL_MANIFEST_FILENAME);
|
|
3554
2973
|
const entry = getProjectEntry(project);
|
|
@@ -3585,7 +3004,10 @@ const createBrowserRuntime = async ({ context, projectEntries: initialProjectEnt
|
|
|
3585
3004
|
server: {
|
|
3586
3005
|
printUrls: false,
|
|
3587
3006
|
port: project.normalizedConfig.browser.port ?? (isContainerServer ? 4000 : 0),
|
|
3588
|
-
strictPort: project.normalizedConfig.browser.strictPort
|
|
3007
|
+
strictPort: project.normalizedConfig.browser.strictPort,
|
|
3008
|
+
setup: isContainerServer ? ({ server })=>{
|
|
3009
|
+
server.middlewares.use(serveContainerRoute);
|
|
3010
|
+
} : void 0
|
|
3589
3011
|
},
|
|
3590
3012
|
dev: createBrowserRsbuildDevConfig(enableHmr),
|
|
3591
3013
|
environments: {
|
|
@@ -3704,7 +3126,7 @@ const createBrowserRuntime = async ({ context, projectEntries: initialProjectEnt
|
|
|
3704
3126
|
if (watchState.hooksEnabled) watchState.pendingBuildTimeMs = Math.max(watchState.pendingBuildTimeMs, Date.now() - compileStart);
|
|
3705
3127
|
}
|
|
3706
3128
|
if (stats) {
|
|
3707
|
-
const [projectEntry] = await
|
|
3129
|
+
const [projectEntry] = await browserRsbuild_collectProjectEntries(context, [
|
|
3708
3130
|
project
|
|
3709
3131
|
]);
|
|
3710
3132
|
const entryTestFiles = new Set(collectWatchTestFiles(projectEntry ? [
|
|
@@ -3800,27 +3222,6 @@ const createBrowserRuntime = async ({ context, projectEntries: initialProjectEnt
|
|
|
3800
3222
|
}
|
|
3801
3223
|
return;
|
|
3802
3224
|
}
|
|
3803
|
-
if (isContainerServer) {
|
|
3804
|
-
if ('/' === url.pathname) {
|
|
3805
|
-
if (await respondWithDevServerHtml(url, res)) return;
|
|
3806
|
-
const html = injectedContainerHtml || containerHtmlTemplate?.replace(OPTIONS_PLACEHOLDER, 'null');
|
|
3807
|
-
if (html) {
|
|
3808
|
-
res.setHeader('Content-Type', 'text/html');
|
|
3809
|
-
res.end(html);
|
|
3810
|
-
return;
|
|
3811
|
-
}
|
|
3812
|
-
res.statusCode = 502;
|
|
3813
|
-
res.end('Container UI is not available.');
|
|
3814
|
-
return;
|
|
3815
|
-
}
|
|
3816
|
-
if (url.pathname.startsWith('/container-static/')) {
|
|
3817
|
-
if (await proxyDevServerAsset(req, res)) return;
|
|
3818
|
-
if (serveContainer) return void serveContainer(req, res, next);
|
|
3819
|
-
res.statusCode = 502;
|
|
3820
|
-
res.end('Container assets are not available.');
|
|
3821
|
-
return;
|
|
3822
|
-
}
|
|
3823
|
-
}
|
|
3824
3225
|
if ('/runner.html' === url.pathname) {
|
|
3825
3226
|
res.setHeader('Content-Type', 'text/html');
|
|
3826
3227
|
res.end(htmlTemplate);
|
|
@@ -3903,1209 +3304,1722 @@ async function resolveProjectEntries(context, shardedEntries, browserProjects) {
|
|
|
3903
3304
|
}
|
|
3904
3305
|
return projectEntries;
|
|
3905
3306
|
}
|
|
3906
|
-
return
|
|
3307
|
+
return browserRsbuild_collectProjectEntries(context, browserProjects);
|
|
3907
3308
|
}
|
|
3908
|
-
const
|
|
3909
|
-
|
|
3910
|
-
|
|
3911
|
-
|
|
3912
|
-
|
|
3913
|
-
|
|
3914
|
-
|
|
3915
|
-
|
|
3916
|
-
|
|
3917
|
-
|
|
3918
|
-
|
|
3919
|
-
|
|
3920
|
-
sourcemap: null
|
|
3921
|
-
};
|
|
3922
|
-
const normalizedUrl = normalizeJavaScriptUrl(sourcePath);
|
|
3923
|
-
if (!normalizedUrl) return {
|
|
3924
|
-
handled: true,
|
|
3925
|
-
sourcemap: null
|
|
3926
|
-
};
|
|
3927
|
-
if (browserSourceMapCache.has(normalizedUrl)) return {
|
|
3928
|
-
handled: true,
|
|
3929
|
-
sourcemap: browserSourceMapCache.get(normalizedUrl) ?? null
|
|
3930
|
-
};
|
|
3931
|
-
return {
|
|
3932
|
-
handled: true,
|
|
3933
|
-
sourcemap: await loadSourceMapWithCache({
|
|
3934
|
-
jsUrl: normalizedUrl,
|
|
3935
|
-
cache: browserSourceMapCache
|
|
3936
|
-
})
|
|
3937
|
-
};
|
|
3938
|
-
};
|
|
3939
|
-
const getBrowserSourcemap = async (sourcePath)=>{
|
|
3940
|
-
const result = await resolveBrowserSourcemap(sourcePath);
|
|
3941
|
-
return result.handled ? result.sourcemap : null;
|
|
3309
|
+
const TYPE_REQUEST = "q";
|
|
3310
|
+
const TYPE_RESPONSE = "s";
|
|
3311
|
+
function createPromiseWithResolvers() {
|
|
3312
|
+
let resolve;
|
|
3313
|
+
let reject;
|
|
3314
|
+
return {
|
|
3315
|
+
promise: new Promise((res, rej)=>{
|
|
3316
|
+
resolve = res;
|
|
3317
|
+
reject = rej;
|
|
3318
|
+
}),
|
|
3319
|
+
resolve,
|
|
3320
|
+
reject
|
|
3942
3321
|
};
|
|
3943
|
-
|
|
3944
|
-
|
|
3945
|
-
|
|
3946
|
-
|
|
3947
|
-
|
|
3948
|
-
|
|
3949
|
-
|
|
3950
|
-
|
|
3951
|
-
|
|
3952
|
-
|
|
3953
|
-
|
|
3954
|
-
|
|
3955
|
-
|
|
3956
|
-
|
|
3957
|
-
|
|
3958
|
-
|
|
3959
|
-
|
|
3322
|
+
}
|
|
3323
|
+
const random = Math.random.bind(Math);
|
|
3324
|
+
const urlAlphabet = "useandom-26T198340PX75pxJACKVERYMINDBUSHWOLF_GQZbfghjklqvwyzrict";
|
|
3325
|
+
function nanoid(size = 21) {
|
|
3326
|
+
let id = "";
|
|
3327
|
+
let i = size;
|
|
3328
|
+
while(i--)id += urlAlphabet[64 * random() | 0];
|
|
3329
|
+
return id;
|
|
3330
|
+
}
|
|
3331
|
+
const DEFAULT_TIMEOUT = 6e4;
|
|
3332
|
+
const defaultSerialize = (i)=>i;
|
|
3333
|
+
const defaultDeserialize = defaultSerialize;
|
|
3334
|
+
const { clearTimeout: dist_clearTimeout, setTimeout: dist_setTimeout } = globalThis;
|
|
3335
|
+
function createBirpc($functions, options) {
|
|
3336
|
+
const { post, on, off = ()=>{}, eventNames = [], serialize = defaultSerialize, deserialize = defaultDeserialize, resolver, bind = "rpc", timeout = DEFAULT_TIMEOUT, proxify = true } = options;
|
|
3337
|
+
let $closed = false;
|
|
3338
|
+
const _rpcPromiseMap = /* @__PURE__ */ new Map();
|
|
3339
|
+
let _promiseInit;
|
|
3340
|
+
let rpc;
|
|
3341
|
+
async function _call(method, args, event, optional) {
|
|
3342
|
+
if ($closed) throw new Error(`[birpc] rpc is closed, cannot call "${method}"`);
|
|
3343
|
+
const req = {
|
|
3344
|
+
m: method,
|
|
3345
|
+
a: args,
|
|
3346
|
+
t: TYPE_REQUEST
|
|
3960
3347
|
};
|
|
3961
|
-
if (
|
|
3962
|
-
|
|
3963
|
-
|
|
3964
|
-
|
|
3965
|
-
|
|
3966
|
-
getSourcemap: getBrowserSourcemap,
|
|
3967
|
-
unhandledErrors: errorResult.unhandledErrors
|
|
3968
|
-
});
|
|
3969
|
-
return errorResult;
|
|
3970
|
-
};
|
|
3971
|
-
const toError = (error)=>error instanceof Error ? error : new Error(String(error));
|
|
3972
|
-
const failWithError = async (error, cleanup)=>{
|
|
3973
|
-
if (isWatchMode) ensureProcessExitCode(1);
|
|
3974
|
-
const normalizedError = toError(error);
|
|
3975
|
-
if (cleanup && !isWatchMode) return buildErrorResult(normalizedError, cleanup);
|
|
3976
|
-
try {
|
|
3977
|
-
return await buildErrorResult(normalizedError);
|
|
3348
|
+
if (optional) req.o = true;
|
|
3349
|
+
const send = async (_req)=>post(serialize(_req));
|
|
3350
|
+
if (event) return void await send(req);
|
|
3351
|
+
if (_promiseInit) try {
|
|
3352
|
+
await _promiseInit;
|
|
3978
3353
|
} finally{
|
|
3979
|
-
|
|
3980
|
-
}
|
|
3981
|
-
};
|
|
3982
|
-
const collectDeletedTestPaths = (previous, current)=>{
|
|
3983
|
-
const currentPathSet = new Set(current.map((file)=>file.testPath));
|
|
3984
|
-
return previous.map((file)=>file.testPath).filter((testPath)=>!currentPathSet.has(testPath));
|
|
3985
|
-
};
|
|
3986
|
-
const notifyTestRunStart = async ()=>{
|
|
3987
|
-
if (!isWatchMode) return;
|
|
3988
|
-
for (const reporter of context.reporters)await reporter.onTestRunStart?.();
|
|
3989
|
-
};
|
|
3990
|
-
const coverageConfig = browserProjects.find((project)=>project.normalizedConfig.coverage?.enabled)?.normalizedConfig.coverage;
|
|
3991
|
-
const coverageProvider = coverageConfig?.enabled ? await createCoverageProvider(coverageConfig, context.rootPath) : null;
|
|
3992
|
-
const notifyTestRunEnd = async ({ duration, unhandledErrors, filterRerunTestPaths })=>{
|
|
3993
|
-
if (!isWatchMode) return;
|
|
3994
|
-
let mergedCoverage;
|
|
3995
|
-
if (coverageProvider) {
|
|
3996
|
-
const coverageMap = coverageProvider.createCoverageMap();
|
|
3997
|
-
let hasCoverage = false;
|
|
3998
|
-
for (const result of context.reporterResults.results)if (result.coverage) {
|
|
3999
|
-
coverageMap.merge(result.coverage);
|
|
4000
|
-
hasCoverage = true;
|
|
4001
|
-
}
|
|
4002
|
-
if (hasCoverage) mergedCoverage = coverageMap.toJSON();
|
|
3354
|
+
_promiseInit = void 0;
|
|
4003
3355
|
}
|
|
4004
|
-
|
|
4005
|
-
|
|
4006
|
-
|
|
4007
|
-
|
|
4008
|
-
|
|
4009
|
-
|
|
4010
|
-
|
|
4011
|
-
|
|
4012
|
-
|
|
4013
|
-
|
|
4014
|
-
|
|
4015
|
-
|
|
4016
|
-
|
|
4017
|
-
|
|
4018
|
-
|
|
4019
|
-
|
|
4020
|
-
|
|
4021
|
-
|
|
4022
|
-
|
|
4023
|
-
|
|
4024
|
-
|
|
4025
|
-
return failWithError(originalError);
|
|
4026
|
-
}
|
|
4027
|
-
if (!containerDevServer) try {
|
|
4028
|
-
containerDistPath = resolveContainerDist();
|
|
4029
|
-
} catch (error) {
|
|
4030
|
-
return failWithError(error);
|
|
4031
|
-
}
|
|
4032
|
-
}
|
|
4033
|
-
let projectEntries = await resolveProjectEntries(context, options?.shardedEntries, browserProjects);
|
|
4034
|
-
let totalTests = projectEntries.reduce((total, item)=>total + item.testFiles.length, 0);
|
|
4035
|
-
const shouldKeepWatchingWithEmptySet = isWatchMode && allowEmptyWatchRun;
|
|
4036
|
-
const shouldInitializeEmptyBrowserHooks = 0 === totalTests && hasUserRstestConfigPlugins(browserProjects);
|
|
4037
|
-
const createEmptyRunResult = ()=>{
|
|
4038
|
-
const elapsed = Math.max(0, Date.now() - buildStart);
|
|
4039
|
-
return {
|
|
4040
|
-
results: [],
|
|
4041
|
-
testResults: [],
|
|
4042
|
-
duration: {
|
|
4043
|
-
totalTime: elapsed,
|
|
4044
|
-
buildTime: elapsed,
|
|
4045
|
-
testTime: 0
|
|
4046
|
-
},
|
|
4047
|
-
hasFailure: false,
|
|
4048
|
-
getSourcemap: getBrowserSourcemap,
|
|
4049
|
-
resolveSourcemap: resolveBrowserSourcemap
|
|
4050
|
-
};
|
|
4051
|
-
};
|
|
4052
|
-
const reportEmptyTestSet = ()=>{
|
|
4053
|
-
const code = context.normalizedConfig.passWithNoTests ? 0 : 1;
|
|
4054
|
-
if (isWatchMode || !allowEmptyRun) {
|
|
4055
|
-
const message = shouldKeepWatchingWithEmptySet ? 'No test files found.' : getNoTestFilesMessage({
|
|
4056
|
-
context,
|
|
4057
|
-
code,
|
|
4058
|
-
defaultMessage: `No test files found, exiting with code ${code}.`
|
|
3356
|
+
let { promise, resolve, reject } = createPromiseWithResolvers();
|
|
3357
|
+
const id = nanoid();
|
|
3358
|
+
req.i = id;
|
|
3359
|
+
let timeoutId;
|
|
3360
|
+
async function handler(newReq = req) {
|
|
3361
|
+
if (timeout >= 0) {
|
|
3362
|
+
timeoutId = dist_setTimeout(()=>{
|
|
3363
|
+
try {
|
|
3364
|
+
if (options.onTimeoutError?.call(rpc, method, args) !== true) throw new Error(`[birpc] timeout on calling "${method}"`);
|
|
3365
|
+
} catch (e) {
|
|
3366
|
+
reject(e);
|
|
3367
|
+
}
|
|
3368
|
+
_rpcPromiseMap.delete(id);
|
|
3369
|
+
}, timeout);
|
|
3370
|
+
if ("object" == typeof timeoutId) timeoutId = timeoutId.unref?.();
|
|
3371
|
+
}
|
|
3372
|
+
_rpcPromiseMap.set(id, {
|
|
3373
|
+
resolve,
|
|
3374
|
+
reject,
|
|
3375
|
+
timeoutId,
|
|
3376
|
+
method
|
|
4059
3377
|
});
|
|
4060
|
-
|
|
4061
|
-
|
|
4062
|
-
if (context.relatedFilters?.length) logger.log(color.gray('related: '), context.relatedFilters.join(color.gray(', ')));
|
|
4063
|
-
else if (context.fileFilters?.length) logger.log(color.gray('filter: '), context.fileFilters.join(color.gray(', ')));
|
|
3378
|
+
await send(newReq);
|
|
3379
|
+
return promise;
|
|
4064
3380
|
}
|
|
4065
|
-
if (isWatchMode && 0 !== code && !shouldKeepWatchingWithEmptySet && !allowEmptyRun) ensureProcessExitCode(code);
|
|
4066
|
-
return !shouldKeepWatchingWithEmptySet;
|
|
4067
|
-
};
|
|
4068
|
-
if (0 === totalTests && !shouldInitializeEmptyBrowserHooks) {
|
|
4069
|
-
if (reportEmptyTestSet()) return allowEmptyRun ? createEmptyRunResult() : void 0;
|
|
4070
|
-
}
|
|
4071
|
-
if (!filesOnly) await notifyTestRunStart();
|
|
4072
|
-
const enableCliShortcuts = isWatchMode && isTTY('stdin');
|
|
4073
|
-
const browserTempOutputRoot = context.normalizedConfig.output.distPath.root;
|
|
4074
|
-
const tempDir = isWatchMode && watchContext.runtime ? watchContext.runtime.tempDir : isWatchMode ? join(context.rootPath, browserTempOutputRoot, 'browser', 'watch') : join(context.rootPath, browserTempOutputRoot, 'browser', Date.now().toString());
|
|
4075
|
-
let runtime = isWatchMode ? watchContext.runtime : null;
|
|
4076
|
-
let triggerRerun;
|
|
4077
|
-
let awaitHeadlessRerunIdle;
|
|
4078
|
-
if (!runtime) {
|
|
4079
3381
|
try {
|
|
4080
|
-
|
|
4081
|
-
|
|
4082
|
-
|
|
4083
|
-
|
|
4084
|
-
|
|
4085
|
-
|
|
4086
|
-
|
|
4087
|
-
|
|
4088
|
-
onTriggerRerun: isWatchMode ? async ()=>{
|
|
4089
|
-
await triggerRerun?.();
|
|
4090
|
-
} : void 0,
|
|
4091
|
-
containerDistPath,
|
|
4092
|
-
containerDevServer,
|
|
4093
|
-
skipProviderLaunch: filesOnly,
|
|
4094
|
-
appliedModifyRstestConfigEnvironments: options?.appliedModifyRstestConfigEnvironments
|
|
4095
|
-
});
|
|
4096
|
-
} catch (error) {
|
|
4097
|
-
return failWithError(error, async ()=>{
|
|
4098
|
-
await promises.rm(tempDir, {
|
|
4099
|
-
recursive: true,
|
|
4100
|
-
force: true
|
|
4101
|
-
}).catch(()=>{});
|
|
4102
|
-
});
|
|
4103
|
-
}
|
|
4104
|
-
if (isWatchMode) {
|
|
4105
|
-
watchContext.runtime = runtime;
|
|
4106
|
-
registerWatchCleanup(context.embedded);
|
|
3382
|
+
if (options.onRequest) await options.onRequest.call(rpc, req, handler, resolve);
|
|
3383
|
+
else await handler();
|
|
3384
|
+
} catch (e) {
|
|
3385
|
+
if (options.onGeneralError?.call(rpc, e) !== true) throw e;
|
|
3386
|
+
return;
|
|
3387
|
+
} finally{
|
|
3388
|
+
dist_clearTimeout(timeoutId);
|
|
3389
|
+
_rpcPromiseMap.delete(id);
|
|
4107
3390
|
}
|
|
3391
|
+
return promise;
|
|
4108
3392
|
}
|
|
4109
|
-
const
|
|
4110
|
-
|
|
4111
|
-
|
|
4112
|
-
|
|
4113
|
-
|
|
4114
|
-
|
|
4115
|
-
|
|
4116
|
-
|
|
4117
|
-
pending.add(file.testPath);
|
|
4118
|
-
watchState.pendingAffectedTestFiles.set(file.projectName, pending);
|
|
4119
|
-
seeded += 1;
|
|
4120
|
-
}
|
|
4121
|
-
return seeded;
|
|
4122
|
-
};
|
|
4123
|
-
const watchHandles = isWatchMode ? {
|
|
4124
|
-
rerun: async (testPaths)=>{
|
|
4125
|
-
const seeded = seedPendingRerun(testPaths);
|
|
4126
|
-
if (testPaths && 0 === seeded) return;
|
|
4127
|
-
await triggerRerun?.();
|
|
4128
|
-
await awaitHeadlessRerunIdle?.();
|
|
3393
|
+
const builtinMethods = {
|
|
3394
|
+
$call: (method, ...args)=>_call(method, args, false),
|
|
3395
|
+
$callOptional: (method, ...args)=>_call(method, args, false, true),
|
|
3396
|
+
$callEvent: (method, ...args)=>_call(method, args, true),
|
|
3397
|
+
$callRaw: (options$1)=>_call(options$1.method, options$1.args, options$1.event, options$1.optional),
|
|
3398
|
+
$rejectPendingCalls,
|
|
3399
|
+
get $closed () {
|
|
3400
|
+
return $closed;
|
|
4129
3401
|
},
|
|
4130
|
-
|
|
4131
|
-
|
|
4132
|
-
const finalizeWatchRerun = async ({ rerunTestPaths, testTime, unhandledErrors })=>{
|
|
4133
|
-
const rerunPathSet = new Set(rerunTestPaths);
|
|
4134
|
-
let sessionCoverage;
|
|
4135
|
-
const coverageMap = buildBrowserCoverageMap(context.reporterResults.results, coverageProvider, {
|
|
4136
|
-
keepResultCoverage: true
|
|
4137
|
-
});
|
|
4138
|
-
if (coverageMap && coverageMap.files().length > 0) sessionCoverage = coverageMap.toJSON();
|
|
4139
|
-
const outcome = {
|
|
4140
|
-
results: context.reporterResults.results.filter((result)=>rerunPathSet.has(result.testPath)),
|
|
4141
|
-
testResults: context.reporterResults.testResults.filter((result)=>rerunPathSet.has(result.testPath)),
|
|
4142
|
-
errors: unhandledErrors ?? [],
|
|
4143
|
-
testPaths: rerunTestPaths,
|
|
4144
|
-
duration: {
|
|
4145
|
-
buildTime: drainPendingBuildTime(watchState),
|
|
4146
|
-
testTime
|
|
4147
|
-
},
|
|
4148
|
-
coverage: sessionCoverage ? {
|
|
4149
|
-
map: sessionCoverage
|
|
4150
|
-
} : void 0,
|
|
4151
|
-
resolveSourcemap: resolveBrowserSourcemap
|
|
4152
|
-
};
|
|
4153
|
-
await finalizeRunCycle(context, {
|
|
4154
|
-
outcomes: [
|
|
4155
|
-
outcome
|
|
4156
|
-
],
|
|
4157
|
-
mode: 'on-demand',
|
|
4158
|
-
isWatchMode: true,
|
|
4159
|
-
coverageProvider,
|
|
4160
|
-
reportOnFailure: coverageConfig?.reportOnFailure ?? false
|
|
4161
|
-
});
|
|
4162
|
-
};
|
|
4163
|
-
projectEntries = runtime.projectEntries;
|
|
4164
|
-
totalTests = projectEntries.reduce((total, item)=>total + item.testFiles.length, 0);
|
|
4165
|
-
const buildTime = Date.now() - buildStart;
|
|
4166
|
-
if (filesOnly) return {
|
|
4167
|
-
results: [],
|
|
4168
|
-
testResults: [],
|
|
4169
|
-
duration: {
|
|
4170
|
-
totalTime: buildTime,
|
|
4171
|
-
buildTime,
|
|
4172
|
-
testTime: 0
|
|
3402
|
+
get $meta () {
|
|
3403
|
+
return options.meta;
|
|
4173
3404
|
},
|
|
4174
|
-
|
|
4175
|
-
|
|
4176
|
-
resolveSourcemap: resolveBrowserSourcemap,
|
|
4177
|
-
close: ()=>destroyBrowserRuntime(runtime)
|
|
3405
|
+
$close,
|
|
3406
|
+
$functions
|
|
4178
3407
|
};
|
|
4179
|
-
|
|
4180
|
-
|
|
4181
|
-
|
|
3408
|
+
rpc = proxify ? new Proxy({}, {
|
|
3409
|
+
get (_, method) {
|
|
3410
|
+
if (Object.prototype.hasOwnProperty.call(builtinMethods, method)) return builtinMethods[method];
|
|
3411
|
+
if ("then" === method && !eventNames.includes("then") && !("then" in $functions)) return;
|
|
3412
|
+
const sendEvent = (...args)=>_call(method, args, true);
|
|
3413
|
+
if (eventNames.includes(method)) {
|
|
3414
|
+
sendEvent.asEvent = sendEvent;
|
|
3415
|
+
return sendEvent;
|
|
3416
|
+
}
|
|
3417
|
+
const sendCall = (...args)=>_call(method, args, false);
|
|
3418
|
+
sendCall.asEvent = sendEvent;
|
|
3419
|
+
return sendCall;
|
|
3420
|
+
}
|
|
3421
|
+
}) : builtinMethods;
|
|
3422
|
+
function $close(customError) {
|
|
3423
|
+
$closed = true;
|
|
3424
|
+
_rpcPromiseMap.forEach(({ reject, method })=>{
|
|
3425
|
+
const error = /* @__PURE__ */ new Error(`[birpc] rpc is closed, cannot call "${method}"`);
|
|
3426
|
+
if (customError) {
|
|
3427
|
+
customError.cause ??= error;
|
|
3428
|
+
return reject(customError);
|
|
3429
|
+
}
|
|
3430
|
+
reject(error);
|
|
3431
|
+
});
|
|
3432
|
+
_rpcPromiseMap.clear();
|
|
3433
|
+
off(onMessage);
|
|
4182
3434
|
}
|
|
4183
|
-
|
|
4184
|
-
|
|
4185
|
-
|
|
4186
|
-
|
|
4187
|
-
|
|
4188
|
-
|
|
4189
|
-
|
|
4190
|
-
environmentName: project.environmentName,
|
|
4191
|
-
projectRoot: normalize(project.rootPath),
|
|
4192
|
-
runtimeConfig: serializableConfig(projectRuntimeConfig(project, {
|
|
4193
|
-
envMode: 'static',
|
|
4194
|
-
envOverlay: env
|
|
4195
|
-
})),
|
|
4196
|
-
viewport: project.normalizedConfig.browser.viewport
|
|
4197
|
-
}));
|
|
4198
|
-
const maxTestTimeoutForRpc = getMaxTestTimeoutForRpc(browserProjects);
|
|
4199
|
-
const projectRunnerUrls = Object.fromEntries([
|
|
4200
|
-
...runtime.projectServers
|
|
4201
|
-
].map(([name, server])=>[
|
|
4202
|
-
name,
|
|
4203
|
-
`http://localhost:${server.port}`
|
|
4204
|
-
]));
|
|
4205
|
-
const hostOptions = {
|
|
4206
|
-
rootPath: normalize(context.rootPath),
|
|
4207
|
-
projects: projectRuntimeConfigs,
|
|
4208
|
-
snapshot: {
|
|
4209
|
-
updateSnapshot: options?.updateSnapshot ?? context.snapshotManager.options.updateSnapshot
|
|
4210
|
-
},
|
|
4211
|
-
runnerUrl: `http://localhost:${runtime.containerServer.port}`,
|
|
4212
|
-
projectRunnerUrls,
|
|
4213
|
-
wsPort,
|
|
4214
|
-
debug: isDebug(),
|
|
4215
|
-
rpcTimeout: maxTestTimeoutForRpc
|
|
4216
|
-
};
|
|
4217
|
-
const browserProviderProjects = browserProjects.map((project)=>({
|
|
4218
|
-
rootPath: normalize(project.rootPath),
|
|
4219
|
-
provider: project.normalizedConfig.browser.provider
|
|
4220
|
-
}));
|
|
4221
|
-
const implementationByProvider = new Map();
|
|
4222
|
-
for (const browserProject of browserProviderProjects)if (!implementationByProvider.has(browserProject.provider)) implementationByProvider.set(browserProject.provider, getBrowserProviderImplementation(browserProject.provider));
|
|
4223
|
-
let activeContainerPage = null;
|
|
4224
|
-
let getHeadlessRunnerPageBySessionId;
|
|
4225
|
-
const dispatchBrowserRpcRequest = async ({ request, target })=>{
|
|
4226
|
-
const timeoutFallbackMs = maxTestTimeoutForRpc;
|
|
4227
|
-
const provider = resolveProviderForTestPath({
|
|
4228
|
-
testPath: request.testPath,
|
|
4229
|
-
browserProjects: browserProviderProjects
|
|
3435
|
+
function $rejectPendingCalls(handler) {
|
|
3436
|
+
const handlerResults = Array.from(_rpcPromiseMap.values()).map(({ method, reject })=>{
|
|
3437
|
+
if (!handler) return reject(/* @__PURE__ */ new Error(`[birpc]: rejected pending call "${method}".`));
|
|
3438
|
+
return handler({
|
|
3439
|
+
method,
|
|
3440
|
+
reject
|
|
3441
|
+
});
|
|
4230
3442
|
});
|
|
4231
|
-
|
|
4232
|
-
|
|
4233
|
-
|
|
4234
|
-
|
|
4235
|
-
|
|
3443
|
+
_rpcPromiseMap.clear();
|
|
3444
|
+
return handlerResults;
|
|
3445
|
+
}
|
|
3446
|
+
async function onMessage(data, ...extra) {
|
|
3447
|
+
let msg;
|
|
4236
3448
|
try {
|
|
4237
|
-
|
|
4238
|
-
|
|
4239
|
-
|
|
4240
|
-
|
|
4241
|
-
timeoutFallbackMs
|
|
4242
|
-
});
|
|
4243
|
-
} catch (error) {
|
|
4244
|
-
if (error instanceof Error) throw error.message;
|
|
4245
|
-
throw String(error);
|
|
3449
|
+
msg = deserialize(data);
|
|
3450
|
+
} catch (e) {
|
|
3451
|
+
if (options.onGeneralError?.call(rpc, e) !== true) throw e;
|
|
3452
|
+
return;
|
|
4246
3453
|
}
|
|
4247
|
-
|
|
4248
|
-
|
|
4249
|
-
|
|
4250
|
-
|
|
4251
|
-
|
|
4252
|
-
|
|
3454
|
+
if (msg.t === TYPE_REQUEST) {
|
|
3455
|
+
const { m: method, a: args, o: optional } = msg;
|
|
3456
|
+
let result, error;
|
|
3457
|
+
let fn = await (resolver ? resolver.call(rpc, method, $functions[method]) : $functions[method]);
|
|
3458
|
+
if (optional) fn ||= ()=>void 0;
|
|
3459
|
+
if (fn) try {
|
|
3460
|
+
result = await fn.apply("rpc" === bind ? rpc : $functions, args);
|
|
3461
|
+
} catch (e) {
|
|
3462
|
+
error = e;
|
|
3463
|
+
}
|
|
3464
|
+
else error = /* @__PURE__ */ new Error(`[birpc] function "${method}" not found`);
|
|
3465
|
+
if (msg.i) {
|
|
3466
|
+
if (error && options.onFunctionError) {
|
|
3467
|
+
if (true === options.onFunctionError.call(rpc, error, method, args)) return;
|
|
3468
|
+
}
|
|
3469
|
+
if (!error) try {
|
|
3470
|
+
await post(serialize({
|
|
3471
|
+
t: TYPE_RESPONSE,
|
|
3472
|
+
i: msg.i,
|
|
3473
|
+
r: result
|
|
3474
|
+
}), ...extra);
|
|
3475
|
+
return;
|
|
3476
|
+
} catch (e) {
|
|
3477
|
+
error = e;
|
|
3478
|
+
if (options.onGeneralError?.call(rpc, e, method, args) !== true) throw e;
|
|
3479
|
+
}
|
|
3480
|
+
try {
|
|
3481
|
+
await post(serialize({
|
|
3482
|
+
t: TYPE_RESPONSE,
|
|
3483
|
+
i: msg.i,
|
|
3484
|
+
e: error
|
|
3485
|
+
}), ...extra);
|
|
3486
|
+
} catch (e) {
|
|
3487
|
+
if (options.onGeneralError?.call(rpc, e, method, args) !== true) throw e;
|
|
3488
|
+
}
|
|
3489
|
+
}
|
|
3490
|
+
} else {
|
|
3491
|
+
const { i: ack, r: result, e: error } = msg;
|
|
3492
|
+
const promise = _rpcPromiseMap.get(ack);
|
|
3493
|
+
if (promise) {
|
|
3494
|
+
dist_clearTimeout(promise.timeoutId);
|
|
3495
|
+
if (error) promise.reject(error);
|
|
3496
|
+
else promise.resolve(result);
|
|
3497
|
+
}
|
|
3498
|
+
_rpcPromiseMap.delete(ack);
|
|
3499
|
+
}
|
|
3500
|
+
}
|
|
3501
|
+
_promiseInit = on(onMessage);
|
|
3502
|
+
return rpc;
|
|
3503
|
+
}
|
|
3504
|
+
class ContainerRpcManager {
|
|
3505
|
+
wss;
|
|
3506
|
+
ws = null;
|
|
3507
|
+
rpc = null;
|
|
3508
|
+
methods;
|
|
3509
|
+
onDisconnect;
|
|
3510
|
+
detachActiveSocketListeners = null;
|
|
3511
|
+
constructor(wss, methods, onDisconnect){
|
|
3512
|
+
this.wss = wss;
|
|
3513
|
+
this.methods = methods;
|
|
3514
|
+
this.onDisconnect = onDisconnect;
|
|
3515
|
+
this.setupConnectionHandler();
|
|
3516
|
+
}
|
|
3517
|
+
updateMethods(methods, onDisconnect) {
|
|
3518
|
+
this.methods = methods;
|
|
3519
|
+
this.onDisconnect = onDisconnect;
|
|
3520
|
+
if (this.ws && this.ws.readyState === this.ws.OPEN) this.attachWebSocket(this.ws);
|
|
3521
|
+
}
|
|
3522
|
+
setupConnectionHandler() {
|
|
3523
|
+
this.wss.on('connection', (ws)=>{
|
|
3524
|
+
logger.debug('[Browser UI] Container WebSocket connected');
|
|
3525
|
+
logger.debug(`[Browser UI] Current ws: ${this.ws ? 'exists' : 'null'}, new ws: ${ws ? 'exists' : 'null'}`);
|
|
3526
|
+
this.attachWebSocket(ws);
|
|
3527
|
+
});
|
|
3528
|
+
}
|
|
3529
|
+
attachWebSocket(ws) {
|
|
3530
|
+
this.detachActiveSocketListeners?.();
|
|
3531
|
+
if (this.rpc && !this.rpc.$closed) this.rpc.$close(new Error('Container RPC transport reattached'));
|
|
3532
|
+
this.ws = ws;
|
|
3533
|
+
const messageHandlers = new WeakMap();
|
|
3534
|
+
this.rpc = createBirpc(this.methods, {
|
|
3535
|
+
timeout: -1,
|
|
3536
|
+
post: (data)=>{
|
|
3537
|
+
if (ws.readyState === ws.OPEN) ws.send(JSON.stringify(data));
|
|
3538
|
+
},
|
|
3539
|
+
on: (fn)=>{
|
|
3540
|
+
const handler = (message)=>{
|
|
3541
|
+
try {
|
|
3542
|
+
const data = JSON.parse(message.toString());
|
|
3543
|
+
fn(data);
|
|
3544
|
+
} catch {}
|
|
3545
|
+
};
|
|
3546
|
+
messageHandlers.set(fn, handler);
|
|
3547
|
+
ws.on('message', handler);
|
|
3548
|
+
},
|
|
3549
|
+
off: (fn)=>{
|
|
3550
|
+
const handler = messageHandlers.get(fn);
|
|
3551
|
+
if (!handler) return;
|
|
3552
|
+
ws.off('message', handler);
|
|
3553
|
+
messageHandlers.delete(fn);
|
|
3554
|
+
}
|
|
4253
3555
|
});
|
|
3556
|
+
const handleClose = ()=>{
|
|
3557
|
+
if (this.ws === ws) this.ws = null;
|
|
3558
|
+
this.detachActiveSocketListeners?.();
|
|
3559
|
+
this.detachActiveSocketListeners = null;
|
|
3560
|
+
if (this.rpc && !this.rpc.$closed) {
|
|
3561
|
+
const disconnectError = new Error('Browser UI WebSocket disconnected before reload completed');
|
|
3562
|
+
this.rpc.$close(disconnectError);
|
|
3563
|
+
this.onDisconnect?.(disconnectError);
|
|
3564
|
+
}
|
|
3565
|
+
this.rpc = null;
|
|
3566
|
+
};
|
|
3567
|
+
ws.on('close', handleClose);
|
|
3568
|
+
this.detachActiveSocketListeners = ()=>{
|
|
3569
|
+
ws.off('close', handleClose);
|
|
3570
|
+
};
|
|
3571
|
+
}
|
|
3572
|
+
get isConnected() {
|
|
3573
|
+
return null !== this.ws && this.ws.readyState === this.ws.OPEN;
|
|
3574
|
+
}
|
|
3575
|
+
get currentWebSocket() {
|
|
3576
|
+
return this.ws;
|
|
3577
|
+
}
|
|
3578
|
+
reattach(ws) {
|
|
3579
|
+
this.attachWebSocket(ws);
|
|
3580
|
+
}
|
|
3581
|
+
async notifyTestFileUpdate(files) {
|
|
3582
|
+
await this.rpc?.onTestFileUpdate(files);
|
|
3583
|
+
}
|
|
3584
|
+
async updateHostConfig(config) {
|
|
3585
|
+
await this.rpc?.onHostConfigUpdate(config);
|
|
3586
|
+
}
|
|
3587
|
+
async reloadTestFile(testFile, testNamePattern) {
|
|
3588
|
+
logger.debug(`[Browser UI] reloadTestFile called, rpc: ${this.rpc ? 'exists' : 'null'}, ws: ${this.ws ? 'exists' : 'null'}`);
|
|
3589
|
+
if (!this.rpc) throw new Error('Browser UI RPC not available for reloadTestFile');
|
|
3590
|
+
logger.debug(`[Browser UI] Calling reloadTestFile: ${testFile}`);
|
|
3591
|
+
return this.rpc.reloadTestFile(testFile, testNamePattern);
|
|
3592
|
+
}
|
|
3593
|
+
}
|
|
3594
|
+
const createHeadedSerialTaskQueue = ()=>{
|
|
3595
|
+
let queue = Promise.resolve();
|
|
3596
|
+
const enqueue = (task)=>{
|
|
3597
|
+
const next = queue.then(task);
|
|
3598
|
+
queue = next.catch(()=>{});
|
|
3599
|
+
return next;
|
|
3600
|
+
};
|
|
3601
|
+
return {
|
|
3602
|
+
enqueue
|
|
3603
|
+
};
|
|
3604
|
+
};
|
|
3605
|
+
const getFileTaskId = (testPath)=>`file:${testPath}`;
|
|
3606
|
+
const toError = (error)=>error instanceof Error ? error : new Error(String(error));
|
|
3607
|
+
const createDeferredPromise = ()=>{
|
|
3608
|
+
let resolve;
|
|
3609
|
+
let reject;
|
|
3610
|
+
const promise = new Promise((res, rej)=>{
|
|
3611
|
+
resolve = res;
|
|
3612
|
+
reject = rej;
|
|
4254
3613
|
});
|
|
4255
|
-
|
|
4256
|
-
|
|
4257
|
-
|
|
4258
|
-
|
|
4259
|
-
|
|
4260
|
-
|
|
4261
|
-
|
|
3614
|
+
return {
|
|
3615
|
+
promise,
|
|
3616
|
+
resolve,
|
|
3617
|
+
reject
|
|
3618
|
+
};
|
|
3619
|
+
};
|
|
3620
|
+
const claimHeadedCycleScope = (testPaths, currentTestFiles, pendingTestNamePatterns)=>{
|
|
3621
|
+
const scope = [];
|
|
3622
|
+
const filesByPath = new Map(currentTestFiles.map((file)=>[
|
|
3623
|
+
file.testPath,
|
|
3624
|
+
file
|
|
4262
3625
|
]));
|
|
4263
|
-
const
|
|
4264
|
-
|
|
4265
|
-
|
|
4266
|
-
|
|
4267
|
-
|
|
4268
|
-
|
|
3626
|
+
for (const testPath of testPaths){
|
|
3627
|
+
const normalizedTestPath = normalize(testPath);
|
|
3628
|
+
const file = filesByPath.get(normalizedTestPath);
|
|
3629
|
+
if (file) {
|
|
3630
|
+
scope.push({
|
|
3631
|
+
file,
|
|
3632
|
+
testNamePattern: pendingTestNamePatterns.get(normalizedTestPath)
|
|
3633
|
+
});
|
|
3634
|
+
pendingTestNamePatterns.delete(normalizedTestPath);
|
|
3635
|
+
}
|
|
3636
|
+
}
|
|
3637
|
+
return scope;
|
|
3638
|
+
};
|
|
3639
|
+
const createHeadedScheduler = async ({ context, runtime, allTestFiles, hostOptions, isWatchMode, createDispatchRouter, handlers: { handleTestFileStart, handleTestCaseResult, handleTestFileComplete, handleLog, handleFatal }, fatalErrorRef, watchSignals, setDispatchPageResolver, createWatchSession, collectProjectEntries, logWatchReady, destroyRuntime })=>{
|
|
3640
|
+
const { browser, browserLaunchOptions, watchState, wss } = runtime;
|
|
3641
|
+
let currentTestFiles = allTestFiles;
|
|
3642
|
+
const RUNNER_FRAMES_READY_TIMEOUT_MS = 30000;
|
|
3643
|
+
let currentRunnerFramesSignature = null;
|
|
3644
|
+
const runnerFramesWaiters = new Map();
|
|
3645
|
+
const createTestFilesSignature = (testFiles)=>JSON.stringify(testFiles.map((testFile)=>normalize(testFile)));
|
|
3646
|
+
const markRunnerFramesReady = (testFiles)=>{
|
|
3647
|
+
const signature = createTestFilesSignature(testFiles);
|
|
3648
|
+
currentRunnerFramesSignature = signature;
|
|
3649
|
+
const waiters = runnerFramesWaiters.get(signature);
|
|
3650
|
+
if (!waiters) return;
|
|
3651
|
+
runnerFramesWaiters.delete(signature);
|
|
3652
|
+
for (const waiter of waiters)waiter();
|
|
4269
3653
|
};
|
|
4270
|
-
const
|
|
4271
|
-
|
|
4272
|
-
|
|
4273
|
-
|
|
4274
|
-
|
|
4275
|
-
|
|
4276
|
-
|
|
4277
|
-
|
|
4278
|
-
|
|
4279
|
-
|
|
4280
|
-
|
|
4281
|
-
|
|
4282
|
-
|
|
4283
|
-
|
|
4284
|
-
|
|
4285
|
-
}
|
|
4286
|
-
|
|
3654
|
+
const waitForRunnerFramesReady = async (testFiles)=>{
|
|
3655
|
+
const signature = createTestFilesSignature(testFiles);
|
|
3656
|
+
if (currentRunnerFramesSignature === signature) return;
|
|
3657
|
+
await new Promise((resolve, reject)=>{
|
|
3658
|
+
const waiters = runnerFramesWaiters.get(signature) ?? new Set();
|
|
3659
|
+
const cleanup = ()=>{
|
|
3660
|
+
const currentWaiters = runnerFramesWaiters.get(signature);
|
|
3661
|
+
if (!currentWaiters) return;
|
|
3662
|
+
currentWaiters.delete(onReady);
|
|
3663
|
+
if (0 === currentWaiters.size) runnerFramesWaiters.delete(signature);
|
|
3664
|
+
};
|
|
3665
|
+
const onReady = ()=>{
|
|
3666
|
+
if (timeoutId) clearTimeout(timeoutId);
|
|
3667
|
+
cleanup();
|
|
3668
|
+
resolve();
|
|
3669
|
+
};
|
|
3670
|
+
const timeoutId = setTimeout(()=>{
|
|
3671
|
+
cleanup();
|
|
3672
|
+
reject(new Error(`Timed out waiting for headed runner frames to be ready for ${testFiles.length} file(s).`));
|
|
3673
|
+
}, RUNNER_FRAMES_READY_TIMEOUT_MS);
|
|
3674
|
+
waiters.add(onReady);
|
|
3675
|
+
runnerFramesWaiters.set(signature, waiters);
|
|
3676
|
+
if (currentRunnerFramesSignature === signature) onReady();
|
|
3677
|
+
});
|
|
3678
|
+
};
|
|
3679
|
+
const getTestFileInfo = (testFile)=>{
|
|
3680
|
+
const normalizedTestFile = normalize(testFile);
|
|
3681
|
+
const fileInfo = currentTestFiles.find((file)=>file.testPath === normalizedTestFile);
|
|
3682
|
+
if (!fileInfo) throw new Error(`Unknown browser test file: ${JSON.stringify(testFile)}`);
|
|
3683
|
+
return fileInfo;
|
|
3684
|
+
};
|
|
3685
|
+
let containerContext;
|
|
3686
|
+
let containerPage;
|
|
3687
|
+
let isNewPage = false;
|
|
3688
|
+
if (isWatchMode && runtime.containerPage && runtime.containerContext) {
|
|
3689
|
+
containerContext = runtime.containerContext;
|
|
3690
|
+
containerPage = runtime.containerPage;
|
|
3691
|
+
logger.log(color.gray('\n[Watch] Reusing existing container page\n'));
|
|
3692
|
+
} else {
|
|
3693
|
+
isNewPage = true;
|
|
3694
|
+
containerContext = await browser.newContext({
|
|
3695
|
+
providerOptions: browserLaunchOptions.providerOptions,
|
|
3696
|
+
viewport: null
|
|
3697
|
+
});
|
|
3698
|
+
containerPage = await containerContext.newPage();
|
|
3699
|
+
containerPage.on('popup', async (popup)=>{
|
|
3700
|
+
await popup.close().catch(()=>{});
|
|
3701
|
+
});
|
|
3702
|
+
containerContext.on('page', async (page)=>{
|
|
3703
|
+
if (page !== containerPage) await page.close().catch(()=>{});
|
|
3704
|
+
});
|
|
3705
|
+
if (isWatchMode) {
|
|
3706
|
+
runtime.containerPage = containerPage;
|
|
3707
|
+
runtime.containerContext = containerContext;
|
|
3708
|
+
}
|
|
3709
|
+
containerPage.on('console', (msg)=>{
|
|
3710
|
+
const text = msg.text();
|
|
3711
|
+
if (text.startsWith('[Container]') || text.startsWith('[Runner]')) logger.log(color.gray(`[Browser Console] ${text}`));
|
|
3712
|
+
});
|
|
3713
|
+
}
|
|
3714
|
+
setDispatchPageResolver(()=>({
|
|
3715
|
+
containerPage
|
|
3716
|
+
}));
|
|
3717
|
+
const dispatchRouter = createDispatchRouter();
|
|
3718
|
+
const headedReloadQueue = createHeadedSerialTaskQueue();
|
|
3719
|
+
const pendingHeadedReloads = new Map();
|
|
3720
|
+
let enqueueHeadedReload = async (_file, _testNamePattern)=>{
|
|
3721
|
+
throw new Error('Headed reload queue is not initialized');
|
|
3722
|
+
};
|
|
3723
|
+
const rejectPendingHeadedReload = (testPath, error, runId)=>{
|
|
3724
|
+
const pending = pendingHeadedReloads.get(testPath);
|
|
3725
|
+
if (!pending) return;
|
|
3726
|
+
if (runId && pending.runId !== runId) return;
|
|
3727
|
+
pendingHeadedReloads.delete(testPath);
|
|
3728
|
+
pending.deferred.reject(error);
|
|
3729
|
+
};
|
|
3730
|
+
const rejectAllPendingHeadedReloads = (error)=>{
|
|
3731
|
+
for (const [testPath, pending] of pendingHeadedReloads){
|
|
3732
|
+
pendingHeadedReloads.delete(testPath);
|
|
3733
|
+
pending.deferred.reject(error);
|
|
3734
|
+
}
|
|
3735
|
+
};
|
|
3736
|
+
const registerPendingHeadedReload = (testPath, runId)=>{
|
|
3737
|
+
const previousPending = pendingHeadedReloads.get(testPath);
|
|
3738
|
+
if (previousPending) {
|
|
3739
|
+
previousPending.deferred.reject(new Error(`Reload for "${testPath}" was superseded by a newer request.`));
|
|
3740
|
+
pendingHeadedReloads.delete(testPath);
|
|
3741
|
+
}
|
|
3742
|
+
const deferred = createDeferredPromise();
|
|
3743
|
+
pendingHeadedReloads.set(testPath, {
|
|
3744
|
+
runId,
|
|
3745
|
+
deferred
|
|
3746
|
+
});
|
|
3747
|
+
return deferred.promise;
|
|
3748
|
+
};
|
|
3749
|
+
const resolvePendingHeadedReload = (testPath, runId)=>{
|
|
3750
|
+
const pending = pendingHeadedReloads.get(testPath);
|
|
3751
|
+
if (!pending) return;
|
|
3752
|
+
if (runId && pending.runId !== runId) return void logger.debug(`[Browser UI] Ignoring stale file-complete for ${testPath}. current=${pending.runId}, incoming=${runId}`);
|
|
3753
|
+
pendingHeadedReloads.delete(testPath);
|
|
3754
|
+
pending.deferred.resolve();
|
|
3755
|
+
};
|
|
3756
|
+
const reloadTestFileAndWait = async (file, testNamePattern)=>{
|
|
3757
|
+
let reloadAck;
|
|
3758
|
+
try {
|
|
3759
|
+
reloadAck = await rpcManager.reloadTestFile(file.testPath, testNamePattern);
|
|
3760
|
+
await registerPendingHeadedReload(file.testPath, reloadAck.runId);
|
|
3761
|
+
} catch (error) {
|
|
3762
|
+
if (reloadAck?.runId) rejectPendingHeadedReload(file.testPath, toError(error), reloadAck.runId);
|
|
3763
|
+
throw error;
|
|
3764
|
+
}
|
|
3765
|
+
};
|
|
3766
|
+
let runUiRequestedRerun = async (file, testNamePattern)=>{
|
|
3767
|
+
await enqueueHeadedReload(file, testNamePattern);
|
|
3768
|
+
};
|
|
3769
|
+
const createRpcMethods = ()=>({
|
|
3770
|
+
async rerunTest (testFile, testNamePattern) {
|
|
3771
|
+
const projectName = context.normalizedConfig.name || 'project';
|
|
3772
|
+
const relativePath = relative(context.rootPath, testFile);
|
|
3773
|
+
const displayPath = `<${projectName}>/${relativePath}`;
|
|
3774
|
+
logger.log(color.cyan(`\nRe-running test: ${displayPath}${testNamePattern ? ` (pattern: ${testNamePattern})` : ''}\n`));
|
|
3775
|
+
await runUiRequestedRerun(getTestFileInfo(testFile), testNamePattern);
|
|
3776
|
+
},
|
|
3777
|
+
async getTestFiles () {
|
|
3778
|
+
return currentTestFiles;
|
|
3779
|
+
},
|
|
3780
|
+
async onRunnerFramesReady (testFiles) {
|
|
3781
|
+
markRunnerFramesReady(testFiles);
|
|
3782
|
+
},
|
|
3783
|
+
async onTestFileStart (payload) {
|
|
3784
|
+
await handleTestFileStart(payload);
|
|
3785
|
+
},
|
|
3786
|
+
async onTestCaseResult (payload) {
|
|
3787
|
+
await handleTestCaseResult(payload);
|
|
3788
|
+
},
|
|
3789
|
+
async onTestFileComplete (payload) {
|
|
3790
|
+
try {
|
|
3791
|
+
await handleTestFileComplete(payload);
|
|
3792
|
+
resolvePendingHeadedReload(payload.testPath, payload.runId);
|
|
3793
|
+
} catch (error) {
|
|
3794
|
+
rejectPendingHeadedReload(payload.testPath, toError(error), payload.runId);
|
|
3795
|
+
throw error;
|
|
3796
|
+
}
|
|
3797
|
+
},
|
|
3798
|
+
async onLog (payload) {
|
|
3799
|
+
await handleLog(payload);
|
|
3800
|
+
},
|
|
3801
|
+
async onFatal (payload) {
|
|
3802
|
+
const error = new Error(payload.message);
|
|
3803
|
+
error.stack = payload.stack;
|
|
3804
|
+
rejectAllPendingHeadedReloads(error);
|
|
3805
|
+
await handleFatal(payload);
|
|
3806
|
+
},
|
|
3807
|
+
async dispatch (request) {
|
|
3808
|
+
return dispatchRouter.dispatch(request);
|
|
3809
|
+
}
|
|
3810
|
+
});
|
|
3811
|
+
let rpcManager;
|
|
3812
|
+
if (isWatchMode && runtime.rpcManager) {
|
|
3813
|
+
rpcManager = runtime.rpcManager;
|
|
3814
|
+
rpcManager.updateMethods(createRpcMethods(), rejectAllPendingHeadedReloads);
|
|
3815
|
+
const existingWs = rpcManager.currentWebSocket;
|
|
3816
|
+
if (existingWs) rpcManager.reattach(existingWs);
|
|
3817
|
+
} else {
|
|
3818
|
+
rpcManager = new ContainerRpcManager(wss, createRpcMethods(), rejectAllPendingHeadedReloads);
|
|
3819
|
+
if (isWatchMode) runtime.rpcManager = rpcManager;
|
|
3820
|
+
}
|
|
3821
|
+
if (isNewPage) {
|
|
3822
|
+
const pagePath = '/';
|
|
3823
|
+
const containerPort = runtime.containerServer.port;
|
|
3824
|
+
await containerPage.goto(`http://localhost:${containerPort}${pagePath}`, {
|
|
3825
|
+
waitUntil: 'load'
|
|
3826
|
+
});
|
|
3827
|
+
logger.log(color.cyan(`\nBrowser mode opened at http://localhost:${containerPort}${pagePath}\n`));
|
|
3828
|
+
}
|
|
3829
|
+
enqueueHeadedReload = async (file, testNamePattern)=>headedReloadQueue.enqueue(async ()=>{
|
|
3830
|
+
if (fatalErrorRef.current) return;
|
|
3831
|
+
await reloadTestFileAndWait(file, testNamePattern);
|
|
3832
|
+
});
|
|
3833
|
+
let testTime = 0;
|
|
3834
|
+
if (currentTestFiles.length > 0) {
|
|
3835
|
+
const testStart = Date.now();
|
|
3836
|
+
try {
|
|
3837
|
+
await waitForRunnerFramesReady(currentTestFiles.map((file)=>file.testPath));
|
|
3838
|
+
for (const file of currentTestFiles){
|
|
3839
|
+
await enqueueHeadedReload(file);
|
|
3840
|
+
if (fatalErrorRef.current) break;
|
|
3841
|
+
}
|
|
3842
|
+
} catch (error) {
|
|
3843
|
+
fatalErrorRef.current = fatalErrorRef.current ?? toError(error);
|
|
3844
|
+
}
|
|
3845
|
+
testTime = Date.now() - testStart;
|
|
3846
|
+
}
|
|
3847
|
+
let watchSession;
|
|
3848
|
+
if (isWatchMode) {
|
|
3849
|
+
const pendingTestNamePatterns = new Map();
|
|
3850
|
+
const runScope = async (testPaths)=>{
|
|
3851
|
+
const cycleScope = claimHeadedCycleScope(testPaths, currentTestFiles, pendingTestNamePatterns);
|
|
3852
|
+
for (const { file, testNamePattern } of cycleScope)await enqueueHeadedReload(file, testNamePattern);
|
|
3853
|
+
};
|
|
3854
|
+
const refreshHostConfig = async ()=>{
|
|
3855
|
+
const refreshedHostOptions = {
|
|
3856
|
+
...hostOptions,
|
|
3857
|
+
snapshot: {
|
|
3858
|
+
updateSnapshot: context.snapshotManager.options.updateSnapshot
|
|
3859
|
+
}
|
|
3860
|
+
};
|
|
3861
|
+
runtime.setContainerOptions(refreshedHostOptions);
|
|
3862
|
+
await rpcManager.updateHostConfig(refreshedHostOptions);
|
|
3863
|
+
};
|
|
3864
|
+
watchSignals.setDispatchRerun(async ()=>{
|
|
3865
|
+
const [, newProjectEntries] = await Promise.all([
|
|
3866
|
+
refreshHostConfig(),
|
|
3867
|
+
collectProjectEntries()
|
|
3868
|
+
]);
|
|
3869
|
+
const rerunPlan = planWatchRerun({
|
|
3870
|
+
projectEntries: newProjectEntries,
|
|
3871
|
+
previousTestFiles: watchState.lastTestFiles,
|
|
3872
|
+
affectedTestFiles: drainPendingAffectedTestFiles(watchState)
|
|
3873
|
+
});
|
|
3874
|
+
if (rerunPlan.filesChanged) {
|
|
3875
|
+
const deletedTestPaths = collectDeletedTestPaths(watchState.lastTestFiles, rerunPlan.currentTestFiles);
|
|
3876
|
+
if (deletedTestPaths.length > 0) context.updateReporterResultState([], [], deletedTestPaths);
|
|
3877
|
+
watchState.lastTestFiles = rerunPlan.currentTestFiles;
|
|
3878
|
+
currentTestFiles = rerunPlan.currentTestFiles;
|
|
3879
|
+
await rpcManager.notifyTestFileUpdate(currentTestFiles);
|
|
3880
|
+
if (0 === currentTestFiles.length) {
|
|
3881
|
+
logger.log(color.cyan('No browser test files remain after update.\n'));
|
|
3882
|
+
logWatchReady();
|
|
3883
|
+
return;
|
|
3884
|
+
}
|
|
3885
|
+
await waitForRunnerFramesReady(currentTestFiles.map((file)=>file.testPath));
|
|
3886
|
+
}
|
|
3887
|
+
if (rerunPlan.normalizedAffectedTestFiles.length > 0) {
|
|
3888
|
+
logger.log(color.cyan(`Re-running ${rerunPlan.normalizedAffectedTestFiles.length} affected test file(s)...\n`));
|
|
3889
|
+
await watchSignals.signalInvalidation(rerunPlan.normalizedAffectedTestFiles);
|
|
3890
|
+
return;
|
|
3891
|
+
}
|
|
3892
|
+
if (!rerunPlan.filesChanged) logger.log(color.cyan('Tests will be re-executed automatically\n'));
|
|
3893
|
+
logWatchReady();
|
|
3894
|
+
});
|
|
3895
|
+
runUiRequestedRerun = async (file, testNamePattern)=>{
|
|
3896
|
+
await refreshHostConfig();
|
|
3897
|
+
await watchSignals.signalInvalidation([
|
|
3898
|
+
file.testPath
|
|
3899
|
+
], ()=>{
|
|
3900
|
+
if (void 0 === testNamePattern) pendingTestNamePatterns.delete(normalize(file.testPath));
|
|
3901
|
+
else pendingTestNamePatterns.set(normalize(file.testPath), testNamePattern);
|
|
3902
|
+
});
|
|
3903
|
+
await watchSignals.awaitSignalledCycle();
|
|
3904
|
+
};
|
|
3905
|
+
watchSession = createWatchSession(runScope);
|
|
3906
|
+
}
|
|
3907
|
+
const closeContainerRuntime = isWatchMode ? void 0 : async ()=>{
|
|
3908
|
+
try {
|
|
3909
|
+
await containerPage.close();
|
|
3910
|
+
} catch {}
|
|
3911
|
+
try {
|
|
3912
|
+
await containerContext.close();
|
|
3913
|
+
} catch {}
|
|
3914
|
+
await destroyRuntime();
|
|
3915
|
+
};
|
|
3916
|
+
return {
|
|
3917
|
+
testTime,
|
|
3918
|
+
watchSession,
|
|
3919
|
+
close: closeContainerRuntime
|
|
3920
|
+
};
|
|
3921
|
+
};
|
|
3922
|
+
const DEFAULT_MAX_HEADLESS_WORKERS = 12;
|
|
3923
|
+
const resolveHeadlessWorkerCount = ({ command, maxWorkers, totalTasks, numCpus = browser_getNumCpus() })=>{
|
|
3924
|
+
const base = Math.max(Math.min(DEFAULT_MAX_HEADLESS_WORKERS, numCpus - 1), 1);
|
|
3925
|
+
return resolveWorkerCount({
|
|
3926
|
+
command,
|
|
3927
|
+
maxWorkers,
|
|
3928
|
+
totalTasks,
|
|
3929
|
+
recommended: base,
|
|
3930
|
+
watchRecommended: Math.max(Math.floor(base / 2), 1),
|
|
3931
|
+
numCpus
|
|
3932
|
+
});
|
|
3933
|
+
};
|
|
3934
|
+
const getHeadlessConcurrency = (context, totalTests)=>resolveHeadlessWorkerCount({
|
|
3935
|
+
command: context.command,
|
|
3936
|
+
maxWorkers: context.normalizedConfig.pool.maxWorkers,
|
|
3937
|
+
totalTasks: totalTests
|
|
3938
|
+
});
|
|
3939
|
+
const attachHeadlessRunnerTransport = async (page, handlers)=>{
|
|
3940
|
+
await page.exposeFunction(DISPATCH_MESSAGE_TYPE, handlers.onDispatchMessage);
|
|
3941
|
+
await page.exposeFunction(DISPATCH_RPC_BRIDGE_NAME, handlers.onDispatchRpc);
|
|
3942
|
+
};
|
|
3943
|
+
const createCancelSignal = ()=>{
|
|
3944
|
+
let settled = false;
|
|
3945
|
+
let resolveSignal = ()=>{};
|
|
3946
|
+
const signal = new Promise((resolve)=>{
|
|
3947
|
+
resolveSignal = ()=>{
|
|
3948
|
+
if (!settled) {
|
|
3949
|
+
settled = true;
|
|
3950
|
+
resolve();
|
|
3951
|
+
}
|
|
3952
|
+
};
|
|
3953
|
+
});
|
|
3954
|
+
return {
|
|
3955
|
+
signal,
|
|
3956
|
+
resolve: resolveSignal
|
|
3957
|
+
};
|
|
3958
|
+
};
|
|
3959
|
+
const createRunSession = (token)=>{
|
|
3960
|
+
const { signal, resolve } = createCancelSignal();
|
|
3961
|
+
return {
|
|
3962
|
+
token,
|
|
3963
|
+
cancelled: false,
|
|
3964
|
+
cancelSignal: signal,
|
|
3965
|
+
signalCancel: resolve
|
|
3966
|
+
};
|
|
3967
|
+
};
|
|
3968
|
+
class RunSessionLifecycle {
|
|
3969
|
+
currentToken = 0;
|
|
3970
|
+
active = null;
|
|
3971
|
+
get activeSession() {
|
|
3972
|
+
return this.active;
|
|
3973
|
+
}
|
|
3974
|
+
get activeToken() {
|
|
3975
|
+
return this.currentToken;
|
|
3976
|
+
}
|
|
3977
|
+
createSession(factory) {
|
|
3978
|
+
const session = factory(++this.currentToken);
|
|
3979
|
+
this.active = session;
|
|
3980
|
+
return session;
|
|
3981
|
+
}
|
|
3982
|
+
isTokenActive(token) {
|
|
3983
|
+
return token === this.currentToken;
|
|
3984
|
+
}
|
|
3985
|
+
isTokenStale(token) {
|
|
3986
|
+
return !this.isTokenActive(token);
|
|
3987
|
+
}
|
|
3988
|
+
invalidateActiveToken() {
|
|
3989
|
+
this.currentToken += 1;
|
|
3990
|
+
return this.currentToken;
|
|
3991
|
+
}
|
|
3992
|
+
clearIfActive(session) {
|
|
3993
|
+
if (this.active === session) this.active = null;
|
|
3994
|
+
}
|
|
3995
|
+
async cancel(session, options) {
|
|
3996
|
+
const waitForDone = options?.waitForDone ?? true;
|
|
3997
|
+
if (!session.cancelled) {
|
|
3998
|
+
session.cancelled = true;
|
|
3999
|
+
session.signalCancel();
|
|
4000
|
+
await options?.onCancel?.(session);
|
|
4001
|
+
}
|
|
4002
|
+
if (waitForDone) await session.done?.catch(()=>{});
|
|
4003
|
+
}
|
|
4004
|
+
async cancelActive(options) {
|
|
4005
|
+
if (!this.active) return;
|
|
4006
|
+
await this.cancel(this.active, options);
|
|
4007
|
+
}
|
|
4008
|
+
}
|
|
4009
|
+
class RunnerSessionRegistry {
|
|
4010
|
+
nextId = 0;
|
|
4011
|
+
sessionsById = new Map();
|
|
4012
|
+
sessionIdByTestFile = new Map();
|
|
4013
|
+
register(input) {
|
|
4014
|
+
const id = input.id ?? `runner-session-${++this.nextId}`;
|
|
4015
|
+
const createdAt = input.createdAt ?? Date.now();
|
|
4016
|
+
const record = {
|
|
4017
|
+
...input,
|
|
4018
|
+
id,
|
|
4019
|
+
createdAt
|
|
4020
|
+
};
|
|
4021
|
+
this.sessionsById.set(id, record);
|
|
4022
|
+
this.sessionIdByTestFile.set(record.testFile, id);
|
|
4023
|
+
return record;
|
|
4024
|
+
}
|
|
4025
|
+
getById(id) {
|
|
4026
|
+
return this.sessionsById.get(id);
|
|
4027
|
+
}
|
|
4028
|
+
getByTestFile(testFile) {
|
|
4029
|
+
const id = this.sessionIdByTestFile.get(testFile);
|
|
4030
|
+
if (!id) return;
|
|
4031
|
+
return this.sessionsById.get(id);
|
|
4032
|
+
}
|
|
4033
|
+
list() {
|
|
4034
|
+
return Array.from(this.sessionsById.values());
|
|
4035
|
+
}
|
|
4036
|
+
listByRunToken(runToken) {
|
|
4037
|
+
return this.list().filter((session)=>session.runToken === runToken);
|
|
4038
|
+
}
|
|
4039
|
+
deleteById(id) {
|
|
4040
|
+
const record = this.sessionsById.get(id);
|
|
4041
|
+
if (!record) return false;
|
|
4042
|
+
this.sessionsById.delete(id);
|
|
4043
|
+
if (this.sessionIdByTestFile.get(record.testFile) === id) this.sessionIdByTestFile.delete(record.testFile);
|
|
4044
|
+
return true;
|
|
4045
|
+
}
|
|
4046
|
+
deleteByTestFile(testFile) {
|
|
4047
|
+
const id = this.sessionIdByTestFile.get(testFile);
|
|
4048
|
+
if (!id) return false;
|
|
4049
|
+
return this.deleteById(id);
|
|
4050
|
+
}
|
|
4051
|
+
clear() {
|
|
4052
|
+
this.sessionsById.clear();
|
|
4053
|
+
this.sessionIdByTestFile.clear();
|
|
4054
|
+
}
|
|
4055
|
+
}
|
|
4056
|
+
const createHeadlessScheduler = async ({ context, browser, browserLaunchOptions, projectServers, allTestFiles, projectRuntimeConfigs, hostOptions, watchState, isWatchMode, createDispatchRouter, handlers: { handleFatal, handleTestFileComplete }, watchSignals, setDispatchPageResolver, createWatchSession, collectProjectEntries, logWatchReady, destroyRuntime })=>{
|
|
4057
|
+
const viewportByProject = mapViewportByProject(projectRuntimeConfigs);
|
|
4058
|
+
const runLifecycle = new RunSessionLifecycle();
|
|
4059
|
+
const sessionRegistry = new RunnerSessionRegistry();
|
|
4060
|
+
setDispatchPageResolver((target)=>({
|
|
4061
|
+
runnerPage: target?.sessionId ? sessionRegistry.getById(target.sessionId)?.page : void 0
|
|
4062
|
+
}));
|
|
4063
|
+
let dispatchRequestCounter = 0;
|
|
4064
|
+
const nextDispatchRequestId = (namespace)=>`${namespace}-${++dispatchRequestCounter}`;
|
|
4065
|
+
const closeContextSafely = async (browserContext)=>{
|
|
4066
|
+
try {
|
|
4067
|
+
await browserContext.close();
|
|
4068
|
+
} catch {}
|
|
4069
|
+
};
|
|
4070
|
+
const cancelRun = async (run, waitForDone = true)=>{
|
|
4071
|
+
await runLifecycle.cancel(run, {
|
|
4072
|
+
waitForDone,
|
|
4073
|
+
onCancel: async (session)=>{
|
|
4074
|
+
await Promise.all(Array.from(session.contexts).map((browserContext)=>closeContextSafely(browserContext)));
|
|
4287
4075
|
}
|
|
4288
|
-
}
|
|
4289
|
-
|
|
4290
|
-
|
|
4291
|
-
|
|
4292
|
-
|
|
4293
|
-
});
|
|
4294
|
-
await promises.writeFile(filepath, content, 'utf-8');
|
|
4295
|
-
},
|
|
4296
|
-
async removeSnapshotFile (filepath) {
|
|
4297
|
-
try {
|
|
4298
|
-
await promises.unlink(filepath);
|
|
4299
|
-
} catch {}
|
|
4076
|
+
});
|
|
4077
|
+
};
|
|
4078
|
+
const dispatchRouter = createDispatchRouter({
|
|
4079
|
+
isRunTokenStale: (runToken)=>runLifecycle.isTokenStale(runToken),
|
|
4080
|
+
onStale: (request)=>{
|
|
4081
|
+
if ("runner" === request.namespace) logger.debug(`[Headless] Dropped stale message "${request.method}" for ${request.target?.testFile ?? 'unknown'}`);
|
|
4300
4082
|
}
|
|
4083
|
+
});
|
|
4084
|
+
const dispatchRunnerMessage = async (run, file, sessionId, message)=>{
|
|
4085
|
+
const response = await dispatchRouter.dispatch({
|
|
4086
|
+
requestId: nextDispatchRequestId("runner"),
|
|
4087
|
+
runToken: run.token,
|
|
4088
|
+
namespace: "runner",
|
|
4089
|
+
method: message.type,
|
|
4090
|
+
args: 'payload' in message ? message.payload : void 0,
|
|
4091
|
+
target: {
|
|
4092
|
+
sessionId,
|
|
4093
|
+
testFile: file.testPath,
|
|
4094
|
+
projectName: file.projectName
|
|
4095
|
+
}
|
|
4096
|
+
});
|
|
4097
|
+
if (response.stale) return;
|
|
4098
|
+
if (response.error) throw new Error(response.error);
|
|
4301
4099
|
};
|
|
4302
|
-
const
|
|
4303
|
-
|
|
4304
|
-
|
|
4305
|
-
|
|
4306
|
-
|
|
4307
|
-
|
|
4308
|
-
|
|
4100
|
+
const runSingleFile = async (run, file)=>{
|
|
4101
|
+
if (run.cancelled || runLifecycle.isTokenStale(run.token)) return;
|
|
4102
|
+
const viewport = viewportByProject.get(file.projectName);
|
|
4103
|
+
const browserContext = await browser.newContext({
|
|
4104
|
+
providerOptions: browserLaunchOptions.providerOptions,
|
|
4105
|
+
viewport: viewport ?? null
|
|
4106
|
+
});
|
|
4107
|
+
run.contexts.add(browserContext);
|
|
4108
|
+
let page = null;
|
|
4109
|
+
let sessionId = null;
|
|
4110
|
+
let settled = false;
|
|
4111
|
+
let resolveDone = null;
|
|
4112
|
+
const markDone = ()=>{
|
|
4113
|
+
if (!settled) {
|
|
4114
|
+
settled = true;
|
|
4115
|
+
resolveDone?.();
|
|
4116
|
+
}
|
|
4117
|
+
};
|
|
4118
|
+
const donePromise = new Promise((resolve)=>{
|
|
4119
|
+
resolveDone = resolve;
|
|
4120
|
+
});
|
|
4121
|
+
const crashDeferred = createDeferredPromise();
|
|
4122
|
+
const onPageDead = (reason)=>{
|
|
4123
|
+
if (settled || run.cancelled || !runLifecycle.isTokenActive(run.token)) return;
|
|
4124
|
+
settled = true;
|
|
4125
|
+
crashDeferred.resolve(reason);
|
|
4126
|
+
};
|
|
4127
|
+
try {
|
|
4128
|
+
page = await browserContext.newPage();
|
|
4129
|
+
page.on('crash', ()=>onPageDead(`Browser page crashed while running ${file.testPath}.`));
|
|
4130
|
+
page.on('close', ()=>onPageDead(`Browser page closed unexpectedly while running ${file.testPath}.`));
|
|
4131
|
+
const session = sessionRegistry.register({
|
|
4132
|
+
testFile: file.testPath,
|
|
4133
|
+
projectName: file.projectName,
|
|
4134
|
+
runToken: run.token,
|
|
4135
|
+
mode: 'headless-page',
|
|
4136
|
+
context: browserContext,
|
|
4137
|
+
page
|
|
4138
|
+
});
|
|
4139
|
+
sessionId = session.id;
|
|
4140
|
+
await attachHeadlessRunnerTransport(page, {
|
|
4141
|
+
onDispatchMessage: async (message)=>{
|
|
4142
|
+
try {
|
|
4143
|
+
await dispatchRunnerMessage(run, file, session.id, message);
|
|
4144
|
+
if ('file-complete' === message.type || 'complete' === message.type) markDone();
|
|
4145
|
+
else if ('fatal' === message.type) {
|
|
4146
|
+
markDone();
|
|
4147
|
+
await cancelRun(run, false);
|
|
4148
|
+
}
|
|
4149
|
+
} catch (error) {
|
|
4150
|
+
const formatted = toError(error);
|
|
4151
|
+
await handleFatal({
|
|
4152
|
+
message: formatted.message,
|
|
4153
|
+
stack: formatted.stack
|
|
4154
|
+
});
|
|
4155
|
+
markDone();
|
|
4156
|
+
await cancelRun(run, false);
|
|
4157
|
+
}
|
|
4309
4158
|
},
|
|
4310
|
-
|
|
4159
|
+
onDispatchRpc: async (request)=>dispatchRouter.dispatch({
|
|
4160
|
+
...request,
|
|
4161
|
+
runToken: run.token,
|
|
4162
|
+
target: {
|
|
4163
|
+
sessionId: session.id,
|
|
4164
|
+
testFile: file.testPath,
|
|
4165
|
+
projectName: file.projectName,
|
|
4166
|
+
...request.target
|
|
4167
|
+
}
|
|
4168
|
+
})
|
|
4311
4169
|
});
|
|
4312
|
-
|
|
4313
|
-
|
|
4170
|
+
const inlineOptions = {
|
|
4171
|
+
...hostOptions,
|
|
4172
|
+
snapshot: {
|
|
4173
|
+
updateSnapshot: context.snapshotManager.options.updateSnapshot
|
|
4174
|
+
},
|
|
4175
|
+
testFile: file.testPath,
|
|
4176
|
+
runId: `${run.token}:${session.id}`
|
|
4177
|
+
};
|
|
4178
|
+
const serializedOptions = serializeForInlineScript(inlineOptions);
|
|
4179
|
+
await page.addInitScript(`window.__RSTEST_BROWSER_OPTIONS__ = ${serializedOptions};`);
|
|
4180
|
+
const projectServer = projectServers.get(file.projectName);
|
|
4181
|
+
if (!projectServer) throw new Error(`No browser dev server for project "${file.projectName}" (test file: ${file.testPath}).`);
|
|
4182
|
+
await page.goto(`http://localhost:${projectServer.port}/runner.html`, {
|
|
4183
|
+
waitUntil: 'load'
|
|
4184
|
+
});
|
|
4185
|
+
const state = await Promise.race([
|
|
4186
|
+
donePromise.then(()=>({
|
|
4187
|
+
type: 'done'
|
|
4188
|
+
})),
|
|
4189
|
+
crashDeferred.promise.then((reason)=>({
|
|
4190
|
+
type: 'crash',
|
|
4191
|
+
reason
|
|
4192
|
+
})),
|
|
4193
|
+
run.cancelSignal.then(()=>({
|
|
4194
|
+
type: 'cancelled'
|
|
4195
|
+
}))
|
|
4196
|
+
]);
|
|
4197
|
+
if ('cancelled' === state.type) return;
|
|
4198
|
+
if ('crash' === state.type && runLifecycle.isTokenActive(run.token) && !run.cancelled) {
|
|
4199
|
+
await handleFatal({
|
|
4200
|
+
message: state.reason
|
|
4201
|
+
});
|
|
4202
|
+
await cancelRun(run, false);
|
|
4203
|
+
}
|
|
4204
|
+
} catch (error) {
|
|
4205
|
+
if (runLifecycle.isTokenActive(run.token) && !run.cancelled) {
|
|
4206
|
+
const formatted = toError(error);
|
|
4207
|
+
await handleFatal({
|
|
4208
|
+
message: formatted.message,
|
|
4209
|
+
stack: formatted.stack
|
|
4210
|
+
});
|
|
4211
|
+
await cancelRun(run, false);
|
|
4212
|
+
}
|
|
4213
|
+
} finally{
|
|
4214
|
+
const abandoned = run.cancelled || runLifecycle.isTokenStale(run.token);
|
|
4215
|
+
const teardown = async ()=>{
|
|
4216
|
+
if (page) try {
|
|
4217
|
+
await page.close();
|
|
4218
|
+
} catch {}
|
|
4219
|
+
await closeContextSafely(browserContext);
|
|
4220
|
+
};
|
|
4221
|
+
if (sessionId) sessionRegistry.deleteById(sessionId);
|
|
4222
|
+
run.contexts.delete(browserContext);
|
|
4223
|
+
if (abandoned) teardown();
|
|
4224
|
+
else await teardown();
|
|
4314
4225
|
}
|
|
4315
|
-
await sinkForProjectName(payload.projectName).onTestFileStart({
|
|
4316
|
-
testId: getFileTaskId(payload.testPath),
|
|
4317
|
-
testPath: payload.testPath,
|
|
4318
|
-
tests: []
|
|
4319
|
-
});
|
|
4320
|
-
};
|
|
4321
|
-
const handleTestFileReady = async (payload)=>{
|
|
4322
|
-
phaseTrackers?.get(payload.testPath)?.transition('tests');
|
|
4323
|
-
await sinkForTestPath(payload.testPath).onTestFileReady(payload);
|
|
4324
|
-
};
|
|
4325
|
-
const handleTestSuiteStart = async (payload)=>{
|
|
4326
|
-
phaseTrackers?.get(payload.testPath)?.recordSuiteStart(payload);
|
|
4327
|
-
await sinkForTestPath(payload.testPath).onTestSuiteStart(payload);
|
|
4328
4226
|
};
|
|
4329
|
-
const
|
|
4330
|
-
|
|
4331
|
-
|
|
4332
|
-
|
|
4333
|
-
|
|
4334
|
-
|
|
4335
|
-
|
|
4336
|
-
taskType: 'suite',
|
|
4337
|
-
testPath: payload.testPath
|
|
4227
|
+
const makeSkippedFileResult = (file)=>({
|
|
4228
|
+
testId: getFileTaskId(file.testPath),
|
|
4229
|
+
status: 'skip',
|
|
4230
|
+
name: '',
|
|
4231
|
+
testPath: file.testPath,
|
|
4232
|
+
project: file.projectName,
|
|
4233
|
+
results: []
|
|
4338
4234
|
});
|
|
4235
|
+
const runFilesWithPool = async (files)=>{
|
|
4236
|
+
if (0 === files.length) return;
|
|
4237
|
+
const previous = runLifecycle.activeSession;
|
|
4238
|
+
if (previous) await cancelRun(previous);
|
|
4239
|
+
const run = runLifecycle.createSession((token)=>({
|
|
4240
|
+
...createRunSession(token),
|
|
4241
|
+
contexts: new Set()
|
|
4242
|
+
}));
|
|
4243
|
+
const queue = [
|
|
4244
|
+
...files
|
|
4245
|
+
];
|
|
4246
|
+
const concurrency = getHeadlessConcurrency(context, queue.length);
|
|
4247
|
+
const bail = context.normalizedConfig.bail;
|
|
4248
|
+
const worker = async ()=>{
|
|
4249
|
+
while(queue.length > 0 && !run.cancelled && runLifecycle.isTokenActive(run.token)){
|
|
4250
|
+
if (bail && context.stateManager.getCountOfFailedTests() >= bail) {
|
|
4251
|
+
let skipped = queue.shift();
|
|
4252
|
+
while(skipped){
|
|
4253
|
+
await handleTestFileComplete(makeSkippedFileResult(skipped));
|
|
4254
|
+
skipped = queue.shift();
|
|
4255
|
+
}
|
|
4256
|
+
return;
|
|
4257
|
+
}
|
|
4258
|
+
const next = queue.shift();
|
|
4259
|
+
if (!next) return;
|
|
4260
|
+
await runSingleFile(run, next);
|
|
4261
|
+
}
|
|
4262
|
+
};
|
|
4263
|
+
run.done = Promise.all(Array.from({
|
|
4264
|
+
length: Math.min(queue.length, Math.max(concurrency, 1))
|
|
4265
|
+
}, ()=>worker())).then(()=>{});
|
|
4266
|
+
await run.done;
|
|
4267
|
+
runLifecycle.clearIfActive(run);
|
|
4339
4268
|
};
|
|
4340
|
-
const
|
|
4341
|
-
|
|
4342
|
-
|
|
4343
|
-
|
|
4344
|
-
|
|
4345
|
-
|
|
4346
|
-
|
|
4347
|
-
|
|
4348
|
-
|
|
4349
|
-
|
|
4350
|
-
|
|
4351
|
-
|
|
4352
|
-
|
|
4353
|
-
|
|
4269
|
+
const testStart = Date.now();
|
|
4270
|
+
await runFilesWithPool(allTestFiles);
|
|
4271
|
+
const testTime = Date.now() - testStart;
|
|
4272
|
+
let watchSession;
|
|
4273
|
+
if (isWatchMode) {
|
|
4274
|
+
const runScope = async (testPaths)=>{
|
|
4275
|
+
const pathSet = new Set(testPaths.map((testPath)=>normalize(testPath)));
|
|
4276
|
+
await runFilesWithPool(watchState.lastTestFiles.filter((file)=>pathSet.has(file.testPath)));
|
|
4277
|
+
};
|
|
4278
|
+
watchSignals.setInterrupt(async ()=>{
|
|
4279
|
+
const active = runLifecycle.activeSession;
|
|
4280
|
+
if (!active || active.cancelled) return;
|
|
4281
|
+
runLifecycle.invalidateActiveToken();
|
|
4282
|
+
await runLifecycle.cancel(active, {
|
|
4283
|
+
waitForDone: false
|
|
4284
|
+
});
|
|
4354
4285
|
});
|
|
4355
|
-
|
|
4356
|
-
|
|
4357
|
-
|
|
4358
|
-
|
|
4359
|
-
|
|
4360
|
-
|
|
4361
|
-
|
|
4362
|
-
|
|
4363
|
-
|
|
4364
|
-
|
|
4365
|
-
|
|
4366
|
-
if (
|
|
4367
|
-
|
|
4286
|
+
watchSignals.setDispatchRerun(async ()=>{
|
|
4287
|
+
const newProjectEntries = await collectProjectEntries();
|
|
4288
|
+
const rerunPlan = planWatchRerun({
|
|
4289
|
+
projectEntries: newProjectEntries,
|
|
4290
|
+
previousTestFiles: watchState.lastTestFiles,
|
|
4291
|
+
affectedTestFiles: drainPendingAffectedTestFiles(watchState)
|
|
4292
|
+
});
|
|
4293
|
+
if (rerunPlan.filesChanged) {
|
|
4294
|
+
const deletedTestPaths = collectDeletedTestPaths(watchState.lastTestFiles, rerunPlan.currentTestFiles);
|
|
4295
|
+
if (deletedTestPaths.length > 0) context.updateReporterResultState([], [], deletedTestPaths);
|
|
4296
|
+
watchState.lastTestFiles = rerunPlan.currentTestFiles;
|
|
4297
|
+
if (0 === rerunPlan.currentTestFiles.length) {
|
|
4298
|
+
logger.log(color.cyan('No browser test files remain after update.\n'));
|
|
4299
|
+
await watchSignals.signalInvalidation([]);
|
|
4300
|
+
return;
|
|
4301
|
+
}
|
|
4302
|
+
logger.log(color.cyan(`Test file set changed, re-running ${rerunPlan.currentTestFiles.length} file(s)...\n`));
|
|
4303
|
+
await watchSignals.signalInvalidation([
|
|
4304
|
+
...new Set(rerunPlan.currentTestFiles.map((file)=>file.testPath))
|
|
4305
|
+
]);
|
|
4306
|
+
return;
|
|
4368
4307
|
}
|
|
4369
|
-
|
|
4370
|
-
|
|
4371
|
-
|
|
4372
|
-
|
|
4373
|
-
|
|
4374
|
-
|
|
4375
|
-
|
|
4308
|
+
if (0 === rerunPlan.affectedTestFiles.length) {
|
|
4309
|
+
logger.log(color.cyan('No affected browser test files detected, skipping re-run.\n'));
|
|
4310
|
+
logWatchReady();
|
|
4311
|
+
return;
|
|
4312
|
+
}
|
|
4313
|
+
logger.log(color.cyan(`Re-running ${rerunPlan.affectedTestFiles.length} affected test file(s)...\n`));
|
|
4314
|
+
await watchSignals.signalInvalidation([
|
|
4315
|
+
...new Set(rerunPlan.affectedTestFiles.map((file)=>file.testPath))
|
|
4316
|
+
]);
|
|
4376
4317
|
});
|
|
4377
|
-
|
|
4378
|
-
|
|
4318
|
+
watchSession = createWatchSession(runScope);
|
|
4319
|
+
}
|
|
4320
|
+
const closeHeadlessRuntime = isWatchMode ? void 0 : async ()=>{
|
|
4321
|
+
sessionRegistry.clear();
|
|
4322
|
+
await destroyRuntime();
|
|
4323
|
+
};
|
|
4324
|
+
return {
|
|
4325
|
+
testTime,
|
|
4326
|
+
watchSession,
|
|
4327
|
+
close: closeHeadlessRuntime
|
|
4328
|
+
};
|
|
4329
|
+
};
|
|
4330
|
+
const isRecord = (value)=>'object' == typeof value && null !== value;
|
|
4331
|
+
const readString = (value, key, label)=>{
|
|
4332
|
+
const result = value[key];
|
|
4333
|
+
if ('string' != typeof result) throw new Error(`Invalid browser RPC request: ${label} must be a string`);
|
|
4334
|
+
return result;
|
|
4335
|
+
};
|
|
4336
|
+
const readUnknownArray = (value, key, label)=>{
|
|
4337
|
+
const result = value[key];
|
|
4338
|
+
if (!Array.isArray(result)) throw new Error(`Invalid browser RPC request: ${label} must be an array`);
|
|
4339
|
+
return result;
|
|
4340
|
+
};
|
|
4341
|
+
const parseBrowserLocatorIR = (value, label)=>{
|
|
4342
|
+
if (!isRecord(value)) throw new Error(`Invalid browser RPC request: ${label} must be an object`);
|
|
4343
|
+
const steps = value.steps;
|
|
4344
|
+
if (!Array.isArray(steps)) throw new Error(`Invalid browser RPC request: ${label}.steps must be an array`);
|
|
4345
|
+
return value;
|
|
4346
|
+
};
|
|
4347
|
+
const validateBrowserRpcRequest = (payload)=>{
|
|
4348
|
+
if (!isRecord(payload)) throw new Error('Invalid browser RPC request: payload must be an object');
|
|
4349
|
+
const kind = readString(payload, 'kind', 'kind');
|
|
4350
|
+
if ('locator' !== kind && 'expect' !== kind && 'config' !== kind) throw new Error(`Invalid browser RPC request: unsupported kind ${JSON.stringify(kind)}`);
|
|
4351
|
+
const request = {
|
|
4352
|
+
id: readString(payload, 'id', 'id'),
|
|
4353
|
+
testPath: readString(payload, 'testPath', 'testPath'),
|
|
4354
|
+
runId: readString(payload, 'runId', 'runId'),
|
|
4355
|
+
kind,
|
|
4356
|
+
locator: parseBrowserLocatorIR(payload.locator, 'locator'),
|
|
4357
|
+
method: readString(payload, 'method', 'method'),
|
|
4358
|
+
args: readUnknownArray(payload, 'args', 'args')
|
|
4359
|
+
};
|
|
4360
|
+
const isNot = payload.isNot;
|
|
4361
|
+
if (void 0 !== isNot) {
|
|
4362
|
+
if ('boolean' != typeof isNot) throw new Error('Invalid browser RPC request: isNot must be a boolean');
|
|
4363
|
+
request.isNot = isNot;
|
|
4364
|
+
}
|
|
4365
|
+
const timeout = payload.timeout;
|
|
4366
|
+
if (void 0 !== timeout) {
|
|
4367
|
+
if ('number' != typeof timeout) throw new Error('Invalid browser RPC request: timeout must be a number');
|
|
4368
|
+
request.timeout = timeout;
|
|
4369
|
+
}
|
|
4370
|
+
return request;
|
|
4371
|
+
};
|
|
4372
|
+
const normalizeJavaScriptUrl = (value, options)=>{
|
|
4373
|
+
try {
|
|
4374
|
+
const url = options?.origin ? new URL(value, options.origin) : new URL(value);
|
|
4375
|
+
if ('http:' !== url.protocol && 'https:' !== url.protocol) return null;
|
|
4376
|
+
url.search = '';
|
|
4377
|
+
url.hash = '';
|
|
4378
|
+
return url.toString();
|
|
4379
|
+
} catch {
|
|
4380
|
+
return null;
|
|
4381
|
+
}
|
|
4382
|
+
};
|
|
4383
|
+
const resolveInlineSourceMap = (code)=>{
|
|
4384
|
+
const converter = convert_source_map.fromSource(code);
|
|
4385
|
+
if (!converter) return null;
|
|
4386
|
+
return converter.toObject();
|
|
4387
|
+
};
|
|
4388
|
+
const fetchSourceMap = async (jsUrl, fetcher)=>{
|
|
4389
|
+
const jsResponse = await fetcher(jsUrl);
|
|
4390
|
+
if (!jsResponse.ok) return null;
|
|
4391
|
+
const code = await jsResponse.text();
|
|
4392
|
+
const inlineMap = resolveInlineSourceMap(code);
|
|
4393
|
+
if (inlineMap) return inlineMap;
|
|
4394
|
+
const mapResponse = await fetcher(`${jsUrl}.map`);
|
|
4395
|
+
if (!mapResponse.ok) return null;
|
|
4396
|
+
return await mapResponse.json();
|
|
4397
|
+
};
|
|
4398
|
+
const loadSourceMapWithCache = async ({ jsUrl, cache, force = false, origin, fetcher = fetch })=>{
|
|
4399
|
+
const normalizedUrl = normalizeJavaScriptUrl(jsUrl, {
|
|
4400
|
+
origin
|
|
4401
|
+
});
|
|
4402
|
+
if (!normalizedUrl) return null;
|
|
4403
|
+
if (!force && cache.has(normalizedUrl)) return cache.get(normalizedUrl) ?? null;
|
|
4404
|
+
try {
|
|
4405
|
+
const sourceMap = await fetchSourceMap(normalizedUrl, fetcher);
|
|
4406
|
+
cache.set(normalizedUrl, sourceMap);
|
|
4407
|
+
return sourceMap;
|
|
4408
|
+
} catch {
|
|
4409
|
+
cache.set(normalizedUrl, null);
|
|
4410
|
+
return null;
|
|
4411
|
+
}
|
|
4412
|
+
};
|
|
4413
|
+
const watchContext = {
|
|
4414
|
+
runtime: null,
|
|
4415
|
+
cleanupRegistered: false,
|
|
4416
|
+
cleanupPromise: null
|
|
4417
|
+
};
|
|
4418
|
+
const runWatchRuntimeTeardown = (state, destroy)=>{
|
|
4419
|
+
if (state.cleanupPromise) return state.cleanupPromise;
|
|
4420
|
+
state.cleanupPromise = (async ()=>{
|
|
4421
|
+
if (!state.runtime) return;
|
|
4422
|
+
await destroy(state.runtime);
|
|
4423
|
+
state.runtime = null;
|
|
4424
|
+
})();
|
|
4425
|
+
return state.cleanupPromise.finally(()=>{
|
|
4426
|
+
state.cleanupPromise = null;
|
|
4427
|
+
});
|
|
4428
|
+
};
|
|
4429
|
+
const cleanupWatchRuntime = ()=>runWatchRuntimeTeardown(watchContext, destroyBrowserRuntime);
|
|
4430
|
+
const registerWatchCleanup = (embedded)=>{
|
|
4431
|
+
if (watchContext.cleanupRegistered) return;
|
|
4432
|
+
watchContext.cleanupRegistered = true;
|
|
4433
|
+
if (embedded) return;
|
|
4434
|
+
for (const signal of FATAL_SIGNALS)process.once(signal, ()=>{
|
|
4435
|
+
cleanupWatchRuntime();
|
|
4436
|
+
});
|
|
4437
|
+
process.once('exit', ()=>{
|
|
4438
|
+
cleanupWatchRuntime();
|
|
4439
|
+
});
|
|
4440
|
+
};
|
|
4441
|
+
const createWatchSignals = (onInvalidate)=>{
|
|
4442
|
+
let dispatchRerun;
|
|
4443
|
+
let interruptInFlightRun;
|
|
4444
|
+
let signalledCycle;
|
|
4445
|
+
return {
|
|
4446
|
+
setDispatchRerun (fn) {
|
|
4447
|
+
dispatchRerun = fn;
|
|
4448
|
+
},
|
|
4449
|
+
async runDispatchRerun () {
|
|
4450
|
+
await dispatchRerun?.();
|
|
4451
|
+
},
|
|
4452
|
+
setInterrupt (fn) {
|
|
4453
|
+
interruptInFlightRun = fn;
|
|
4454
|
+
},
|
|
4455
|
+
async signalInvalidation (fileFilters, claimScope) {
|
|
4456
|
+
await interruptInFlightRun?.();
|
|
4457
|
+
claimScope?.();
|
|
4458
|
+
signalledCycle = Promise.resolve(onInvalidate?.({
|
|
4459
|
+
isFirstBuild: false,
|
|
4460
|
+
fileFilters
|
|
4461
|
+
})).catch((error)=>{
|
|
4462
|
+
logger.error(color.red('Browser Mode watch cycle failed:'), error);
|
|
4463
|
+
});
|
|
4464
|
+
},
|
|
4465
|
+
async awaitSignalledCycle () {
|
|
4466
|
+
await signalledCycle;
|
|
4467
|
+
}
|
|
4379
4468
|
};
|
|
4380
|
-
|
|
4381
|
-
|
|
4382
|
-
|
|
4383
|
-
|
|
4384
|
-
|
|
4385
|
-
|
|
4386
|
-
|
|
4387
|
-
|
|
4388
|
-
|
|
4389
|
-
|
|
4390
|
-
|
|
4469
|
+
};
|
|
4470
|
+
let nextBrowserFilePid = 1000000000;
|
|
4471
|
+
const resolveEmptyLaunchExitCode = (current, { allowEmptyRun, isWatchMode, passWithNoTests })=>{
|
|
4472
|
+
if (allowEmptyRun || !isWatchMode || passWithNoTests) return current;
|
|
4473
|
+
return void 0 === current || 0 === current ? 1 : current;
|
|
4474
|
+
};
|
|
4475
|
+
const getMaxTestTimeoutForRpc = (projects)=>Math.max(...projects.map((p)=>p.normalizedConfig.testTimeout ?? DEFAULT_TEST_TIMEOUT));
|
|
4476
|
+
const resolveProviderForTestPath = ({ testPath, browserProjects })=>{
|
|
4477
|
+
const normalizedTestPath = normalize(testPath);
|
|
4478
|
+
const sortedProjects = [
|
|
4479
|
+
...browserProjects
|
|
4480
|
+
].sort((a, b)=>b.rootPath.length - a.rootPath.length);
|
|
4481
|
+
for (const project of sortedProjects)if (normalizedTestPath.startsWith(project.rootPath)) return project.provider;
|
|
4482
|
+
throw new Error(`Cannot resolve browser provider for test path: ${JSON.stringify(testPath)}. Known project roots: ${JSON.stringify(sortedProjects.map((p)=>p.rootPath))}`);
|
|
4483
|
+
};
|
|
4484
|
+
const runBrowserController = async (context, options)=>{
|
|
4485
|
+
const { allowEmptyRun = false, filesOnly = false, onTraceEvents, env, onInvalidate } = options ?? {};
|
|
4486
|
+
const buildStart = Date.now();
|
|
4487
|
+
const isWatchMode = 'watch' === context.command;
|
|
4488
|
+
const phaseTrackers = onTraceEvents ? new Map() : void 0;
|
|
4489
|
+
const trackerKey = (project, testPath)=>`${project}\u0000${testPath}`;
|
|
4490
|
+
const browserProjects = options?.projects ?? getBrowserProjects(context);
|
|
4491
|
+
const useHeadlessDirect = browserProjects.every((project)=>project.normalizedConfig.browser.headless);
|
|
4492
|
+
const browserSourceMapCache = new Map();
|
|
4493
|
+
const isHttpLikeFile = (file)=>/^https?:\/\//.test(file);
|
|
4494
|
+
const resolveBrowserSourcemap = async (sourcePath)=>{
|
|
4495
|
+
if (!isHttpLikeFile(sourcePath)) return {
|
|
4496
|
+
handled: false,
|
|
4497
|
+
sourcemap: null
|
|
4498
|
+
};
|
|
4499
|
+
const normalizedUrl = normalizeJavaScriptUrl(sourcePath);
|
|
4500
|
+
if (!normalizedUrl) return {
|
|
4501
|
+
handled: true,
|
|
4502
|
+
sourcemap: null
|
|
4503
|
+
};
|
|
4504
|
+
if (browserSourceMapCache.has(normalizedUrl)) return {
|
|
4505
|
+
handled: true,
|
|
4506
|
+
sourcemap: browserSourceMapCache.get(normalizedUrl) ?? null
|
|
4507
|
+
};
|
|
4508
|
+
return {
|
|
4509
|
+
handled: true,
|
|
4510
|
+
sourcemap: await loadSourceMapWithCache({
|
|
4511
|
+
jsUrl: normalizedUrl,
|
|
4512
|
+
cache: browserSourceMapCache
|
|
4513
|
+
})
|
|
4391
4514
|
};
|
|
4392
|
-
silentConsoleController.onConsoleLog(log);
|
|
4393
4515
|
};
|
|
4394
|
-
const
|
|
4395
|
-
const
|
|
4396
|
-
|
|
4397
|
-
fatalError = error;
|
|
4398
|
-
if (isWatchMode) ensureProcessExitCode(1);
|
|
4516
|
+
const getBrowserSourcemap = async (sourcePath)=>{
|
|
4517
|
+
const result = await resolveBrowserSourcemap(sourcePath);
|
|
4518
|
+
return result.handled ? result.sourcemap : null;
|
|
4399
4519
|
};
|
|
4400
|
-
const
|
|
4401
|
-
|
|
4402
|
-
|
|
4403
|
-
|
|
4404
|
-
|
|
4405
|
-
|
|
4406
|
-
|
|
4407
|
-
|
|
4408
|
-
|
|
4409
|
-
|
|
4410
|
-
|
|
4411
|
-
|
|
4412
|
-
|
|
4413
|
-
|
|
4414
|
-
|
|
4520
|
+
const buildErrorResult = (error, close)=>{
|
|
4521
|
+
const elapsed = Math.max(0, Date.now() - buildStart);
|
|
4522
|
+
return {
|
|
4523
|
+
results: [],
|
|
4524
|
+
testResults: [],
|
|
4525
|
+
duration: {
|
|
4526
|
+
totalTime: elapsed,
|
|
4527
|
+
buildTime: elapsed,
|
|
4528
|
+
testTime: 0
|
|
4529
|
+
},
|
|
4530
|
+
hasFailure: true,
|
|
4531
|
+
unhandledErrors: [
|
|
4532
|
+
error
|
|
4533
|
+
],
|
|
4534
|
+
getSourcemap: getBrowserSourcemap,
|
|
4535
|
+
resolveSourcemap: resolveBrowserSourcemap,
|
|
4536
|
+
close
|
|
4537
|
+
};
|
|
4538
|
+
};
|
|
4539
|
+
const failWithError = async (error, cleanup)=>{
|
|
4540
|
+
const normalizedError = toError(error);
|
|
4541
|
+
if (cleanup && !isWatchMode) return buildErrorResult(normalizedError, cleanup);
|
|
4542
|
+
try {
|
|
4543
|
+
return buildErrorResult(normalizedError);
|
|
4544
|
+
} finally{
|
|
4545
|
+
await cleanup?.();
|
|
4415
4546
|
}
|
|
4416
4547
|
};
|
|
4417
|
-
const
|
|
4418
|
-
|
|
4419
|
-
|
|
4420
|
-
|
|
4421
|
-
|
|
4422
|
-
|
|
4423
|
-
|
|
4424
|
-
|
|
4425
|
-
|
|
4426
|
-
|
|
4427
|
-
|
|
4428
|
-
|
|
4548
|
+
const coverageConfig = browserProjects.find((project)=>project.normalizedConfig.coverage?.enabled)?.normalizedConfig.coverage;
|
|
4549
|
+
const coverageProvider = coverageConfig?.enabled ? await createCoverageProvider(coverageConfig, context.rootPath) : null;
|
|
4550
|
+
const containerDevServerEnv = process.env.RSTEST_CONTAINER_DEV_SERVER;
|
|
4551
|
+
let containerDevServer;
|
|
4552
|
+
let containerDistPath;
|
|
4553
|
+
if (!useHeadlessDirect) {
|
|
4554
|
+
if (containerDevServerEnv) try {
|
|
4555
|
+
containerDevServer = new URL(containerDevServerEnv).toString();
|
|
4556
|
+
logger.debug(`[Browser UI] Using dev server for container: ${containerDevServer}`);
|
|
4557
|
+
} catch (error) {
|
|
4558
|
+
const originalError = toError(error);
|
|
4559
|
+
originalError.message = `Invalid RSTEST_CONTAINER_DEV_SERVER value: ${originalError.message}`;
|
|
4560
|
+
return failWithError(originalError);
|
|
4561
|
+
}
|
|
4562
|
+
if (!containerDevServer) try {
|
|
4563
|
+
containerDistPath = resolveContainerDist();
|
|
4564
|
+
} catch (error) {
|
|
4565
|
+
return failWithError(error);
|
|
4566
|
+
}
|
|
4567
|
+
}
|
|
4568
|
+
let projectEntries = await resolveProjectEntries(context, options?.shardedEntries, browserProjects);
|
|
4569
|
+
let totalTests = projectEntries.reduce((total, item)=>total + item.testFiles.length, 0);
|
|
4570
|
+
const shouldInitializeEmptyBrowserHooks = 0 === totalTests && hasUserRstestConfigPlugins(browserProjects);
|
|
4571
|
+
const createEmptyRunResult = ()=>{
|
|
4572
|
+
const elapsed = Math.max(0, Date.now() - buildStart);
|
|
4573
|
+
return {
|
|
4574
|
+
results: [],
|
|
4575
|
+
testResults: [],
|
|
4576
|
+
duration: {
|
|
4577
|
+
totalTime: elapsed,
|
|
4578
|
+
buildTime: elapsed,
|
|
4579
|
+
testTime: 0
|
|
4429
4580
|
},
|
|
4430
|
-
|
|
4431
|
-
|
|
4432
|
-
|
|
4433
|
-
logger.debug(`[Dispatch] Skip registering dispatch namespace "${namespace}" because it is already reserved`);
|
|
4434
|
-
}
|
|
4435
|
-
});
|
|
4436
|
-
if (useHeadlessDirect) {
|
|
4437
|
-
const viewportByProject = mapViewportByProject(projectRuntimeConfigs);
|
|
4438
|
-
const runLifecycle = new RunSessionLifecycle();
|
|
4439
|
-
const sessionRegistry = new RunnerSessionRegistry();
|
|
4440
|
-
getHeadlessRunnerPageBySessionId = (sessionId)=>sessionRegistry.getById(sessionId)?.page;
|
|
4441
|
-
let dispatchRequestCounter = 0;
|
|
4442
|
-
const nextDispatchRequestId = (namespace)=>`${namespace}-${++dispatchRequestCounter}`;
|
|
4443
|
-
const closeContextSafely = async (browserContext)=>{
|
|
4444
|
-
try {
|
|
4445
|
-
await browserContext.close();
|
|
4446
|
-
} catch {}
|
|
4447
|
-
};
|
|
4448
|
-
const cancelRun = async (run, waitForDone = true)=>{
|
|
4449
|
-
await runLifecycle.cancel(run, {
|
|
4450
|
-
waitForDone,
|
|
4451
|
-
onCancel: async (session)=>{
|
|
4452
|
-
await Promise.all(Array.from(session.contexts).map((browserContext)=>closeContextSafely(browserContext)));
|
|
4453
|
-
}
|
|
4454
|
-
});
|
|
4581
|
+
hasFailure: false,
|
|
4582
|
+
getSourcemap: getBrowserSourcemap,
|
|
4583
|
+
resolveSourcemap: resolveBrowserSourcemap
|
|
4455
4584
|
};
|
|
4456
|
-
|
|
4457
|
-
|
|
4458
|
-
|
|
4459
|
-
|
|
4460
|
-
|
|
4585
|
+
};
|
|
4586
|
+
const writeEmptyLaunchExitCode = ()=>{
|
|
4587
|
+
process.exitCode = resolveEmptyLaunchExitCode(process.exitCode, {
|
|
4588
|
+
allowEmptyRun,
|
|
4589
|
+
isWatchMode,
|
|
4590
|
+
passWithNoTests: context.normalizedConfig.passWithNoTests
|
|
4461
4591
|
});
|
|
4462
|
-
|
|
4463
|
-
|
|
4464
|
-
|
|
4465
|
-
|
|
4466
|
-
|
|
4467
|
-
|
|
4468
|
-
|
|
4469
|
-
|
|
4470
|
-
|
|
4471
|
-
|
|
4472
|
-
|
|
4473
|
-
|
|
4474
|
-
|
|
4475
|
-
|
|
4476
|
-
|
|
4477
|
-
|
|
4478
|
-
|
|
4479
|
-
|
|
4480
|
-
|
|
4481
|
-
|
|
4482
|
-
|
|
4483
|
-
|
|
4484
|
-
|
|
4485
|
-
|
|
4486
|
-
|
|
4487
|
-
|
|
4488
|
-
|
|
4489
|
-
let resolveDone = null;
|
|
4490
|
-
const markDone = ()=>{
|
|
4491
|
-
if (!settled) {
|
|
4492
|
-
settled = true;
|
|
4493
|
-
resolveDone?.();
|
|
4494
|
-
}
|
|
4495
|
-
};
|
|
4496
|
-
const donePromise = new Promise((resolve)=>{
|
|
4497
|
-
resolveDone = resolve;
|
|
4592
|
+
};
|
|
4593
|
+
if (0 === totalTests && !shouldInitializeEmptyBrowserHooks) {
|
|
4594
|
+
writeEmptyLaunchExitCode();
|
|
4595
|
+
return allowEmptyRun ? createEmptyRunResult() : void 0;
|
|
4596
|
+
}
|
|
4597
|
+
const enableCliShortcuts = isWatchMode && isTTY('stdin');
|
|
4598
|
+
const browserTempOutputRoot = context.normalizedConfig.output.distPath.root;
|
|
4599
|
+
const tempDir = isWatchMode && watchContext.runtime ? watchContext.runtime.tempDir : isWatchMode ? join(context.rootPath, browserTempOutputRoot, 'browser', 'watch') : join(context.rootPath, browserTempOutputRoot, 'browser', Date.now().toString());
|
|
4600
|
+
let runtime = isWatchMode ? watchContext.runtime : null;
|
|
4601
|
+
const watchSignals = createWatchSignals(onInvalidate);
|
|
4602
|
+
if (!runtime) {
|
|
4603
|
+
try {
|
|
4604
|
+
runtime = await createBrowserRuntime({
|
|
4605
|
+
context,
|
|
4606
|
+
projectEntries,
|
|
4607
|
+
browserProjects,
|
|
4608
|
+
shardedEntries: options?.shardedEntries,
|
|
4609
|
+
freezeShardedEntries: options?.freezeShardedEntries,
|
|
4610
|
+
tempDir,
|
|
4611
|
+
isWatchMode,
|
|
4612
|
+
onTriggerRerun: isWatchMode ? async ()=>{
|
|
4613
|
+
await watchSignals.runDispatchRerun();
|
|
4614
|
+
} : void 0,
|
|
4615
|
+
containerDistPath,
|
|
4616
|
+
containerDevServer,
|
|
4617
|
+
skipProviderLaunch: filesOnly,
|
|
4618
|
+
appliedModifyRstestConfigEnvironments: options?.appliedModifyRstestConfigEnvironments
|
|
4498
4619
|
});
|
|
4499
|
-
|
|
4500
|
-
|
|
4501
|
-
|
|
4502
|
-
|
|
4503
|
-
|
|
4504
|
-
|
|
4505
|
-
try {
|
|
4506
|
-
page = await browserContext.newPage();
|
|
4507
|
-
page.on('crash', ()=>onPageDead(`Browser page crashed while running ${file.testPath}.`));
|
|
4508
|
-
page.on('close', ()=>onPageDead(`Browser page closed unexpectedly while running ${file.testPath}.`));
|
|
4509
|
-
const session = sessionRegistry.register({
|
|
4510
|
-
testFile: file.testPath,
|
|
4511
|
-
projectName: file.projectName,
|
|
4512
|
-
runToken: run.token,
|
|
4513
|
-
mode: 'headless-page',
|
|
4514
|
-
context: browserContext,
|
|
4515
|
-
page
|
|
4516
|
-
});
|
|
4517
|
-
sessionId = session.id;
|
|
4518
|
-
await attachHeadlessRunnerTransport(page, {
|
|
4519
|
-
onDispatchMessage: async (message)=>{
|
|
4520
|
-
try {
|
|
4521
|
-
await dispatchRunnerMessage(run, file, session.id, message);
|
|
4522
|
-
if ('file-complete' === message.type || 'complete' === message.type) markDone();
|
|
4523
|
-
else if ('fatal' === message.type) {
|
|
4524
|
-
markDone();
|
|
4525
|
-
await cancelRun(run, false);
|
|
4526
|
-
}
|
|
4527
|
-
} catch (error) {
|
|
4528
|
-
const formatted = toError(error);
|
|
4529
|
-
await handleFatal({
|
|
4530
|
-
message: formatted.message,
|
|
4531
|
-
stack: formatted.stack
|
|
4532
|
-
});
|
|
4533
|
-
markDone();
|
|
4534
|
-
await cancelRun(run, false);
|
|
4535
|
-
}
|
|
4536
|
-
},
|
|
4537
|
-
onDispatchRpc: async (request)=>dispatchRouter.dispatch({
|
|
4538
|
-
...request,
|
|
4539
|
-
runToken: run.token,
|
|
4540
|
-
target: {
|
|
4541
|
-
sessionId: session.id,
|
|
4542
|
-
testFile: file.testPath,
|
|
4543
|
-
projectName: file.projectName,
|
|
4544
|
-
...request.target
|
|
4545
|
-
}
|
|
4546
|
-
})
|
|
4547
|
-
});
|
|
4548
|
-
const inlineOptions = {
|
|
4549
|
-
...hostOptions,
|
|
4550
|
-
snapshot: {
|
|
4551
|
-
updateSnapshot: context.snapshotManager.options.updateSnapshot
|
|
4552
|
-
},
|
|
4553
|
-
testFile: file.testPath,
|
|
4554
|
-
runId: `${run.token}:${session.id}`
|
|
4555
|
-
};
|
|
4556
|
-
const serializedOptions = serializeForInlineScript(inlineOptions);
|
|
4557
|
-
await page.addInitScript(`window.__RSTEST_BROWSER_OPTIONS__ = ${serializedOptions};`);
|
|
4558
|
-
const projectServer = runtime.projectServers.get(file.projectName);
|
|
4559
|
-
if (!projectServer) throw new Error(`No browser dev server for project "${file.projectName}" (test file: ${file.testPath}).`);
|
|
4560
|
-
await page.goto(`http://localhost:${projectServer.port}/runner.html`, {
|
|
4561
|
-
waitUntil: 'load'
|
|
4562
|
-
});
|
|
4563
|
-
const state = await Promise.race([
|
|
4564
|
-
donePromise.then(()=>({
|
|
4565
|
-
type: 'done'
|
|
4566
|
-
})),
|
|
4567
|
-
crashDeferred.promise.then((reason)=>({
|
|
4568
|
-
type: 'crash',
|
|
4569
|
-
reason
|
|
4570
|
-
})),
|
|
4571
|
-
run.cancelSignal.then(()=>({
|
|
4572
|
-
type: 'cancelled'
|
|
4573
|
-
}))
|
|
4574
|
-
]);
|
|
4575
|
-
if ('cancelled' === state.type) return;
|
|
4576
|
-
if ('crash' === state.type && runLifecycle.isTokenActive(run.token) && !run.cancelled) {
|
|
4577
|
-
await handleFatal({
|
|
4578
|
-
message: state.reason
|
|
4579
|
-
});
|
|
4580
|
-
await cancelRun(run, false);
|
|
4581
|
-
}
|
|
4582
|
-
} catch (error) {
|
|
4583
|
-
if (runLifecycle.isTokenActive(run.token) && !run.cancelled) {
|
|
4584
|
-
const formatted = toError(error);
|
|
4585
|
-
await handleFatal({
|
|
4586
|
-
message: formatted.message,
|
|
4587
|
-
stack: formatted.stack
|
|
4588
|
-
});
|
|
4589
|
-
await cancelRun(run, false);
|
|
4590
|
-
}
|
|
4591
|
-
} finally{
|
|
4592
|
-
if (page) try {
|
|
4593
|
-
await page.close();
|
|
4594
|
-
} catch {}
|
|
4595
|
-
if (sessionId) sessionRegistry.deleteById(sessionId);
|
|
4596
|
-
run.contexts.delete(browserContext);
|
|
4597
|
-
await closeContextSafely(browserContext);
|
|
4598
|
-
}
|
|
4599
|
-
};
|
|
4600
|
-
const makeSkippedFileResult = (file)=>({
|
|
4601
|
-
testId: getFileTaskId(file.testPath),
|
|
4602
|
-
status: 'skip',
|
|
4603
|
-
name: '',
|
|
4604
|
-
testPath: file.testPath,
|
|
4605
|
-
project: file.projectName,
|
|
4606
|
-
results: []
|
|
4620
|
+
} catch (error) {
|
|
4621
|
+
return failWithError(error, async ()=>{
|
|
4622
|
+
await promises.rm(tempDir, {
|
|
4623
|
+
recursive: true,
|
|
4624
|
+
force: true
|
|
4625
|
+
}).catch(()=>{});
|
|
4607
4626
|
});
|
|
4608
|
-
|
|
4609
|
-
|
|
4610
|
-
|
|
4611
|
-
|
|
4612
|
-
|
|
4613
|
-
|
|
4614
|
-
|
|
4615
|
-
|
|
4616
|
-
|
|
4617
|
-
|
|
4618
|
-
|
|
4619
|
-
|
|
4620
|
-
|
|
4621
|
-
const
|
|
4622
|
-
|
|
4623
|
-
|
|
4624
|
-
|
|
4625
|
-
|
|
4626
|
-
|
|
4627
|
-
|
|
4628
|
-
|
|
4629
|
-
|
|
4630
|
-
|
|
4631
|
-
|
|
4632
|
-
|
|
4633
|
-
|
|
4634
|
-
|
|
4635
|
-
|
|
4636
|
-
|
|
4637
|
-
|
|
4638
|
-
|
|
4639
|
-
|
|
4640
|
-
runLifecycle.clearIfActive(run);
|
|
4641
|
-
};
|
|
4642
|
-
const latestRerunScheduler = createHeadlessLatestRerunScheduler({
|
|
4643
|
-
getActiveRun: ()=>runLifecycle.activeSession,
|
|
4644
|
-
isRunCancelled: (run)=>run.cancelled,
|
|
4645
|
-
invalidateActiveRun: ()=>{
|
|
4646
|
-
runLifecycle.invalidateActiveToken();
|
|
4647
|
-
},
|
|
4648
|
-
interruptActiveRun: async (run)=>{
|
|
4649
|
-
await cancelRun(run, false);
|
|
4627
|
+
}
|
|
4628
|
+
if (isWatchMode && !filesOnly) {
|
|
4629
|
+
watchContext.runtime = runtime;
|
|
4630
|
+
registerWatchCleanup(context.embedded);
|
|
4631
|
+
}
|
|
4632
|
+
}
|
|
4633
|
+
const watchState = runtime.watchState;
|
|
4634
|
+
if (isWatchMode) watchState.lastTestFiles = collectWatchTestFiles(projectEntries);
|
|
4635
|
+
const seedPendingRerun = (testPaths)=>{
|
|
4636
|
+
const wanted = testPaths ? new Set(testPaths.map((testPath)=>normalize(testPath))) : null;
|
|
4637
|
+
let seeded = 0;
|
|
4638
|
+
for (const file of watchState.lastTestFiles){
|
|
4639
|
+
if (wanted && !wanted.has(file.testPath)) continue;
|
|
4640
|
+
const pending = watchState.pendingAffectedTestFiles.get(file.projectName) ?? new Set();
|
|
4641
|
+
pending.add(file.testPath);
|
|
4642
|
+
watchState.pendingAffectedTestFiles.set(file.projectName, pending);
|
|
4643
|
+
seeded += 1;
|
|
4644
|
+
}
|
|
4645
|
+
return seeded;
|
|
4646
|
+
};
|
|
4647
|
+
const buildRerunOutcome = ({ rerunTestPaths, testTime, unhandledErrors })=>{
|
|
4648
|
+
const rerunPathSet = new Set(rerunTestPaths);
|
|
4649
|
+
const rerunResults = context.reporterResults.results.filter((result)=>rerunPathSet.has(result.testPath));
|
|
4650
|
+
const coverageMap = buildBrowserCoverageMap(rerunResults, coverageProvider);
|
|
4651
|
+
return {
|
|
4652
|
+
results: rerunResults,
|
|
4653
|
+
testResults: context.reporterResults.testResults.filter((result)=>rerunPathSet.has(result.testPath)),
|
|
4654
|
+
errors: unhandledErrors ?? [],
|
|
4655
|
+
testPaths: rerunTestPaths,
|
|
4656
|
+
duration: {
|
|
4657
|
+
buildTime: drainPendingBuildTime(watchState),
|
|
4658
|
+
testTime
|
|
4650
4659
|
},
|
|
4651
|
-
|
|
4652
|
-
|
|
4653
|
-
|
|
4660
|
+
coverage: coverageMap?.files().length ? {
|
|
4661
|
+
map: coverageMap.toJSON()
|
|
4662
|
+
} : void 0,
|
|
4663
|
+
resolveSourcemap: resolveBrowserSourcemap
|
|
4664
|
+
};
|
|
4665
|
+
};
|
|
4666
|
+
const createWatchSession = (execute)=>({
|
|
4667
|
+
runCycle: async (testPaths)=>{
|
|
4654
4668
|
const rerunStartTime = Date.now();
|
|
4655
|
-
const fatalErrorBeforeRun =
|
|
4669
|
+
const fatalErrorBeforeRun = fatalErrorRef.current;
|
|
4656
4670
|
let rerunError;
|
|
4657
4671
|
try {
|
|
4658
|
-
await
|
|
4672
|
+
await execute(testPaths);
|
|
4659
4673
|
} catch (error) {
|
|
4660
4674
|
rerunError = toError(error);
|
|
4661
|
-
throw error;
|
|
4662
|
-
} finally{
|
|
4663
|
-
const testTime = Math.max(0, Date.now() - rerunStartTime);
|
|
4664
|
-
const rerunFatalError = fatalError && fatalError !== fatalErrorBeforeRun ? fatalError : void 0;
|
|
4665
|
-
await finalizeWatchRerun({
|
|
4666
|
-
rerunTestPaths: files.map((file)=>file.testPath),
|
|
4667
|
-
testTime,
|
|
4668
|
-
unhandledErrors: rerunError ? [
|
|
4669
|
-
rerunError
|
|
4670
|
-
] : rerunFatalError ? [
|
|
4671
|
-
rerunFatalError
|
|
4672
|
-
] : void 0
|
|
4673
|
-
});
|
|
4674
|
-
logWatchReadyMessage(context, enableCliShortcuts);
|
|
4675
4675
|
}
|
|
4676
|
-
|
|
4677
|
-
|
|
4678
|
-
|
|
4679
|
-
|
|
4680
|
-
|
|
4681
|
-
|
|
4676
|
+
const rerunFatalError = fatalErrorRef.current && fatalErrorRef.current !== fatalErrorBeforeRun ? fatalErrorRef.current : void 0;
|
|
4677
|
+
return buildRerunOutcome({
|
|
4678
|
+
rerunTestPaths: testPaths,
|
|
4679
|
+
testTime: Math.max(0, Date.now() - rerunStartTime),
|
|
4680
|
+
unhandledErrors: rerunError ? [
|
|
4681
|
+
rerunError
|
|
4682
|
+
] : rerunFatalError ? [
|
|
4683
|
+
rerunFatalError
|
|
4684
|
+
] : void 0
|
|
4682
4685
|
});
|
|
4683
4686
|
},
|
|
4684
|
-
|
|
4685
|
-
|
|
4687
|
+
requestRerun: async (testPaths)=>{
|
|
4688
|
+
const seeded = seedPendingRerun(testPaths);
|
|
4689
|
+
if (testPaths && 0 === seeded) return;
|
|
4690
|
+
await watchSignals.runDispatchRerun();
|
|
4691
|
+
await watchSignals.awaitSignalledCycle();
|
|
4686
4692
|
}
|
|
4687
4693
|
});
|
|
4688
|
-
|
|
4689
|
-
|
|
4690
|
-
|
|
4691
|
-
|
|
4692
|
-
|
|
4693
|
-
|
|
4694
|
-
|
|
4695
|
-
|
|
4696
|
-
results: reporterResults,
|
|
4697
|
-
testResults: caseResults,
|
|
4698
|
-
duration,
|
|
4699
|
-
hasFailure: false,
|
|
4700
|
-
getSourcemap: getBrowserSourcemap,
|
|
4701
|
-
resolveSourcemap: resolveBrowserSourcemap,
|
|
4702
|
-
close: isWatchMode ? void 0 : async ()=>{
|
|
4703
|
-
sessionRegistry.clear();
|
|
4704
|
-
await destroyBrowserRuntime(runtime);
|
|
4705
|
-
},
|
|
4706
|
-
watch: watchHandles
|
|
4707
|
-
};
|
|
4708
|
-
if (isWatchMode) await notifyTestRunEnd({
|
|
4709
|
-
duration
|
|
4710
|
-
});
|
|
4711
|
-
if (isWatchMode) {
|
|
4712
|
-
triggerRerun = async ()=>{
|
|
4713
|
-
const newProjectEntries = await collectProjectEntries(context);
|
|
4714
|
-
const rerunPlan = planWatchRerun({
|
|
4715
|
-
projectEntries: newProjectEntries,
|
|
4716
|
-
previousTestFiles: watchState.lastTestFiles,
|
|
4717
|
-
affectedTestFiles: drainPendingAffectedTestFiles(watchState)
|
|
4718
|
-
});
|
|
4719
|
-
if (rerunPlan.filesChanged) {
|
|
4720
|
-
watchState.lastTestFiles = rerunPlan.currentTestFiles;
|
|
4721
|
-
if (0 === rerunPlan.currentTestFiles.length) {
|
|
4722
|
-
logger.log(color.cyan('No browser test files remain after update.\n'));
|
|
4723
|
-
logWatchReadyMessage(context, enableCliShortcuts);
|
|
4724
|
-
return;
|
|
4725
|
-
}
|
|
4726
|
-
logger.log(color.cyan(`Test file set changed, re-running ${rerunPlan.currentTestFiles.length} file(s)...\n`));
|
|
4727
|
-
await latestRerunScheduler.enqueueLatest(rerunPlan.currentTestFiles);
|
|
4728
|
-
return;
|
|
4729
|
-
}
|
|
4730
|
-
logWatchReadyMessage(context, enableCliShortcuts);
|
|
4731
|
-
};
|
|
4732
|
-
watchState.hooksEnabled = true;
|
|
4733
|
-
logWatchReadyMessage(context, enableCliShortcuts);
|
|
4734
|
-
}
|
|
4735
|
-
return result;
|
|
4736
|
-
}
|
|
4737
|
-
const testStart = Date.now();
|
|
4738
|
-
await runFilesWithPool(allTestFiles);
|
|
4739
|
-
const testTime = Date.now() - testStart;
|
|
4740
|
-
if (isWatchMode) triggerRerun = async ()=>{
|
|
4741
|
-
const newProjectEntries = await collectProjectEntries(context);
|
|
4742
|
-
const rerunPlan = planWatchRerun({
|
|
4743
|
-
projectEntries: newProjectEntries,
|
|
4744
|
-
previousTestFiles: watchState.lastTestFiles,
|
|
4745
|
-
affectedTestFiles: drainPendingAffectedTestFiles(watchState)
|
|
4746
|
-
});
|
|
4747
|
-
if (rerunPlan.filesChanged) {
|
|
4748
|
-
const deletedTestPaths = collectDeletedTestPaths(watchState.lastTestFiles, rerunPlan.currentTestFiles);
|
|
4749
|
-
if (deletedTestPaths.length > 0) context.updateReporterResultState([], [], deletedTestPaths);
|
|
4750
|
-
watchState.lastTestFiles = rerunPlan.currentTestFiles;
|
|
4751
|
-
if (0 === rerunPlan.currentTestFiles.length) {
|
|
4752
|
-
await latestRerunScheduler.enqueueLatest([]);
|
|
4753
|
-
logger.log(color.cyan('No browser test files remain after update.\n'));
|
|
4754
|
-
logWatchReadyMessage(context, enableCliShortcuts);
|
|
4755
|
-
return;
|
|
4756
|
-
}
|
|
4757
|
-
logger.log(color.cyan(`Test file set changed, re-running ${rerunPlan.currentTestFiles.length} file(s)...\n`));
|
|
4758
|
-
await latestRerunScheduler.enqueueLatest(rerunPlan.currentTestFiles);
|
|
4759
|
-
return;
|
|
4760
|
-
}
|
|
4761
|
-
if (0 === rerunPlan.affectedTestFiles.length) {
|
|
4762
|
-
logger.log(color.cyan('No affected browser test files detected, skipping re-run.\n'));
|
|
4763
|
-
logWatchReadyMessage(context, enableCliShortcuts);
|
|
4764
|
-
return;
|
|
4765
|
-
}
|
|
4766
|
-
logger.log(color.cyan(`Re-running ${rerunPlan.affectedTestFiles.length} affected test file(s)...\n`));
|
|
4767
|
-
await latestRerunScheduler.enqueueLatest(rerunPlan.affectedTestFiles);
|
|
4768
|
-
};
|
|
4769
|
-
const closeHeadlessRuntime = isWatchMode ? void 0 : async ()=>{
|
|
4770
|
-
sessionRegistry.clear();
|
|
4771
|
-
await destroyBrowserRuntime(runtime);
|
|
4772
|
-
};
|
|
4773
|
-
if (fatalError) return failWithError(fatalError, closeHeadlessRuntime);
|
|
4774
|
-
const duration = {
|
|
4775
|
-
totalTime: buildTime + testTime,
|
|
4694
|
+
projectEntries = runtime.projectEntries;
|
|
4695
|
+
totalTests = projectEntries.reduce((total, item)=>total + item.testFiles.length, 0);
|
|
4696
|
+
const buildTime = Date.now() - buildStart;
|
|
4697
|
+
if (filesOnly) return {
|
|
4698
|
+
results: [],
|
|
4699
|
+
testResults: [],
|
|
4700
|
+
duration: {
|
|
4701
|
+
totalTime: buildTime,
|
|
4776
4702
|
buildTime,
|
|
4777
|
-
testTime
|
|
4778
|
-
}
|
|
4779
|
-
|
|
4780
|
-
|
|
4781
|
-
|
|
4782
|
-
|
|
4783
|
-
results: reporterResults,
|
|
4784
|
-
testResults: caseResults,
|
|
4785
|
-
duration,
|
|
4786
|
-
hasFailure: isFailure,
|
|
4787
|
-
getSourcemap: getBrowserSourcemap,
|
|
4788
|
-
resolveSourcemap: resolveBrowserSourcemap,
|
|
4789
|
-
close: closeHeadlessRuntime,
|
|
4790
|
-
watch: watchHandles
|
|
4791
|
-
};
|
|
4792
|
-
if (isWatchMode) try {
|
|
4793
|
-
await notifyTestRunEnd({
|
|
4794
|
-
duration
|
|
4795
|
-
});
|
|
4796
|
-
} finally{
|
|
4797
|
-
await closeHeadlessRuntime?.();
|
|
4798
|
-
}
|
|
4799
|
-
if (isWatchMode && triggerRerun) {
|
|
4800
|
-
watchState.hooksEnabled = true;
|
|
4801
|
-
logWatchReadyMessage(context, enableCliShortcuts);
|
|
4802
|
-
}
|
|
4803
|
-
return result;
|
|
4804
|
-
}
|
|
4805
|
-
let currentTestFiles = allTestFiles;
|
|
4806
|
-
const RUNNER_FRAMES_READY_TIMEOUT_MS = 30000;
|
|
4807
|
-
let currentRunnerFramesSignature = null;
|
|
4808
|
-
const runnerFramesWaiters = new Map();
|
|
4809
|
-
const createTestFilesSignature = (testFiles)=>JSON.stringify(testFiles.map((testFile)=>normalize(testFile)));
|
|
4810
|
-
const markRunnerFramesReady = (testFiles)=>{
|
|
4811
|
-
const signature = createTestFilesSignature(testFiles);
|
|
4812
|
-
currentRunnerFramesSignature = signature;
|
|
4813
|
-
const waiters = runnerFramesWaiters.get(signature);
|
|
4814
|
-
if (!waiters) return;
|
|
4815
|
-
runnerFramesWaiters.delete(signature);
|
|
4816
|
-
for (const waiter of waiters)waiter();
|
|
4703
|
+
testTime: 0
|
|
4704
|
+
},
|
|
4705
|
+
hasFailure: false,
|
|
4706
|
+
getSourcemap: getBrowserSourcemap,
|
|
4707
|
+
resolveSourcemap: resolveBrowserSourcemap,
|
|
4708
|
+
close: ()=>destroyBrowserRuntime(runtime)
|
|
4817
4709
|
};
|
|
4818
|
-
|
|
4819
|
-
|
|
4820
|
-
|
|
4821
|
-
|
|
4822
|
-
|
|
4823
|
-
|
|
4824
|
-
|
|
4825
|
-
|
|
4826
|
-
|
|
4827
|
-
|
|
4828
|
-
|
|
4829
|
-
|
|
4830
|
-
|
|
4831
|
-
|
|
4832
|
-
|
|
4833
|
-
|
|
4834
|
-
|
|
4835
|
-
|
|
4836
|
-
|
|
4837
|
-
|
|
4838
|
-
|
|
4839
|
-
|
|
4840
|
-
|
|
4841
|
-
|
|
4710
|
+
if (0 === totalTests) {
|
|
4711
|
+
writeEmptyLaunchExitCode();
|
|
4712
|
+
await destroyBrowserRuntime(runtime);
|
|
4713
|
+
return allowEmptyRun ? createEmptyRunResult() : void 0;
|
|
4714
|
+
}
|
|
4715
|
+
const { browser, browserLaunchOptions, wsPort } = runtime;
|
|
4716
|
+
const allTestFiles = projectEntries.flatMap((entry)=>entry.testFiles.map((testPath)=>({
|
|
4717
|
+
testPath: normalize(testPath),
|
|
4718
|
+
projectName: entry.project.name
|
|
4719
|
+
})));
|
|
4720
|
+
const projectRuntimeConfigs = browserProjects.map((project)=>({
|
|
4721
|
+
name: project.name,
|
|
4722
|
+
environmentName: project.environmentName,
|
|
4723
|
+
projectRoot: normalize(project.rootPath),
|
|
4724
|
+
runtimeConfig: serializableConfig(projectRuntimeConfig(project, {
|
|
4725
|
+
envMode: 'static',
|
|
4726
|
+
envOverlay: env
|
|
4727
|
+
})),
|
|
4728
|
+
viewport: project.normalizedConfig.browser.viewport
|
|
4729
|
+
}));
|
|
4730
|
+
const maxTestTimeoutForRpc = getMaxTestTimeoutForRpc(browserProjects);
|
|
4731
|
+
const projectRunnerUrls = Object.fromEntries([
|
|
4732
|
+
...runtime.projectServers
|
|
4733
|
+
].map(([name, server])=>[
|
|
4734
|
+
name,
|
|
4735
|
+
`http://localhost:${server.port}`
|
|
4736
|
+
]));
|
|
4737
|
+
const hostOptions = {
|
|
4738
|
+
rootPath: normalize(context.rootPath),
|
|
4739
|
+
projects: projectRuntimeConfigs,
|
|
4740
|
+
snapshot: {
|
|
4741
|
+
updateSnapshot: options?.updateSnapshot ?? context.snapshotManager.options.updateSnapshot
|
|
4742
|
+
},
|
|
4743
|
+
runnerUrl: `http://localhost:${runtime.containerServer.port}`,
|
|
4744
|
+
projectRunnerUrls,
|
|
4745
|
+
wsPort,
|
|
4746
|
+
debug: isDebug(),
|
|
4747
|
+
rpcTimeout: maxTestTimeoutForRpc
|
|
4842
4748
|
};
|
|
4843
|
-
const
|
|
4844
|
-
|
|
4845
|
-
|
|
4846
|
-
|
|
4847
|
-
|
|
4749
|
+
const browserProviderProjects = browserProjects.map((project)=>({
|
|
4750
|
+
rootPath: normalize(project.rootPath),
|
|
4751
|
+
provider: project.normalizedConfig.browser.provider
|
|
4752
|
+
}));
|
|
4753
|
+
const implementationByProvider = new Map();
|
|
4754
|
+
for (const browserProject of browserProviderProjects)if (!implementationByProvider.has(browserProject.provider)) implementationByProvider.set(browserProject.provider, getBrowserProviderImplementation(browserProject.provider));
|
|
4755
|
+
let resolveDispatchPages = ()=>({});
|
|
4756
|
+
const setDispatchPageResolver = (resolver)=>{
|
|
4757
|
+
resolveDispatchPages = resolver;
|
|
4848
4758
|
};
|
|
4849
|
-
|
|
4850
|
-
|
|
4851
|
-
|
|
4852
|
-
|
|
4853
|
-
|
|
4854
|
-
containerPage = runtime.containerPage;
|
|
4855
|
-
logger.log(color.gray('\n[Watch] Reusing existing container page\n'));
|
|
4856
|
-
} else {
|
|
4857
|
-
isNewPage = true;
|
|
4858
|
-
containerContext = await browser.newContext({
|
|
4859
|
-
providerOptions: browserLaunchOptions.providerOptions,
|
|
4860
|
-
viewport: null
|
|
4861
|
-
});
|
|
4862
|
-
containerPage = await containerContext.newPage();
|
|
4863
|
-
containerPage.on('popup', async (popup)=>{
|
|
4864
|
-
await popup.close().catch(()=>{});
|
|
4865
|
-
});
|
|
4866
|
-
containerContext.on('page', async (page)=>{
|
|
4867
|
-
if (page !== containerPage) await page.close().catch(()=>{});
|
|
4759
|
+
const dispatchBrowserRpcRequest = async ({ request, target })=>{
|
|
4760
|
+
const timeoutFallbackMs = maxTestTimeoutForRpc;
|
|
4761
|
+
const provider = resolveProviderForTestPath({
|
|
4762
|
+
testPath: request.testPath,
|
|
4763
|
+
browserProjects: browserProviderProjects
|
|
4868
4764
|
});
|
|
4869
|
-
|
|
4870
|
-
|
|
4871
|
-
|
|
4765
|
+
const implementation = implementationByProvider.get(provider);
|
|
4766
|
+
if (!implementation) throw new Error(`Browser provider implementation not found: ${provider}`);
|
|
4767
|
+
const { runnerPage, containerPage } = resolveDispatchPages(target);
|
|
4768
|
+
if (target?.sessionId && !runnerPage) throw new Error(`Runner page session not found for browser dispatch: ${target.sessionId}`);
|
|
4769
|
+
if (!runnerPage && !containerPage) throw new Error('Browser container page is not initialized');
|
|
4770
|
+
try {
|
|
4771
|
+
return await implementation.dispatchRpc({
|
|
4772
|
+
containerPage: runnerPage ? void 0 : containerPage,
|
|
4773
|
+
runnerPage,
|
|
4774
|
+
request,
|
|
4775
|
+
timeoutFallbackMs
|
|
4776
|
+
});
|
|
4777
|
+
} catch (error) {
|
|
4778
|
+
if (error instanceof Error) throw error.message;
|
|
4779
|
+
throw String(error);
|
|
4872
4780
|
}
|
|
4873
|
-
|
|
4874
|
-
|
|
4875
|
-
|
|
4781
|
+
};
|
|
4782
|
+
runtime.dispatchHandlers.set(DISPATCH_NAMESPACE_BROWSER, async (dispatchRequest)=>{
|
|
4783
|
+
const request = validateBrowserRpcRequest(dispatchRequest.args);
|
|
4784
|
+
return dispatchBrowserRpcRequest({
|
|
4785
|
+
request,
|
|
4786
|
+
target: dispatchRequest.target
|
|
4876
4787
|
});
|
|
4877
|
-
}
|
|
4878
|
-
|
|
4879
|
-
const
|
|
4880
|
-
const
|
|
4881
|
-
const
|
|
4882
|
-
|
|
4883
|
-
throw new Error('Headed reload queue is not initialized');
|
|
4788
|
+
});
|
|
4789
|
+
runtime.setContainerOptions(hostOptions);
|
|
4790
|
+
const reporterResults = [];
|
|
4791
|
+
const caseResults = [];
|
|
4792
|
+
const fatalErrorRef = {
|
|
4793
|
+
current: null
|
|
4884
4794
|
};
|
|
4885
|
-
const
|
|
4886
|
-
|
|
4887
|
-
|
|
4888
|
-
|
|
4889
|
-
|
|
4890
|
-
|
|
4795
|
+
const runnerSinks = new Map(browserProjects.map((project)=>[
|
|
4796
|
+
project.name,
|
|
4797
|
+
createRunnerEventSink(context, project.normalizedConfig)
|
|
4798
|
+
]));
|
|
4799
|
+
const sinkForProjectName = (projectName)=>{
|
|
4800
|
+
const sink = runnerSinks.get(projectName);
|
|
4801
|
+
if (!sink) throw new Error(`No runner event sink for project "${projectName}"`);
|
|
4802
|
+
return sink;
|
|
4891
4803
|
};
|
|
4892
|
-
const
|
|
4893
|
-
|
|
4894
|
-
|
|
4895
|
-
|
|
4804
|
+
const silentConsoleController = createSilentConsoleController({
|
|
4805
|
+
runtimeConfig: {
|
|
4806
|
+
silent: context.normalizedConfig.silent,
|
|
4807
|
+
disableConsoleIntercept: context.normalizedConfig.disableConsoleIntercept
|
|
4808
|
+
},
|
|
4809
|
+
emitInterceptedLog: (log)=>sinkForProjectName(log.project).onConsoleLog(log),
|
|
4810
|
+
writeOriginalLog: ()=>{}
|
|
4811
|
+
});
|
|
4812
|
+
const snapshotRpcMethods = {
|
|
4813
|
+
async resolveSnapshotPath (testPath) {
|
|
4814
|
+
return resolveSnapshotPathDefault(testPath, context.normalizedConfig.resolveSnapshotPath);
|
|
4815
|
+
},
|
|
4816
|
+
async readSnapshotFile (filepath) {
|
|
4817
|
+
try {
|
|
4818
|
+
return await promises.readFile(filepath, 'utf-8');
|
|
4819
|
+
} catch {
|
|
4820
|
+
return null;
|
|
4821
|
+
}
|
|
4822
|
+
},
|
|
4823
|
+
async saveSnapshotFile (filepath, content) {
|
|
4824
|
+
const dir = dirname(filepath);
|
|
4825
|
+
await promises.mkdir(dir, {
|
|
4826
|
+
recursive: true
|
|
4827
|
+
});
|
|
4828
|
+
await promises.writeFile(filepath, content, 'utf-8');
|
|
4829
|
+
},
|
|
4830
|
+
async removeSnapshotFile (filepath) {
|
|
4831
|
+
try {
|
|
4832
|
+
await promises.unlink(filepath);
|
|
4833
|
+
} catch {}
|
|
4896
4834
|
}
|
|
4897
4835
|
};
|
|
4898
|
-
const
|
|
4899
|
-
|
|
4900
|
-
|
|
4901
|
-
|
|
4902
|
-
|
|
4836
|
+
const handleTestFileStart = async (payload)=>{
|
|
4837
|
+
if (phaseTrackers) {
|
|
4838
|
+
const tracker = new PhaseTracker({
|
|
4839
|
+
trace: {
|
|
4840
|
+
testPath: payload.testPath,
|
|
4841
|
+
project: payload.projectName
|
|
4842
|
+
},
|
|
4843
|
+
pid: nextBrowserFilePid++
|
|
4844
|
+
});
|
|
4845
|
+
tracker.transition('prepare');
|
|
4846
|
+
phaseTrackers.set(trackerKey(payload.projectName, payload.testPath), tracker);
|
|
4903
4847
|
}
|
|
4904
|
-
|
|
4905
|
-
|
|
4906
|
-
|
|
4907
|
-
|
|
4848
|
+
await sinkForProjectName(payload.projectName).onTestFileStart({
|
|
4849
|
+
testId: getFileTaskId(payload.testPath),
|
|
4850
|
+
testPath: payload.testPath,
|
|
4851
|
+
project: payload.projectName,
|
|
4852
|
+
tests: []
|
|
4908
4853
|
});
|
|
4909
|
-
return deferred.promise;
|
|
4910
4854
|
};
|
|
4911
|
-
const
|
|
4912
|
-
|
|
4913
|
-
|
|
4914
|
-
if (runId && pending.runId !== runId) return void logger.debug(`[Browser UI] Ignoring stale file-complete for ${testPath}. current=${pending.runId}, incoming=${runId}`);
|
|
4915
|
-
pendingHeadedReloads.delete(testPath);
|
|
4916
|
-
pending.deferred.resolve();
|
|
4855
|
+
const handleTestFileReady = async (payload)=>{
|
|
4856
|
+
phaseTrackers?.get(trackerKey(payload.project, payload.testPath))?.transition('tests');
|
|
4857
|
+
await sinkForProjectName(payload.project).onTestFileReady(payload);
|
|
4917
4858
|
};
|
|
4918
|
-
const
|
|
4919
|
-
|
|
4920
|
-
|
|
4921
|
-
reloadAck = await rpcManager.reloadTestFile(file.testPath, testNamePattern);
|
|
4922
|
-
await registerPendingHeadedReload(file.testPath, reloadAck.runId);
|
|
4923
|
-
} catch (error) {
|
|
4924
|
-
if (reloadAck?.runId) rejectPendingHeadedReload(file.testPath, toError(error), reloadAck.runId);
|
|
4925
|
-
throw error;
|
|
4926
|
-
}
|
|
4859
|
+
const handleTestSuiteStart = async (payload)=>{
|
|
4860
|
+
phaseTrackers?.get(trackerKey(payload.project, payload.testPath))?.recordSuiteStart(payload);
|
|
4861
|
+
await sinkForProjectName(payload.project).onTestSuiteStart(payload);
|
|
4927
4862
|
};
|
|
4928
|
-
const
|
|
4929
|
-
|
|
4930
|
-
|
|
4931
|
-
|
|
4932
|
-
|
|
4933
|
-
|
|
4934
|
-
|
|
4935
|
-
|
|
4936
|
-
|
|
4937
|
-
return currentTestFiles;
|
|
4938
|
-
},
|
|
4939
|
-
async onRunnerFramesReady (testFiles) {
|
|
4940
|
-
markRunnerFramesReady(testFiles);
|
|
4941
|
-
},
|
|
4942
|
-
async onTestFileStart (payload) {
|
|
4943
|
-
await handleTestFileStart(payload);
|
|
4944
|
-
},
|
|
4945
|
-
async onTestCaseResult (payload) {
|
|
4946
|
-
await handleTestCaseResult(payload);
|
|
4947
|
-
},
|
|
4948
|
-
async onTestFileComplete (payload) {
|
|
4949
|
-
try {
|
|
4950
|
-
await handleTestFileComplete(payload);
|
|
4951
|
-
resolvePendingHeadedReload(payload.testPath, payload.runId);
|
|
4952
|
-
} catch (error) {
|
|
4953
|
-
rejectPendingHeadedReload(payload.testPath, toError(error), payload.runId);
|
|
4954
|
-
throw error;
|
|
4955
|
-
}
|
|
4956
|
-
},
|
|
4957
|
-
async onLog (payload) {
|
|
4958
|
-
await handleLog(payload);
|
|
4959
|
-
},
|
|
4960
|
-
async onFatal (payload) {
|
|
4961
|
-
const error = new Error(payload.message);
|
|
4962
|
-
error.stack = payload.stack;
|
|
4963
|
-
rejectAllPendingHeadedReloads(error);
|
|
4964
|
-
await handleFatal(payload);
|
|
4965
|
-
},
|
|
4966
|
-
async dispatch (request) {
|
|
4967
|
-
return dispatchRouter.dispatch(request);
|
|
4968
|
-
}
|
|
4969
|
-
});
|
|
4970
|
-
let rpcManager;
|
|
4971
|
-
if (isWatchMode && runtime.rpcManager) {
|
|
4972
|
-
rpcManager = runtime.rpcManager;
|
|
4973
|
-
rpcManager.updateMethods(createRpcMethods(), rejectAllPendingHeadedReloads);
|
|
4974
|
-
const existingWs = rpcManager.currentWebSocket;
|
|
4975
|
-
if (existingWs) rpcManager.reattach(existingWs);
|
|
4976
|
-
} else {
|
|
4977
|
-
rpcManager = new ContainerRpcManager(wss, createRpcMethods(), rejectAllPendingHeadedReloads);
|
|
4978
|
-
if (isWatchMode) runtime.rpcManager = rpcManager;
|
|
4979
|
-
}
|
|
4980
|
-
if (isNewPage) {
|
|
4981
|
-
const pagePath = '/';
|
|
4982
|
-
const containerPort = runtime.containerServer.port;
|
|
4983
|
-
await containerPage.goto(`http://localhost:${containerPort}${pagePath}`, {
|
|
4984
|
-
waitUntil: 'load'
|
|
4863
|
+
const handleTestSuiteResult = async (payload)=>{
|
|
4864
|
+
phaseTrackers?.get(trackerKey(payload.project, payload.testPath))?.recordSuiteResult(payload);
|
|
4865
|
+
await sinkForProjectName(payload.project).onTestSuiteResult(payload);
|
|
4866
|
+
if ('passed-only' === context.normalizedConfig.silent) silentConsoleController.flushBufferedLogsForTask({
|
|
4867
|
+
taskId: payload.testId,
|
|
4868
|
+
status: payload.status,
|
|
4869
|
+
taskParentNames: payload.parentNames,
|
|
4870
|
+
taskType: 'suite',
|
|
4871
|
+
testPath: payload.testPath
|
|
4985
4872
|
});
|
|
4986
|
-
|
|
4987
|
-
|
|
4988
|
-
|
|
4989
|
-
|
|
4990
|
-
|
|
4873
|
+
};
|
|
4874
|
+
const handleTestCaseStart = async (payload)=>{
|
|
4875
|
+
phaseTrackers?.get(trackerKey(payload.project, payload.testPath))?.recordCaseStart(payload);
|
|
4876
|
+
sinkForProjectName(payload.project).onTestCaseStart(payload);
|
|
4877
|
+
};
|
|
4878
|
+
const handleTestCaseResult = async (payload)=>{
|
|
4879
|
+
caseResults.push(payload);
|
|
4880
|
+
phaseTrackers?.get(trackerKey(payload.project, payload.testPath))?.recordCaseResult(payload);
|
|
4881
|
+
await sinkForProjectName(payload.project).onTestCaseResult(payload);
|
|
4882
|
+
if ('passed-only' === context.normalizedConfig.silent) silentConsoleController.flushBufferedLogsForTask({
|
|
4883
|
+
taskId: payload.testId,
|
|
4884
|
+
status: payload.status,
|
|
4885
|
+
taskParentNames: payload.parentNames,
|
|
4886
|
+
taskType: 'case',
|
|
4887
|
+
testPath: payload.testPath
|
|
4991
4888
|
});
|
|
4992
|
-
|
|
4993
|
-
|
|
4994
|
-
|
|
4995
|
-
|
|
4996
|
-
|
|
4997
|
-
|
|
4998
|
-
|
|
4999
|
-
|
|
4889
|
+
};
|
|
4890
|
+
const handleTestFileComplete = async (payload)=>{
|
|
4891
|
+
reporterResults.push(payload);
|
|
4892
|
+
context.updateReporterResultState([
|
|
4893
|
+
payload
|
|
4894
|
+
], payload.results);
|
|
4895
|
+
if (phaseTrackers) {
|
|
4896
|
+
const key = trackerKey(payload.project, payload.testPath);
|
|
4897
|
+
const tracker = phaseTrackers.get(key);
|
|
4898
|
+
if (tracker) {
|
|
4899
|
+
tracker.end();
|
|
4900
|
+
const events = tracker.getTraceEvents();
|
|
4901
|
+
if (events) onTraceEvents?.(events);
|
|
4902
|
+
phaseTrackers.delete(key);
|
|
5000
4903
|
}
|
|
5001
|
-
} catch (error) {
|
|
5002
|
-
fatalError = fatalError ?? toError(error);
|
|
5003
|
-
if (isWatchMode) ensureProcessExitCode(1);
|
|
5004
4904
|
}
|
|
5005
|
-
|
|
5006
|
-
|
|
5007
|
-
|
|
5008
|
-
|
|
5009
|
-
|
|
5010
|
-
|
|
5011
|
-
updateSnapshot: context.snapshotManager.options.updateSnapshot
|
|
5012
|
-
}
|
|
5013
|
-
};
|
|
5014
|
-
runtime.setContainerOptions(refreshedHostOptions);
|
|
5015
|
-
const [, newProjectEntries] = await Promise.all([
|
|
5016
|
-
rpcManager.updateHostConfig(refreshedHostOptions),
|
|
5017
|
-
collectProjectEntries(context)
|
|
5018
|
-
]);
|
|
5019
|
-
const rerunPlan = planWatchRerun({
|
|
5020
|
-
projectEntries: newProjectEntries,
|
|
5021
|
-
previousTestFiles: watchState.lastTestFiles,
|
|
5022
|
-
affectedTestFiles: drainPendingAffectedTestFiles(watchState)
|
|
4905
|
+
if ('passed-only' === context.normalizedConfig.silent) silentConsoleController.flushBufferedLogsForTask({
|
|
4906
|
+
taskId: payload.testId,
|
|
4907
|
+
status: payload.status,
|
|
4908
|
+
taskParentNames: payload.parentNames,
|
|
4909
|
+
taskType: 'file',
|
|
4910
|
+
testPath: payload.testPath
|
|
5023
4911
|
});
|
|
5024
|
-
|
|
5025
|
-
const deletedTestPaths = collectDeletedTestPaths(watchState.lastTestFiles, rerunPlan.currentTestFiles);
|
|
5026
|
-
if (deletedTestPaths.length > 0) context.updateReporterResultState([], [], deletedTestPaths);
|
|
5027
|
-
watchState.lastTestFiles = rerunPlan.currentTestFiles;
|
|
5028
|
-
currentTestFiles = rerunPlan.currentTestFiles;
|
|
5029
|
-
await rpcManager.notifyTestFileUpdate(currentTestFiles);
|
|
5030
|
-
if (0 === currentTestFiles.length) {
|
|
5031
|
-
logger.log(color.cyan('No browser test files remain after update.\n'));
|
|
5032
|
-
logWatchReadyMessage(context, enableCliShortcuts);
|
|
5033
|
-
return;
|
|
5034
|
-
}
|
|
5035
|
-
await waitForRunnerFramesReady(currentTestFiles.map((file)=>file.testPath));
|
|
5036
|
-
}
|
|
5037
|
-
if (rerunPlan.normalizedAffectedTestFiles.length > 0) {
|
|
5038
|
-
logger.log(color.cyan(`Re-running ${rerunPlan.normalizedAffectedTestFiles.length} affected test file(s)...\n`));
|
|
5039
|
-
prepareWatchRerunState(context);
|
|
5040
|
-
await notifyTestRunStart();
|
|
5041
|
-
const rerunStartTime = Date.now();
|
|
5042
|
-
const fatalErrorBeforeRun = fatalError;
|
|
5043
|
-
let rerunError;
|
|
5044
|
-
try {
|
|
5045
|
-
for (const testFile of rerunPlan.normalizedAffectedTestFiles)await enqueueHeadedReload(getTestFileInfo(testFile));
|
|
5046
|
-
} catch (error) {
|
|
5047
|
-
rerunError = toError(error);
|
|
5048
|
-
throw error;
|
|
5049
|
-
} finally{
|
|
5050
|
-
const testTime = Math.max(0, Date.now() - rerunStartTime);
|
|
5051
|
-
const rerunFatalError = fatalError && fatalError !== fatalErrorBeforeRun ? fatalError : void 0;
|
|
5052
|
-
await finalizeWatchRerun({
|
|
5053
|
-
rerunTestPaths: rerunPlan.normalizedAffectedTestFiles,
|
|
5054
|
-
testTime,
|
|
5055
|
-
unhandledErrors: rerunError ? [
|
|
5056
|
-
rerunError
|
|
5057
|
-
] : rerunFatalError ? [
|
|
5058
|
-
rerunFatalError
|
|
5059
|
-
] : void 0
|
|
5060
|
-
});
|
|
5061
|
-
logWatchReadyMessage(context, enableCliShortcuts);
|
|
5062
|
-
}
|
|
5063
|
-
} else if (rerunPlan.filesChanged) logWatchReadyMessage(context, enableCliShortcuts);
|
|
5064
|
-
else {
|
|
5065
|
-
logger.log(color.cyan('Tests will be re-executed automatically\n'));
|
|
5066
|
-
logWatchReadyMessage(context, enableCliShortcuts);
|
|
5067
|
-
}
|
|
4912
|
+
await sinkForProjectName(payload.project).onTestFileResult(payload);
|
|
5068
4913
|
};
|
|
5069
|
-
const
|
|
5070
|
-
|
|
5071
|
-
|
|
5072
|
-
|
|
5073
|
-
|
|
5074
|
-
|
|
5075
|
-
|
|
5076
|
-
|
|
4914
|
+
const handleLog = async (payload)=>{
|
|
4915
|
+
const log = {
|
|
4916
|
+
content: payload.content,
|
|
4917
|
+
name: getPrettyConsoleName(payload.level),
|
|
4918
|
+
taskId: payload.taskId,
|
|
4919
|
+
taskName: payload.taskName,
|
|
4920
|
+
taskParentNames: payload.taskParentNames,
|
|
4921
|
+
taskType: payload.taskType,
|
|
4922
|
+
testPath: payload.testPath,
|
|
4923
|
+
project: payload.projectName,
|
|
4924
|
+
type: payload.type,
|
|
4925
|
+
trace: payload.trace
|
|
4926
|
+
};
|
|
4927
|
+
silentConsoleController.onConsoleLog(log);
|
|
4928
|
+
};
|
|
4929
|
+
const handleFatal = async (payload)=>{
|
|
4930
|
+
const error = new Error(payload.message);
|
|
4931
|
+
error.stack = payload.stack;
|
|
4932
|
+
fatalErrorRef.current = error;
|
|
4933
|
+
};
|
|
4934
|
+
const runSnapshotRpc = async (request)=>{
|
|
4935
|
+
switch(request.method){
|
|
4936
|
+
case 'resolveSnapshotPath':
|
|
4937
|
+
return snapshotRpcMethods.resolveSnapshotPath(request.args.testPath);
|
|
4938
|
+
case 'readSnapshotFile':
|
|
4939
|
+
return snapshotRpcMethods.readSnapshotFile(request.args.filepath);
|
|
4940
|
+
case 'saveSnapshotFile':
|
|
4941
|
+
return snapshotRpcMethods.saveSnapshotFile(request.args.filepath, request.args.content);
|
|
4942
|
+
case 'removeSnapshotFile':
|
|
4943
|
+
return snapshotRpcMethods.removeSnapshotFile(request.args.filepath);
|
|
4944
|
+
default:
|
|
4945
|
+
{
|
|
4946
|
+
const _exhaustive = request;
|
|
4947
|
+
return _exhaustive;
|
|
4948
|
+
}
|
|
4949
|
+
}
|
|
5077
4950
|
};
|
|
5078
|
-
|
|
5079
|
-
|
|
5080
|
-
|
|
5081
|
-
|
|
5082
|
-
|
|
4951
|
+
const createDispatchRouter = (options)=>createHostDispatchRouter({
|
|
4952
|
+
routerOptions: options,
|
|
4953
|
+
runnerCallbacks: {
|
|
4954
|
+
onTestFileStart: handleTestFileStart,
|
|
4955
|
+
onTestFileReady: handleTestFileReady,
|
|
4956
|
+
onTestSuiteStart: handleTestSuiteStart,
|
|
4957
|
+
onTestSuiteResult: handleTestSuiteResult,
|
|
4958
|
+
onTestCaseStart: handleTestCaseStart,
|
|
4959
|
+
onTestCaseResult: handleTestCaseResult,
|
|
4960
|
+
onTestFileComplete: handleTestFileComplete,
|
|
4961
|
+
onLog: handleLog,
|
|
4962
|
+
onFatal: handleFatal
|
|
4963
|
+
},
|
|
4964
|
+
runSnapshotRpc,
|
|
4965
|
+
extensionHandlers: runtime.dispatchHandlers,
|
|
4966
|
+
onDuplicateNamespace: (namespace)=>{
|
|
4967
|
+
logger.debug(`[Dispatch] Skip registering dispatch namespace "${namespace}" because it is already reserved`);
|
|
4968
|
+
}
|
|
4969
|
+
});
|
|
4970
|
+
const schedulerDeps = {
|
|
4971
|
+
context,
|
|
4972
|
+
allTestFiles,
|
|
4973
|
+
hostOptions,
|
|
4974
|
+
isWatchMode,
|
|
4975
|
+
createDispatchRouter,
|
|
4976
|
+
fatalErrorRef,
|
|
4977
|
+
watchSignals,
|
|
4978
|
+
setDispatchPageResolver,
|
|
4979
|
+
createWatchSession,
|
|
4980
|
+
collectProjectEntries: ()=>browserRsbuild_collectProjectEntries(context),
|
|
4981
|
+
logWatchReady: ()=>logWatchReadyMessage(context, enableCliShortcuts),
|
|
4982
|
+
destroyRuntime: ()=>destroyBrowserRuntime(runtime)
|
|
5083
4983
|
};
|
|
4984
|
+
const { testTime, watchSession, close } = useHeadlessDirect ? await createHeadlessScheduler({
|
|
4985
|
+
...schedulerDeps,
|
|
4986
|
+
browser,
|
|
4987
|
+
browserLaunchOptions,
|
|
4988
|
+
projectServers: runtime.projectServers,
|
|
4989
|
+
projectRuntimeConfigs,
|
|
4990
|
+
watchState,
|
|
4991
|
+
handlers: {
|
|
4992
|
+
handleFatal,
|
|
4993
|
+
handleTestFileComplete
|
|
4994
|
+
}
|
|
4995
|
+
}) : await createHeadedScheduler({
|
|
4996
|
+
...schedulerDeps,
|
|
4997
|
+
runtime,
|
|
4998
|
+
handlers: {
|
|
4999
|
+
handleTestFileStart,
|
|
5000
|
+
handleTestCaseResult,
|
|
5001
|
+
handleTestFileComplete,
|
|
5002
|
+
handleLog,
|
|
5003
|
+
handleFatal
|
|
5004
|
+
}
|
|
5005
|
+
});
|
|
5006
|
+
if (fatalErrorRef.current) return failWithError(fatalErrorRef.current, close);
|
|
5084
5007
|
context.updateReporterResultState(reporterResults, caseResults);
|
|
5085
|
-
|
|
5086
|
-
|
|
5087
|
-
const result = {
|
|
5008
|
+
watchState.hooksEnabled = isWatchMode;
|
|
5009
|
+
return {
|
|
5088
5010
|
results: reporterResults,
|
|
5089
5011
|
testResults: caseResults,
|
|
5090
|
-
duration
|
|
5091
|
-
|
|
5012
|
+
duration: {
|
|
5013
|
+
totalTime: buildTime + testTime,
|
|
5014
|
+
buildTime,
|
|
5015
|
+
testTime
|
|
5016
|
+
},
|
|
5017
|
+
hasFailure: reporterResults.some((result)=>'fail' === result.status),
|
|
5092
5018
|
getSourcemap: getBrowserSourcemap,
|
|
5093
5019
|
resolveSourcemap: resolveBrowserSourcemap,
|
|
5094
|
-
close
|
|
5095
|
-
|
|
5020
|
+
close,
|
|
5021
|
+
watchSession
|
|
5096
5022
|
};
|
|
5097
|
-
if (isWatchMode) try {
|
|
5098
|
-
await notifyTestRunEnd({
|
|
5099
|
-
duration
|
|
5100
|
-
});
|
|
5101
|
-
} finally{
|
|
5102
|
-
await closeContainerRuntime?.();
|
|
5103
|
-
}
|
|
5104
|
-
if (isWatchMode && triggerRerun) {
|
|
5105
|
-
watchState.hooksEnabled = true;
|
|
5106
|
-
logWatchReadyMessage(context, enableCliShortcuts);
|
|
5107
|
-
}
|
|
5108
|
-
return result;
|
|
5109
5023
|
};
|
|
5110
5024
|
const listBrowserTests = async (context, options)=>{
|
|
5111
5025
|
const browserProjects = options?.projects ?? getBrowserProjects(context);
|
|
@@ -5164,7 +5078,8 @@ const listBrowserTests = async (context, options)=>{
|
|
|
5164
5078
|
environmentName: project.environmentName,
|
|
5165
5079
|
projectRoot: normalize(project.rootPath),
|
|
5166
5080
|
runtimeConfig: serializableConfig(projectRuntimeConfig(project, {
|
|
5167
|
-
envMode: 'static'
|
|
5081
|
+
envMode: 'static',
|
|
5082
|
+
envOverlay: options?.env
|
|
5168
5083
|
})),
|
|
5169
5084
|
viewport: project.normalizedConfig.browser.viewport
|
|
5170
5085
|
}));
|
|
@@ -5297,8 +5212,11 @@ const emptyOutcome = ()=>({
|
|
|
5297
5212
|
});
|
|
5298
5213
|
async function createBrowserExecutor(context, options) {
|
|
5299
5214
|
const { projects, coverageProvider, shardedEntries, freezeShardedEntries, filesOnly, allowEmptyRun, appliedModifyRstestConfigEnvironments } = options;
|
|
5215
|
+
const isWatchMode = 'watch' === context.command;
|
|
5300
5216
|
let deferredClose;
|
|
5301
5217
|
let inFlightCycle;
|
|
5218
|
+
let invalidationCallback;
|
|
5219
|
+
let watchSession;
|
|
5302
5220
|
const foldOutcome = (result)=>{
|
|
5303
5221
|
if (!result) return emptyOutcome();
|
|
5304
5222
|
const map = buildBrowserCoverageMap(result.results, coverageProvider);
|
|
@@ -5322,6 +5240,15 @@ async function createBrowserExecutor(context, options) {
|
|
|
5322
5240
|
projects,
|
|
5323
5241
|
async init () {},
|
|
5324
5242
|
async runCycle (opts) {
|
|
5243
|
+
if (watchSession) {
|
|
5244
|
+
const cycle = watchSession.runCycle(opts.fileFilters ?? []);
|
|
5245
|
+
inFlightCycle = cycle;
|
|
5246
|
+
try {
|
|
5247
|
+
return await cycle;
|
|
5248
|
+
} finally{
|
|
5249
|
+
inFlightCycle = void 0;
|
|
5250
|
+
}
|
|
5251
|
+
}
|
|
5325
5252
|
const cycle = runBrowserController(context, {
|
|
5326
5253
|
projects,
|
|
5327
5254
|
shardedEntries,
|
|
@@ -5330,24 +5257,37 @@ async function createBrowserExecutor(context, options) {
|
|
|
5330
5257
|
appliedModifyRstestConfigEnvironments,
|
|
5331
5258
|
onTraceEvents: opts.onTraceEvents,
|
|
5332
5259
|
env: opts.env,
|
|
5333
|
-
updateSnapshot: opts.updateSnapshot
|
|
5260
|
+
updateSnapshot: opts.updateSnapshot,
|
|
5261
|
+
onInvalidate: isWatchMode ? (hint)=>invalidationCallback?.(hint) : void 0
|
|
5334
5262
|
});
|
|
5335
5263
|
inFlightCycle = cycle;
|
|
5336
5264
|
try {
|
|
5337
5265
|
const result = await cycle;
|
|
5338
5266
|
deferredClose = result?.close;
|
|
5267
|
+
watchSession = result?.watchSession;
|
|
5339
5268
|
return foldOutcome(result);
|
|
5340
5269
|
} finally{
|
|
5341
5270
|
inFlightCycle = void 0;
|
|
5342
5271
|
}
|
|
5343
5272
|
},
|
|
5344
|
-
|
|
5273
|
+
onInvalidate (cb) {
|
|
5274
|
+
invalidationCallback = cb;
|
|
5275
|
+
},
|
|
5276
|
+
hasWatchSession () {
|
|
5277
|
+
return void 0 !== watchSession;
|
|
5278
|
+
},
|
|
5279
|
+
async requestRerun (testPaths) {
|
|
5280
|
+
if (!watchSession) return void logger.log(color.yellow('\nBrowser Mode has no live watch session, so this rerun skipped it.'));
|
|
5281
|
+
await watchSession.requestRerun(testPaths);
|
|
5282
|
+
},
|
|
5283
|
+
async collect (opts) {
|
|
5345
5284
|
const pending = listBrowserTests(context, {
|
|
5346
5285
|
projects,
|
|
5347
5286
|
shardedEntries,
|
|
5348
5287
|
freezeShardedEntries,
|
|
5349
5288
|
filesOnly,
|
|
5350
|
-
appliedModifyRstestConfigEnvironments
|
|
5289
|
+
appliedModifyRstestConfigEnvironments,
|
|
5290
|
+
env: opts.env
|
|
5351
5291
|
});
|
|
5352
5292
|
inFlightCycle = pending;
|
|
5353
5293
|
try {
|
|
@@ -5362,6 +5302,11 @@ async function createBrowserExecutor(context, options) {
|
|
|
5362
5302
|
},
|
|
5363
5303
|
async close () {
|
|
5364
5304
|
if (inFlightCycle) await inFlightCycle.catch(()=>void 0);
|
|
5305
|
+
if (isWatchMode) {
|
|
5306
|
+
watchSession = void 0;
|
|
5307
|
+
await cleanupWatchRuntime();
|
|
5308
|
+
return;
|
|
5309
|
+
}
|
|
5365
5310
|
const close = deferredClose;
|
|
5366
5311
|
deferredClose = void 0;
|
|
5367
5312
|
await close?.();
|