@rstest/browser 0.8.5 → 0.9.0
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/LICENSE-APACHE-2.0 +202 -0
- package/NOTICE +11 -0
- package/dist/361.js +8 -0
- package/dist/augmentExpect.d.ts +73 -0
- package/dist/browser-container/container-static/css/index.5c72297783.css +1 -0
- package/dist/browser-container/container-static/js/{565.226c9ef5.js → 101.36a8ccdf84.js} +4024 -3856
- package/dist/browser-container/container-static/js/101.36a8ccdf84.js.LICENSE.txt +1 -0
- package/dist/browser-container/container-static/js/{index.c1d17467.js → index.0687a8142a.js} +742 -692
- package/dist/browser-container/container-static/js/{lib-react.97ee79b0.js → lib-react.dcf2a5e57a.js} +10 -10
- package/dist/browser-container/container-static/js/lib-react.dcf2a5e57a.js.LICENSE.txt +1 -0
- package/dist/browser-container/index.html +1 -1
- package/dist/browser.d.ts +2 -0
- package/dist/browser.js +583 -0
- package/dist/browserRpcRegistry.d.ts +18 -0
- package/dist/client/api.d.ts +3 -0
- package/dist/client/browserRpc.d.ts +2 -0
- package/dist/client/dispatchTransport.d.ts +11 -0
- package/dist/client/entry.d.ts +1 -5
- package/dist/client/locator.d.ts +125 -0
- package/dist/client/snapshot.d.ts +0 -6
- package/dist/concurrency.d.ts +12 -0
- package/dist/dispatchCapabilities.d.ts +34 -0
- package/dist/dispatchRouter.d.ts +20 -0
- package/dist/headlessLatestRerunScheduler.d.ts +19 -0
- package/dist/headlessTransport.d.ts +12 -0
- package/dist/index.js +1580 -258
- package/dist/protocol.d.ts +44 -33
- package/dist/providers/index.d.ts +79 -0
- package/dist/providers/playwright/compileLocator.d.ts +3 -0
- package/dist/providers/playwright/dispatchBrowserRpc.d.ts +13 -0
- package/dist/providers/playwright/expectUtils.d.ts +24 -0
- package/dist/providers/playwright/implementation.d.ts +2 -0
- package/dist/providers/playwright/index.d.ts +1 -0
- package/dist/providers/playwright/runtime.d.ts +5 -0
- package/dist/providers/playwright/textMatcher.d.ts +8 -0
- package/dist/rpcProtocol.d.ts +145 -0
- package/dist/runSession.d.ts +33 -0
- package/dist/sessionRegistry.d.ts +34 -0
- package/dist/sourceMap/sourceMapLoader.d.ts +14 -0
- package/dist/watchRerunPlanner.d.ts +21 -0
- package/package.json +15 -10
- package/src/AGENTS.md +128 -0
- package/src/augmentExpect.ts +62 -0
- package/src/browser.ts +3 -0
- package/src/browserRpcRegistry.ts +57 -0
- package/src/client/AGENTS.md +82 -0
- package/src/client/api.ts +213 -0
- package/src/client/browserRpc.ts +86 -0
- package/src/client/dispatchTransport.ts +178 -0
- package/src/client/entry.ts +96 -33
- package/src/client/locator.ts +452 -0
- package/src/client/snapshot.ts +32 -97
- package/src/client/sourceMapSupport.ts +26 -37
- package/src/concurrency.ts +62 -0
- package/src/dispatchCapabilities.ts +162 -0
- package/src/dispatchRouter.ts +82 -0
- package/src/env.d.ts +8 -1
- package/src/headlessLatestRerunScheduler.ts +76 -0
- package/src/headlessTransport.ts +28 -0
- package/src/hostController.ts +1292 -367
- package/src/protocol.ts +66 -31
- package/src/providers/index.ts +103 -0
- package/src/providers/playwright/compileLocator.ts +130 -0
- package/src/providers/playwright/dispatchBrowserRpc.ts +372 -0
- package/src/providers/playwright/expectUtils.ts +57 -0
- package/src/providers/playwright/implementation.ts +33 -0
- package/src/providers/playwright/index.ts +1 -0
- package/src/providers/playwright/runtime.ts +32 -0
- package/src/providers/playwright/textMatcher.ts +10 -0
- package/src/rpcProtocol.ts +220 -0
- package/src/runSession.ts +110 -0
- package/src/sessionRegistry.ts +89 -0
- package/src/sourceMap/sourceMapLoader.ts +96 -0
- package/src/watchRerunPlanner.ts +77 -0
- package/dist/browser-container/container-static/css/index.5a71c757.css +0 -1
- package/dist/browser-container/container-static/js/565.226c9ef5.js.LICENSE.txt +0 -1
- package/dist/browser-container/container-static/js/lib-react.97ee79b0.js.LICENSE.txt +0 -1
- package/dist/browser-container/container-static/js/scheduler.5accca0c.js +0 -407
- package/dist/browser-container/scheduler.html +0 -19
package/dist/index.js
CHANGED
|
@@ -7,6 +7,9 @@ import open_editor from "open-editor";
|
|
|
7
7
|
import { basename, dirname, join, normalize, relative, resolve as external_pathe_resolve } from "pathe";
|
|
8
8
|
import sirv from "sirv";
|
|
9
9
|
import { WebSocketServer } from "ws";
|
|
10
|
+
import node_os from "node:os";
|
|
11
|
+
import convert_source_map from "convert-source-map";
|
|
12
|
+
import { DISPATCH_RPC_BRIDGE_NAME, DISPATCH_MESSAGE_TYPE, DISPATCH_NAMESPACE_RUNNER } from "./361.js";
|
|
10
13
|
__webpack_require__.add({
|
|
11
14
|
"../../node_modules/.pnpm/picomatch@4.0.3/node_modules/picomatch/index.js" (module, __unused_rspack_exports, __webpack_require__) {
|
|
12
15
|
const pico = __webpack_require__("../../node_modules/.pnpm/picomatch@4.0.3/node_modules/picomatch/lib/picomatch.js");
|
|
@@ -1610,6 +1613,909 @@ function createBirpc($functions, options) {
|
|
|
1610
1613
|
_promiseInit = on(onMessage);
|
|
1611
1614
|
return rpc;
|
|
1612
1615
|
}
|
|
1616
|
+
const DEFAULT_MAX_HEADLESS_WORKERS = 12;
|
|
1617
|
+
const getNumCpus = ()=>node_os.availableParallelism?.() ?? node_os.cpus().length;
|
|
1618
|
+
const parseWorkers = (maxWorkers, numCpus = getNumCpus())=>{
|
|
1619
|
+
const parsed = Number.parseInt(maxWorkers.toString(), 10);
|
|
1620
|
+
if ('string' == typeof maxWorkers && maxWorkers.trim().endsWith('%')) {
|
|
1621
|
+
const workers = Math.floor(parsed / 100 * numCpus);
|
|
1622
|
+
return Math.max(workers, 1);
|
|
1623
|
+
}
|
|
1624
|
+
return parsed > 0 ? parsed : 1;
|
|
1625
|
+
};
|
|
1626
|
+
const resolveDefaultHeadlessWorkers = (command, numCpus = getNumCpus())=>{
|
|
1627
|
+
const baseWorkers = Math.max(Math.min(DEFAULT_MAX_HEADLESS_WORKERS, numCpus - 1), 1);
|
|
1628
|
+
return 'watch' === command ? Math.max(Math.floor(baseWorkers / 2), 1) : baseWorkers;
|
|
1629
|
+
};
|
|
1630
|
+
const getHeadlessConcurrency = (context, totalTests)=>{
|
|
1631
|
+
if (totalTests <= 0) return 1;
|
|
1632
|
+
const maxWorkers = context.normalizedConfig.pool.maxWorkers;
|
|
1633
|
+
if (void 0 !== maxWorkers) return Math.min(parseWorkers(maxWorkers), totalTests);
|
|
1634
|
+
return Math.min(resolveDefaultHeadlessWorkers(context.command), totalTests);
|
|
1635
|
+
};
|
|
1636
|
+
const toErrorMessage = (error)=>error instanceof Error ? error.message : String(error);
|
|
1637
|
+
class HostDispatchRouter {
|
|
1638
|
+
handlers = new Map();
|
|
1639
|
+
options;
|
|
1640
|
+
constructor(options){
|
|
1641
|
+
this.options = options ?? {};
|
|
1642
|
+
}
|
|
1643
|
+
register(namespace, handler) {
|
|
1644
|
+
this.handlers.set(namespace, handler);
|
|
1645
|
+
}
|
|
1646
|
+
unregister(namespace) {
|
|
1647
|
+
this.handlers.delete(namespace);
|
|
1648
|
+
}
|
|
1649
|
+
has(namespace) {
|
|
1650
|
+
return this.handlers.has(namespace);
|
|
1651
|
+
}
|
|
1652
|
+
async dispatch(request) {
|
|
1653
|
+
const runToken = request.runToken;
|
|
1654
|
+
if ('number' == typeof runToken && this.options.isRunTokenStale?.(runToken)) {
|
|
1655
|
+
this.options.onStale?.(request);
|
|
1656
|
+
return {
|
|
1657
|
+
requestId: request.requestId,
|
|
1658
|
+
runToken,
|
|
1659
|
+
stale: true
|
|
1660
|
+
};
|
|
1661
|
+
}
|
|
1662
|
+
const handler = this.handlers.get(request.namespace);
|
|
1663
|
+
if (!handler) return {
|
|
1664
|
+
requestId: request.requestId,
|
|
1665
|
+
runToken,
|
|
1666
|
+
error: `No dispatch handler registered for namespace "${request.namespace}"`
|
|
1667
|
+
};
|
|
1668
|
+
try {
|
|
1669
|
+
const result = await handler(request);
|
|
1670
|
+
return {
|
|
1671
|
+
requestId: request.requestId,
|
|
1672
|
+
runToken,
|
|
1673
|
+
result
|
|
1674
|
+
};
|
|
1675
|
+
} catch (error) {
|
|
1676
|
+
return {
|
|
1677
|
+
requestId: request.requestId,
|
|
1678
|
+
runToken,
|
|
1679
|
+
error: toErrorMessage(error)
|
|
1680
|
+
};
|
|
1681
|
+
}
|
|
1682
|
+
}
|
|
1683
|
+
}
|
|
1684
|
+
const toSnapshotRpcRequest = (request)=>{
|
|
1685
|
+
switch(request.method){
|
|
1686
|
+
case 'resolveSnapshotPath':
|
|
1687
|
+
return {
|
|
1688
|
+
id: request.requestId,
|
|
1689
|
+
method: 'resolveSnapshotPath',
|
|
1690
|
+
args: request.args
|
|
1691
|
+
};
|
|
1692
|
+
case 'readSnapshotFile':
|
|
1693
|
+
return {
|
|
1694
|
+
id: request.requestId,
|
|
1695
|
+
method: 'readSnapshotFile',
|
|
1696
|
+
args: request.args
|
|
1697
|
+
};
|
|
1698
|
+
case 'saveSnapshotFile':
|
|
1699
|
+
return {
|
|
1700
|
+
id: request.requestId,
|
|
1701
|
+
method: 'saveSnapshotFile',
|
|
1702
|
+
args: request.args
|
|
1703
|
+
};
|
|
1704
|
+
case 'removeSnapshotFile':
|
|
1705
|
+
return {
|
|
1706
|
+
id: request.requestId,
|
|
1707
|
+
method: 'removeSnapshotFile',
|
|
1708
|
+
args: request.args
|
|
1709
|
+
};
|
|
1710
|
+
default:
|
|
1711
|
+
return null;
|
|
1712
|
+
}
|
|
1713
|
+
};
|
|
1714
|
+
const createHostDispatchRouter = ({ routerOptions, runnerCallbacks, runSnapshotRpc, extensionHandlers, onDuplicateNamespace })=>{
|
|
1715
|
+
const router = new HostDispatchRouter(routerOptions);
|
|
1716
|
+
router.register('runner', async (request)=>{
|
|
1717
|
+
switch(request.method){
|
|
1718
|
+
case 'file-start':
|
|
1719
|
+
await runnerCallbacks.onTestFileStart(request.args);
|
|
1720
|
+
break;
|
|
1721
|
+
case 'file-ready':
|
|
1722
|
+
await runnerCallbacks.onTestFileReady(request.args);
|
|
1723
|
+
break;
|
|
1724
|
+
case 'suite-start':
|
|
1725
|
+
await runnerCallbacks.onTestSuiteStart(request.args);
|
|
1726
|
+
break;
|
|
1727
|
+
case 'suite-result':
|
|
1728
|
+
await runnerCallbacks.onTestSuiteResult(request.args);
|
|
1729
|
+
break;
|
|
1730
|
+
case 'case-start':
|
|
1731
|
+
await runnerCallbacks.onTestCaseStart(request.args);
|
|
1732
|
+
break;
|
|
1733
|
+
case 'case-result':
|
|
1734
|
+
await runnerCallbacks.onTestCaseResult(request.args);
|
|
1735
|
+
break;
|
|
1736
|
+
case 'file-complete':
|
|
1737
|
+
await runnerCallbacks.onTestFileComplete(request.args);
|
|
1738
|
+
break;
|
|
1739
|
+
case 'log':
|
|
1740
|
+
await runnerCallbacks.onLog(request.args);
|
|
1741
|
+
break;
|
|
1742
|
+
case 'fatal':
|
|
1743
|
+
await runnerCallbacks.onFatal(request.args);
|
|
1744
|
+
break;
|
|
1745
|
+
default:
|
|
1746
|
+
break;
|
|
1747
|
+
}
|
|
1748
|
+
});
|
|
1749
|
+
router.register('snapshot', async (request)=>{
|
|
1750
|
+
const snapshotRequest = toSnapshotRpcRequest(request);
|
|
1751
|
+
if (!snapshotRequest) return;
|
|
1752
|
+
return runSnapshotRpc(snapshotRequest);
|
|
1753
|
+
});
|
|
1754
|
+
for (const [namespace, handler] of extensionHandlers ?? []){
|
|
1755
|
+
if (router.has(namespace)) {
|
|
1756
|
+
onDuplicateNamespace?.(namespace);
|
|
1757
|
+
continue;
|
|
1758
|
+
}
|
|
1759
|
+
router.register(namespace, handler);
|
|
1760
|
+
}
|
|
1761
|
+
return router;
|
|
1762
|
+
};
|
|
1763
|
+
const createHeadlessLatestRerunScheduler = (options)=>{
|
|
1764
|
+
let pendingFiles = null;
|
|
1765
|
+
let draining = null;
|
|
1766
|
+
let latestEnqueueVersion = 0;
|
|
1767
|
+
const runDrainLoop = async ()=>{
|
|
1768
|
+
while(pendingFiles){
|
|
1769
|
+
const nextFiles = pendingFiles;
|
|
1770
|
+
pendingFiles = null;
|
|
1771
|
+
try {
|
|
1772
|
+
await options.runFiles(nextFiles);
|
|
1773
|
+
} catch (error) {
|
|
1774
|
+
try {
|
|
1775
|
+
await options.onError?.(error);
|
|
1776
|
+
} catch {}
|
|
1777
|
+
}
|
|
1778
|
+
}
|
|
1779
|
+
};
|
|
1780
|
+
const ensureDrainLoop = ()=>{
|
|
1781
|
+
if (draining) return;
|
|
1782
|
+
draining = runDrainLoop().finally(()=>{
|
|
1783
|
+
draining = null;
|
|
1784
|
+
});
|
|
1785
|
+
};
|
|
1786
|
+
return {
|
|
1787
|
+
async enqueueLatest (files) {
|
|
1788
|
+
const enqueueVersion = ++latestEnqueueVersion;
|
|
1789
|
+
const activeRun = options.getActiveRun();
|
|
1790
|
+
if (activeRun && !options.isRunCancelled(activeRun)) {
|
|
1791
|
+
options.onInterrupt?.(activeRun);
|
|
1792
|
+
options.invalidateActiveRun();
|
|
1793
|
+
await options.interruptActiveRun(activeRun);
|
|
1794
|
+
}
|
|
1795
|
+
if (enqueueVersion !== latestEnqueueVersion) return;
|
|
1796
|
+
pendingFiles = files;
|
|
1797
|
+
ensureDrainLoop();
|
|
1798
|
+
},
|
|
1799
|
+
async whenIdle () {
|
|
1800
|
+
await draining;
|
|
1801
|
+
}
|
|
1802
|
+
};
|
|
1803
|
+
};
|
|
1804
|
+
const attachHeadlessRunnerTransport = async (page, handlers)=>{
|
|
1805
|
+
await page.exposeFunction(DISPATCH_MESSAGE_TYPE, handlers.onDispatchMessage);
|
|
1806
|
+
await page.exposeFunction(DISPATCH_RPC_BRIDGE_NAME, handlers.onDispatchRpc);
|
|
1807
|
+
};
|
|
1808
|
+
const isRecord = (value)=>'object' == typeof value && null !== value;
|
|
1809
|
+
const readString = (value, key, label)=>{
|
|
1810
|
+
const result = value[key];
|
|
1811
|
+
if ('string' != typeof result) throw new Error(`Invalid browser RPC request: ${label} must be a string`);
|
|
1812
|
+
return result;
|
|
1813
|
+
};
|
|
1814
|
+
const readUnknownArray = (value, key, label)=>{
|
|
1815
|
+
const result = value[key];
|
|
1816
|
+
if (!Array.isArray(result)) throw new Error(`Invalid browser RPC request: ${label} must be an array`);
|
|
1817
|
+
return result;
|
|
1818
|
+
};
|
|
1819
|
+
const parseBrowserLocatorIR = (value, label)=>{
|
|
1820
|
+
if (!isRecord(value)) throw new Error(`Invalid browser RPC request: ${label} must be an object`);
|
|
1821
|
+
const steps = value.steps;
|
|
1822
|
+
if (!Array.isArray(steps)) throw new Error(`Invalid browser RPC request: ${label}.steps must be an array`);
|
|
1823
|
+
return value;
|
|
1824
|
+
};
|
|
1825
|
+
const validateBrowserRpcRequest = (payload)=>{
|
|
1826
|
+
if (!isRecord(payload)) throw new Error('Invalid browser RPC request: payload must be an object');
|
|
1827
|
+
const kind = readString(payload, 'kind', 'kind');
|
|
1828
|
+
if ('locator' !== kind && 'expect' !== kind && 'config' !== kind) throw new Error(`Invalid browser RPC request: unsupported kind ${JSON.stringify(kind)}`);
|
|
1829
|
+
const request = {
|
|
1830
|
+
id: readString(payload, 'id', 'id'),
|
|
1831
|
+
testPath: readString(payload, 'testPath', 'testPath'),
|
|
1832
|
+
runId: readString(payload, 'runId', 'runId'),
|
|
1833
|
+
kind,
|
|
1834
|
+
locator: parseBrowserLocatorIR(payload.locator, 'locator'),
|
|
1835
|
+
method: readString(payload, 'method', 'method'),
|
|
1836
|
+
args: readUnknownArray(payload, 'args', 'args')
|
|
1837
|
+
};
|
|
1838
|
+
const isNot = payload.isNot;
|
|
1839
|
+
if (void 0 !== isNot) {
|
|
1840
|
+
if ('boolean' != typeof isNot) throw new Error('Invalid browser RPC request: isNot must be a boolean');
|
|
1841
|
+
request.isNot = isNot;
|
|
1842
|
+
}
|
|
1843
|
+
const timeout = payload.timeout;
|
|
1844
|
+
if (void 0 !== timeout) {
|
|
1845
|
+
if ('number' != typeof timeout) throw new Error('Invalid browser RPC request: timeout must be a number');
|
|
1846
|
+
request.timeout = timeout;
|
|
1847
|
+
}
|
|
1848
|
+
return request;
|
|
1849
|
+
};
|
|
1850
|
+
const supportedLocatorActions = new Set([
|
|
1851
|
+
'click',
|
|
1852
|
+
'dblclick',
|
|
1853
|
+
'fill',
|
|
1854
|
+
'hover',
|
|
1855
|
+
'press',
|
|
1856
|
+
'clear',
|
|
1857
|
+
'check',
|
|
1858
|
+
'uncheck',
|
|
1859
|
+
'focus',
|
|
1860
|
+
'blur',
|
|
1861
|
+
'scrollIntoViewIfNeeded',
|
|
1862
|
+
'waitFor',
|
|
1863
|
+
'dispatchEvent',
|
|
1864
|
+
'selectOption',
|
|
1865
|
+
'setInputFiles'
|
|
1866
|
+
]);
|
|
1867
|
+
const supportedExpectElementMatchers = new Set([
|
|
1868
|
+
'toBeVisible',
|
|
1869
|
+
'toBeHidden',
|
|
1870
|
+
'toBeEnabled',
|
|
1871
|
+
'toBeDisabled',
|
|
1872
|
+
'toBeAttached',
|
|
1873
|
+
'toBeDetached',
|
|
1874
|
+
'toBeEditable',
|
|
1875
|
+
'toBeFocused',
|
|
1876
|
+
'toBeEmpty',
|
|
1877
|
+
'toBeInViewport',
|
|
1878
|
+
'toHaveText',
|
|
1879
|
+
'toContainText',
|
|
1880
|
+
'toHaveValue',
|
|
1881
|
+
'toHaveAttribute',
|
|
1882
|
+
'toHaveClass',
|
|
1883
|
+
'toHaveCount',
|
|
1884
|
+
'toBeChecked',
|
|
1885
|
+
'toBeUnchecked',
|
|
1886
|
+
'toHaveId',
|
|
1887
|
+
'toHaveCSS',
|
|
1888
|
+
'toHaveJSProperty'
|
|
1889
|
+
]);
|
|
1890
|
+
const reviveBrowserLocatorText = (text)=>{
|
|
1891
|
+
if ('string' === text.type) return text.value;
|
|
1892
|
+
return new RegExp(text.source, text.flags);
|
|
1893
|
+
};
|
|
1894
|
+
const compilePlaywrightLocator = (frame, locatorIR)=>{
|
|
1895
|
+
const compileFromFrame = (ir)=>{
|
|
1896
|
+
let current = frame;
|
|
1897
|
+
const ensureLocator = ()=>{
|
|
1898
|
+
if (current.filter) return current;
|
|
1899
|
+
current = current.locator(':root');
|
|
1900
|
+
return current;
|
|
1901
|
+
};
|
|
1902
|
+
for (const step of ir.steps)switch(step.type){
|
|
1903
|
+
case 'getByRole':
|
|
1904
|
+
{
|
|
1905
|
+
const name = step.options?.name ? reviveBrowserLocatorText(step.options.name) : void 0;
|
|
1906
|
+
const options = step.options ? {
|
|
1907
|
+
...step.options,
|
|
1908
|
+
name
|
|
1909
|
+
} : void 0;
|
|
1910
|
+
current = current.getByRole(step.role, options);
|
|
1911
|
+
break;
|
|
1912
|
+
}
|
|
1913
|
+
case 'locator':
|
|
1914
|
+
current = current.locator(step.selector);
|
|
1915
|
+
break;
|
|
1916
|
+
case 'getByText':
|
|
1917
|
+
current = current.getByText(reviveBrowserLocatorText(step.text), step.options);
|
|
1918
|
+
break;
|
|
1919
|
+
case 'getByLabel':
|
|
1920
|
+
current = current.getByLabel(reviveBrowserLocatorText(step.text), step.options);
|
|
1921
|
+
break;
|
|
1922
|
+
case 'getByPlaceholder':
|
|
1923
|
+
current = current.getByPlaceholder(reviveBrowserLocatorText(step.text), step.options);
|
|
1924
|
+
break;
|
|
1925
|
+
case 'getByAltText':
|
|
1926
|
+
current = current.getByAltText(reviveBrowserLocatorText(step.text), step.options);
|
|
1927
|
+
break;
|
|
1928
|
+
case 'getByTitle':
|
|
1929
|
+
current = current.getByTitle(reviveBrowserLocatorText(step.text), step.options);
|
|
1930
|
+
break;
|
|
1931
|
+
case 'getByTestId':
|
|
1932
|
+
current = current.getByTestId(reviveBrowserLocatorText(step.text));
|
|
1933
|
+
break;
|
|
1934
|
+
case 'filter':
|
|
1935
|
+
{
|
|
1936
|
+
const locator = ensureLocator();
|
|
1937
|
+
const options = {};
|
|
1938
|
+
if (step.options?.hasText) options.hasText = reviveBrowserLocatorText(step.options.hasText);
|
|
1939
|
+
if (step.options?.hasNotText) options.hasNotText = reviveBrowserLocatorText(step.options.hasNotText);
|
|
1940
|
+
if (step.options?.has) options.has = compileFromFrame(step.options.has);
|
|
1941
|
+
if (step.options?.hasNot) options.hasNot = compileFromFrame(step.options.hasNot);
|
|
1942
|
+
current = locator.filter(options);
|
|
1943
|
+
break;
|
|
1944
|
+
}
|
|
1945
|
+
case 'and':
|
|
1946
|
+
{
|
|
1947
|
+
const locator = ensureLocator();
|
|
1948
|
+
const other = compileFromFrame(step.locator);
|
|
1949
|
+
current = locator.and(other);
|
|
1950
|
+
break;
|
|
1951
|
+
}
|
|
1952
|
+
case 'or':
|
|
1953
|
+
{
|
|
1954
|
+
const locator = ensureLocator();
|
|
1955
|
+
const other = compileFromFrame(step.locator);
|
|
1956
|
+
current = locator.or(other);
|
|
1957
|
+
break;
|
|
1958
|
+
}
|
|
1959
|
+
case 'nth':
|
|
1960
|
+
{
|
|
1961
|
+
const locator = ensureLocator();
|
|
1962
|
+
current = locator.nth(step.index);
|
|
1963
|
+
break;
|
|
1964
|
+
}
|
|
1965
|
+
case 'first':
|
|
1966
|
+
{
|
|
1967
|
+
const locator = ensureLocator();
|
|
1968
|
+
current = locator.first();
|
|
1969
|
+
break;
|
|
1970
|
+
}
|
|
1971
|
+
case 'last':
|
|
1972
|
+
{
|
|
1973
|
+
const locator = ensureLocator();
|
|
1974
|
+
current = locator.last();
|
|
1975
|
+
break;
|
|
1976
|
+
}
|
|
1977
|
+
default:
|
|
1978
|
+
throw new Error(`Unknown locator step: ${String(step?.type)}`);
|
|
1979
|
+
}
|
|
1980
|
+
return ensureLocator();
|
|
1981
|
+
};
|
|
1982
|
+
return compileFromFrame(locatorIR);
|
|
1983
|
+
};
|
|
1984
|
+
const serializeExpectedText = (text, options)=>{
|
|
1985
|
+
const base = {
|
|
1986
|
+
matchSubstring: options?.matchSubstring,
|
|
1987
|
+
ignoreCase: options?.ignoreCase,
|
|
1988
|
+
normalizeWhiteSpace: options?.normalizeWhiteSpace
|
|
1989
|
+
};
|
|
1990
|
+
if ('string' === text.type) return [
|
|
1991
|
+
{
|
|
1992
|
+
...base,
|
|
1993
|
+
string: text.value
|
|
1994
|
+
}
|
|
1995
|
+
];
|
|
1996
|
+
return [
|
|
1997
|
+
{
|
|
1998
|
+
...base,
|
|
1999
|
+
regexSource: text.source,
|
|
2000
|
+
regexFlags: text.flags
|
|
2001
|
+
}
|
|
2002
|
+
];
|
|
2003
|
+
};
|
|
2004
|
+
const formatExpectError = (result)=>{
|
|
2005
|
+
const parts = [];
|
|
2006
|
+
if (result.errorMessage) parts.push(result.errorMessage);
|
|
2007
|
+
if (result.log?.length) {
|
|
2008
|
+
parts.push('Call log:');
|
|
2009
|
+
parts.push(...result.log.map((l)=>`- ${l}`));
|
|
2010
|
+
}
|
|
2011
|
+
return parts.join('\n');
|
|
2012
|
+
};
|
|
2013
|
+
const escapeCssAttrValue = (value)=>value.replace(/\\/g, '\\\\').replace(/'/g, "\\'");
|
|
2014
|
+
const getRunnerFrame = async (containerPage, testPath, timeoutMs)=>{
|
|
2015
|
+
const selector = `iframe[data-test-file='${escapeCssAttrValue(testPath)}']`;
|
|
2016
|
+
const iframe = containerPage.locator(selector);
|
|
2017
|
+
const count = await iframe.count();
|
|
2018
|
+
if (0 === count) {
|
|
2019
|
+
const known = await containerPage.locator('iframe[data-test-file]').evaluateAll((nodes)=>nodes.map((n)=>n.dataset.testFile));
|
|
2020
|
+
throw new Error(`Runner iframe not found for testPath: ${JSON.stringify(testPath)}. Known iframes: ${JSON.stringify(known)}. Timeout: ${timeoutMs}ms`);
|
|
2021
|
+
}
|
|
2022
|
+
return containerPage.frameLocator(selector);
|
|
2023
|
+
};
|
|
2024
|
+
const callExpect = async (locator, expectMethod, options, fallbackMessage)=>{
|
|
2025
|
+
const result = await locator._expect(expectMethod, options);
|
|
2026
|
+
if (!options.isNot !== result.matches) throw new Error(formatExpectError(result) || fallbackMessage);
|
|
2027
|
+
return null;
|
|
2028
|
+
};
|
|
2029
|
+
const assertSerializedText = (value, matcherName)=>{
|
|
2030
|
+
const t = value;
|
|
2031
|
+
if (!t || 'string' !== t.type && 'regexp' !== t.type) throw new Error(`${matcherName} expects a serialized text matcher`);
|
|
2032
|
+
return t;
|
|
2033
|
+
};
|
|
2034
|
+
const assertStringArg = (value, matcherName, label)=>{
|
|
2035
|
+
if ('string' != typeof value || !value) throw new Error(`${matcherName} expects ${label}`);
|
|
2036
|
+
return value;
|
|
2037
|
+
};
|
|
2038
|
+
const simpleMatchers = {
|
|
2039
|
+
toBeVisible: 'to.be.visible',
|
|
2040
|
+
toBeHidden: 'to.be.hidden',
|
|
2041
|
+
toBeEnabled: 'to.be.enabled',
|
|
2042
|
+
toBeDisabled: 'to.be.disabled',
|
|
2043
|
+
toBeAttached: 'to.be.attached',
|
|
2044
|
+
toBeDetached: 'to.be.detached',
|
|
2045
|
+
toBeEditable: 'to.be.editable',
|
|
2046
|
+
toBeFocused: 'to.be.focused',
|
|
2047
|
+
toBeEmpty: 'to.be.empty'
|
|
2048
|
+
};
|
|
2049
|
+
const textMatchers = {
|
|
2050
|
+
toHaveId: {
|
|
2051
|
+
expectMethod: 'to.have.id'
|
|
2052
|
+
},
|
|
2053
|
+
toHaveText: {
|
|
2054
|
+
expectMethod: 'to.have.text',
|
|
2055
|
+
textOptions: {
|
|
2056
|
+
normalizeWhiteSpace: true
|
|
2057
|
+
}
|
|
2058
|
+
},
|
|
2059
|
+
toContainText: {
|
|
2060
|
+
expectMethod: 'to.have.text',
|
|
2061
|
+
textOptions: {
|
|
2062
|
+
matchSubstring: true,
|
|
2063
|
+
normalizeWhiteSpace: true
|
|
2064
|
+
}
|
|
2065
|
+
},
|
|
2066
|
+
toHaveValue: {
|
|
2067
|
+
expectMethod: 'to.have.value'
|
|
2068
|
+
},
|
|
2069
|
+
toHaveClass: {
|
|
2070
|
+
expectMethod: 'to.have.class'
|
|
2071
|
+
}
|
|
2072
|
+
};
|
|
2073
|
+
const dispatchExpectMatcher = (locator, request, isNot, timeout)=>{
|
|
2074
|
+
const { method, args } = request;
|
|
2075
|
+
const simpleExpect = simpleMatchers[method];
|
|
2076
|
+
if (simpleExpect) return callExpect(locator, simpleExpect, {
|
|
2077
|
+
isNot,
|
|
2078
|
+
timeout
|
|
2079
|
+
}, `Expected element ${method.replace('toBe', 'to be ').replace(/([A-Z])/g, ' $1').trim().toLowerCase()}`);
|
|
2080
|
+
const textDef = textMatchers[method];
|
|
2081
|
+
if (textDef) {
|
|
2082
|
+
const expected = assertSerializedText(args[0], method);
|
|
2083
|
+
return callExpect(locator, textDef.expectMethod, {
|
|
2084
|
+
isNot,
|
|
2085
|
+
timeout,
|
|
2086
|
+
expectedText: serializeExpectedText(expected, textDef.textOptions)
|
|
2087
|
+
}, `Expected element ${method}`);
|
|
2088
|
+
}
|
|
2089
|
+
switch(method){
|
|
2090
|
+
case 'toBeInViewport':
|
|
2091
|
+
{
|
|
2092
|
+
const ratio = args[0];
|
|
2093
|
+
if (void 0 !== ratio && 'number' != typeof ratio) throw new Error(`toBeInViewport expects ratio to be a number, got ${typeof ratio}`);
|
|
2094
|
+
return callExpect(locator, 'to.be.in.viewport', {
|
|
2095
|
+
isNot,
|
|
2096
|
+
timeout,
|
|
2097
|
+
expectedNumber: ratio
|
|
2098
|
+
}, 'Expected element to be in viewport');
|
|
2099
|
+
}
|
|
2100
|
+
case 'toBeChecked':
|
|
2101
|
+
return callExpect(locator, 'to.be.checked', {
|
|
2102
|
+
isNot,
|
|
2103
|
+
timeout,
|
|
2104
|
+
expectedValue: {
|
|
2105
|
+
checked: true
|
|
2106
|
+
}
|
|
2107
|
+
}, 'Expected element to be checked');
|
|
2108
|
+
case 'toBeUnchecked':
|
|
2109
|
+
return callExpect(locator, 'to.be.checked', {
|
|
2110
|
+
isNot,
|
|
2111
|
+
timeout,
|
|
2112
|
+
expectedValue: {
|
|
2113
|
+
checked: false
|
|
2114
|
+
}
|
|
2115
|
+
}, 'Expected element to be unchecked');
|
|
2116
|
+
case 'toHaveCount':
|
|
2117
|
+
{
|
|
2118
|
+
const expected = args[0];
|
|
2119
|
+
if ('number' != typeof expected) throw new Error(`toHaveCount expects a number, got ${typeof expected}`);
|
|
2120
|
+
return callExpect(locator, 'to.have.count', {
|
|
2121
|
+
isNot,
|
|
2122
|
+
timeout,
|
|
2123
|
+
expectedNumber: expected
|
|
2124
|
+
}, `Expected count ${expected}`);
|
|
2125
|
+
}
|
|
2126
|
+
case 'toHaveAttribute':
|
|
2127
|
+
{
|
|
2128
|
+
const name = assertStringArg(args[0], 'toHaveAttribute', 'an attribute name');
|
|
2129
|
+
if (args.length < 2) return callExpect(locator, 'to.have.attribute', {
|
|
2130
|
+
isNot,
|
|
2131
|
+
timeout,
|
|
2132
|
+
expressionArg: name
|
|
2133
|
+
}, `Expected attribute ${name} to be present`);
|
|
2134
|
+
const expected = assertSerializedText(args[1], 'toHaveAttribute');
|
|
2135
|
+
return callExpect(locator, 'to.have.attribute.value', {
|
|
2136
|
+
isNot,
|
|
2137
|
+
timeout,
|
|
2138
|
+
expressionArg: name,
|
|
2139
|
+
expectedText: serializeExpectedText(expected)
|
|
2140
|
+
}, `Expected attribute ${name} to match`);
|
|
2141
|
+
}
|
|
2142
|
+
case 'toHaveCSS':
|
|
2143
|
+
{
|
|
2144
|
+
const name = assertStringArg(args[0], 'toHaveCSS', 'a CSS property name');
|
|
2145
|
+
const expected = assertSerializedText(args[1], 'toHaveCSS');
|
|
2146
|
+
return callExpect(locator, 'to.have.css', {
|
|
2147
|
+
isNot,
|
|
2148
|
+
timeout,
|
|
2149
|
+
expressionArg: name,
|
|
2150
|
+
expectedText: serializeExpectedText(expected)
|
|
2151
|
+
}, `Expected CSS ${name} to match`);
|
|
2152
|
+
}
|
|
2153
|
+
case 'toHaveJSProperty':
|
|
2154
|
+
{
|
|
2155
|
+
const name = assertStringArg(args[0], 'toHaveJSProperty', 'a property name');
|
|
2156
|
+
const expectedValue = args[1];
|
|
2157
|
+
try {
|
|
2158
|
+
JSON.stringify(expectedValue);
|
|
2159
|
+
} catch {
|
|
2160
|
+
throw new Error('toHaveJSProperty expects a JSON-serializable expected value');
|
|
2161
|
+
}
|
|
2162
|
+
return callExpect(locator, 'to.have.property', {
|
|
2163
|
+
isNot,
|
|
2164
|
+
timeout,
|
|
2165
|
+
expressionArg: name,
|
|
2166
|
+
expectedValue
|
|
2167
|
+
}, `Expected JS property ${name} to match`);
|
|
2168
|
+
}
|
|
2169
|
+
}
|
|
2170
|
+
throw new Error(`Unhandled expect matcher: ${method}`);
|
|
2171
|
+
};
|
|
2172
|
+
const dispatchConfigMethod = async (request)=>{
|
|
2173
|
+
switch(request.method){
|
|
2174
|
+
case 'setTestIdAttribute':
|
|
2175
|
+
{
|
|
2176
|
+
const attr = request.args[0];
|
|
2177
|
+
if ('string' != typeof attr || !attr) throw new Error('setTestIdAttribute expects a non-empty string argument');
|
|
2178
|
+
const playwright = await import("playwright");
|
|
2179
|
+
playwright.selectors.setTestIdAttribute(attr);
|
|
2180
|
+
return null;
|
|
2181
|
+
}
|
|
2182
|
+
default:
|
|
2183
|
+
throw new Error(`Unknown config method: ${request.method}`);
|
|
2184
|
+
}
|
|
2185
|
+
};
|
|
2186
|
+
async function dispatchPlaywrightBrowserRpc({ containerPage, runnerPage, request, timeoutFallbackMs }) {
|
|
2187
|
+
if ('config' === request.kind) return dispatchConfigMethod(request);
|
|
2188
|
+
const testPath = request.testPath;
|
|
2189
|
+
if (!testPath) throw new Error('Browser RPC request is missing testPath');
|
|
2190
|
+
const timeout = 'number' == typeof request.timeout ? request.timeout : timeoutFallbackMs;
|
|
2191
|
+
const locatorRoot = runnerPage ? runnerPage : await getRunnerFrame(containerPage ?? (()=>{
|
|
2192
|
+
throw new Error('Browser container page is not initialized');
|
|
2193
|
+
})(), testPath, timeout);
|
|
2194
|
+
const locator = compilePlaywrightLocator(locatorRoot, request.locator);
|
|
2195
|
+
if ('locator' === request.kind) {
|
|
2196
|
+
if (!supportedLocatorActions.has(request.method)) throw new Error(`Locator method not supported: ${request.method}`);
|
|
2197
|
+
const target = locator;
|
|
2198
|
+
return await target[request.method](...request.args);
|
|
2199
|
+
}
|
|
2200
|
+
if ('expect' === request.kind) {
|
|
2201
|
+
if (!supportedExpectElementMatchers.has(request.method)) throw new Error(`Expect matcher not supported: ${request.method}`);
|
|
2202
|
+
return dispatchExpectMatcher(locator, request, !!request.isNot, timeout);
|
|
2203
|
+
}
|
|
2204
|
+
throw new Error(`Unknown browser rpc kind: ${request.kind}`);
|
|
2205
|
+
}
|
|
2206
|
+
async function launchPlaywrightBrowser({ browserName, headless }) {
|
|
2207
|
+
const playwright = await import("playwright");
|
|
2208
|
+
const browserType = playwright[browserName];
|
|
2209
|
+
const browser = await browserType.launch({
|
|
2210
|
+
headless,
|
|
2211
|
+
args: 'chromium' === browserName ? [
|
|
2212
|
+
'--disable-popup-blocking',
|
|
2213
|
+
'--no-first-run',
|
|
2214
|
+
'--no-default-browser-check'
|
|
2215
|
+
] : void 0
|
|
2216
|
+
});
|
|
2217
|
+
return {
|
|
2218
|
+
browser: browser
|
|
2219
|
+
};
|
|
2220
|
+
}
|
|
2221
|
+
const playwrightProviderImplementation = {
|
|
2222
|
+
name: 'playwright',
|
|
2223
|
+
async launchRuntime ({ browserName, headless }) {
|
|
2224
|
+
return launchPlaywrightBrowser({
|
|
2225
|
+
browserName,
|
|
2226
|
+
headless
|
|
2227
|
+
});
|
|
2228
|
+
},
|
|
2229
|
+
async dispatchRpc ({ containerPage, runnerPage, request, timeoutFallbackMs }) {
|
|
2230
|
+
return dispatchPlaywrightBrowserRpc({
|
|
2231
|
+
containerPage: containerPage,
|
|
2232
|
+
runnerPage: runnerPage,
|
|
2233
|
+
request,
|
|
2234
|
+
timeoutFallbackMs
|
|
2235
|
+
});
|
|
2236
|
+
}
|
|
2237
|
+
};
|
|
2238
|
+
const providerImplementations = {
|
|
2239
|
+
playwright: playwrightProviderImplementation
|
|
2240
|
+
};
|
|
2241
|
+
function getBrowserProviderImplementation(provider) {
|
|
2242
|
+
const implementation = providerImplementations[provider];
|
|
2243
|
+
if (!implementation) throw new Error(`Unsupported browser provider: ${String(provider)}`);
|
|
2244
|
+
return implementation;
|
|
2245
|
+
}
|
|
2246
|
+
const createCancelSignal = ()=>{
|
|
2247
|
+
let settled = false;
|
|
2248
|
+
let resolveSignal = ()=>{};
|
|
2249
|
+
const signal = new Promise((resolve)=>{
|
|
2250
|
+
resolveSignal = ()=>{
|
|
2251
|
+
if (!settled) {
|
|
2252
|
+
settled = true;
|
|
2253
|
+
resolve();
|
|
2254
|
+
}
|
|
2255
|
+
};
|
|
2256
|
+
});
|
|
2257
|
+
return {
|
|
2258
|
+
signal,
|
|
2259
|
+
resolve: resolveSignal
|
|
2260
|
+
};
|
|
2261
|
+
};
|
|
2262
|
+
const createRunSession = (token)=>{
|
|
2263
|
+
const { signal, resolve } = createCancelSignal();
|
|
2264
|
+
return {
|
|
2265
|
+
token,
|
|
2266
|
+
cancelled: false,
|
|
2267
|
+
cancelSignal: signal,
|
|
2268
|
+
signalCancel: resolve
|
|
2269
|
+
};
|
|
2270
|
+
};
|
|
2271
|
+
class RunSessionLifecycle {
|
|
2272
|
+
currentToken = 0;
|
|
2273
|
+
active = null;
|
|
2274
|
+
get activeSession() {
|
|
2275
|
+
return this.active;
|
|
2276
|
+
}
|
|
2277
|
+
get activeToken() {
|
|
2278
|
+
return this.currentToken;
|
|
2279
|
+
}
|
|
2280
|
+
createSession(factory) {
|
|
2281
|
+
const session = factory(++this.currentToken);
|
|
2282
|
+
this.active = session;
|
|
2283
|
+
return session;
|
|
2284
|
+
}
|
|
2285
|
+
isTokenActive(token) {
|
|
2286
|
+
return token === this.currentToken;
|
|
2287
|
+
}
|
|
2288
|
+
isTokenStale(token) {
|
|
2289
|
+
return !this.isTokenActive(token);
|
|
2290
|
+
}
|
|
2291
|
+
invalidateActiveToken() {
|
|
2292
|
+
this.currentToken += 1;
|
|
2293
|
+
return this.currentToken;
|
|
2294
|
+
}
|
|
2295
|
+
clearIfActive(session) {
|
|
2296
|
+
if (this.active === session) this.active = null;
|
|
2297
|
+
}
|
|
2298
|
+
async cancel(session, options) {
|
|
2299
|
+
const waitForDone = options?.waitForDone ?? true;
|
|
2300
|
+
if (!session.cancelled) {
|
|
2301
|
+
session.cancelled = true;
|
|
2302
|
+
session.signalCancel();
|
|
2303
|
+
await options?.onCancel?.(session);
|
|
2304
|
+
}
|
|
2305
|
+
if (waitForDone) await session.done?.catch(()=>{});
|
|
2306
|
+
}
|
|
2307
|
+
async cancelActive(options) {
|
|
2308
|
+
if (!this.active) return;
|
|
2309
|
+
await this.cancel(this.active, options);
|
|
2310
|
+
}
|
|
2311
|
+
}
|
|
2312
|
+
class RunnerSessionRegistry {
|
|
2313
|
+
nextId = 0;
|
|
2314
|
+
sessionsById = new Map();
|
|
2315
|
+
sessionIdByTestFile = new Map();
|
|
2316
|
+
register(input) {
|
|
2317
|
+
const id = input.id ?? `runner-session-${++this.nextId}`;
|
|
2318
|
+
const createdAt = input.createdAt ?? Date.now();
|
|
2319
|
+
const record = {
|
|
2320
|
+
...input,
|
|
2321
|
+
id,
|
|
2322
|
+
createdAt
|
|
2323
|
+
};
|
|
2324
|
+
this.sessionsById.set(id, record);
|
|
2325
|
+
this.sessionIdByTestFile.set(record.testFile, id);
|
|
2326
|
+
return record;
|
|
2327
|
+
}
|
|
2328
|
+
getById(id) {
|
|
2329
|
+
return this.sessionsById.get(id);
|
|
2330
|
+
}
|
|
2331
|
+
getByTestFile(testFile) {
|
|
2332
|
+
const id = this.sessionIdByTestFile.get(testFile);
|
|
2333
|
+
if (!id) return;
|
|
2334
|
+
return this.sessionsById.get(id);
|
|
2335
|
+
}
|
|
2336
|
+
list() {
|
|
2337
|
+
return Array.from(this.sessionsById.values());
|
|
2338
|
+
}
|
|
2339
|
+
listByRunToken(runToken) {
|
|
2340
|
+
return this.list().filter((session)=>session.runToken === runToken);
|
|
2341
|
+
}
|
|
2342
|
+
deleteById(id) {
|
|
2343
|
+
const record = this.sessionsById.get(id);
|
|
2344
|
+
if (!record) return false;
|
|
2345
|
+
this.sessionsById.delete(id);
|
|
2346
|
+
if (this.sessionIdByTestFile.get(record.testFile) === id) this.sessionIdByTestFile.delete(record.testFile);
|
|
2347
|
+
return true;
|
|
2348
|
+
}
|
|
2349
|
+
deleteByTestFile(testFile) {
|
|
2350
|
+
const id = this.sessionIdByTestFile.get(testFile);
|
|
2351
|
+
if (!id) return false;
|
|
2352
|
+
return this.deleteById(id);
|
|
2353
|
+
}
|
|
2354
|
+
clear() {
|
|
2355
|
+
this.sessionsById.clear();
|
|
2356
|
+
this.sessionIdByTestFile.clear();
|
|
2357
|
+
}
|
|
2358
|
+
}
|
|
2359
|
+
const normalizeJavaScriptUrl = (value, options)=>{
|
|
2360
|
+
try {
|
|
2361
|
+
const url = options?.origin ? new URL(value, options.origin) : new URL(value);
|
|
2362
|
+
if ('http:' !== url.protocol && 'https:' !== url.protocol) return null;
|
|
2363
|
+
url.search = '';
|
|
2364
|
+
url.hash = '';
|
|
2365
|
+
return url.toString();
|
|
2366
|
+
} catch {
|
|
2367
|
+
return null;
|
|
2368
|
+
}
|
|
2369
|
+
};
|
|
2370
|
+
const resolveInlineSourceMap = (code)=>{
|
|
2371
|
+
const converter = convert_source_map.fromSource(code);
|
|
2372
|
+
if (!converter) return null;
|
|
2373
|
+
return converter.toObject();
|
|
2374
|
+
};
|
|
2375
|
+
const fetchSourceMap = async (jsUrl, fetcher)=>{
|
|
2376
|
+
const jsResponse = await fetcher(jsUrl);
|
|
2377
|
+
if (!jsResponse.ok) return null;
|
|
2378
|
+
const code = await jsResponse.text();
|
|
2379
|
+
const inlineMap = resolveInlineSourceMap(code);
|
|
2380
|
+
if (inlineMap) return inlineMap;
|
|
2381
|
+
const mapResponse = await fetcher(`${jsUrl}.map`);
|
|
2382
|
+
if (!mapResponse.ok) return null;
|
|
2383
|
+
return await mapResponse.json();
|
|
2384
|
+
};
|
|
2385
|
+
const loadSourceMapWithCache = async ({ jsUrl, cache, force = false, origin, fetcher = fetch })=>{
|
|
2386
|
+
const normalizedUrl = normalizeJavaScriptUrl(jsUrl, {
|
|
2387
|
+
origin
|
|
2388
|
+
});
|
|
2389
|
+
if (!normalizedUrl) return null;
|
|
2390
|
+
if (!force && cache.has(normalizedUrl)) return cache.get(normalizedUrl) ?? null;
|
|
2391
|
+
try {
|
|
2392
|
+
const sourceMap = await fetchSourceMap(normalizedUrl, fetcher);
|
|
2393
|
+
cache.set(normalizedUrl, sourceMap);
|
|
2394
|
+
return sourceMap;
|
|
2395
|
+
} catch {
|
|
2396
|
+
cache.set(normalizedUrl, null);
|
|
2397
|
+
return null;
|
|
2398
|
+
}
|
|
2399
|
+
};
|
|
2400
|
+
const BROWSER_VIEWPORT_PRESET_IDS = [
|
|
2401
|
+
'iPhoneSE',
|
|
2402
|
+
'iPhoneXR',
|
|
2403
|
+
'iPhone12Pro',
|
|
2404
|
+
'iPhone14ProMax',
|
|
2405
|
+
'Pixel7',
|
|
2406
|
+
'SamsungGalaxyS8Plus',
|
|
2407
|
+
'SamsungGalaxyS20Ultra',
|
|
2408
|
+
'iPadMini',
|
|
2409
|
+
'iPadAir',
|
|
2410
|
+
'iPadPro',
|
|
2411
|
+
'SurfacePro7',
|
|
2412
|
+
'SurfaceDuo',
|
|
2413
|
+
'GalaxyZFold5',
|
|
2414
|
+
'AsusZenbookFold',
|
|
2415
|
+
'SamsungGalaxyA51A71',
|
|
2416
|
+
'NestHub',
|
|
2417
|
+
'NestHubMax'
|
|
2418
|
+
];
|
|
2419
|
+
const BROWSER_VIEWPORT_PRESET_DIMENSIONS = {
|
|
2420
|
+
iPhoneSE: {
|
|
2421
|
+
width: 375,
|
|
2422
|
+
height: 667
|
|
2423
|
+
},
|
|
2424
|
+
iPhoneXR: {
|
|
2425
|
+
width: 414,
|
|
2426
|
+
height: 896
|
|
2427
|
+
},
|
|
2428
|
+
iPhone12Pro: {
|
|
2429
|
+
width: 390,
|
|
2430
|
+
height: 844
|
|
2431
|
+
},
|
|
2432
|
+
iPhone14ProMax: {
|
|
2433
|
+
width: 430,
|
|
2434
|
+
height: 932
|
|
2435
|
+
},
|
|
2436
|
+
Pixel7: {
|
|
2437
|
+
width: 412,
|
|
2438
|
+
height: 915
|
|
2439
|
+
},
|
|
2440
|
+
SamsungGalaxyS8Plus: {
|
|
2441
|
+
width: 360,
|
|
2442
|
+
height: 740
|
|
2443
|
+
},
|
|
2444
|
+
SamsungGalaxyS20Ultra: {
|
|
2445
|
+
width: 412,
|
|
2446
|
+
height: 915
|
|
2447
|
+
},
|
|
2448
|
+
iPadMini: {
|
|
2449
|
+
width: 768,
|
|
2450
|
+
height: 1024
|
|
2451
|
+
},
|
|
2452
|
+
iPadAir: {
|
|
2453
|
+
width: 820,
|
|
2454
|
+
height: 1180
|
|
2455
|
+
},
|
|
2456
|
+
iPadPro: {
|
|
2457
|
+
width: 1024,
|
|
2458
|
+
height: 1366
|
|
2459
|
+
},
|
|
2460
|
+
SurfacePro7: {
|
|
2461
|
+
width: 912,
|
|
2462
|
+
height: 1368
|
|
2463
|
+
},
|
|
2464
|
+
SurfaceDuo: {
|
|
2465
|
+
width: 540,
|
|
2466
|
+
height: 720
|
|
2467
|
+
},
|
|
2468
|
+
GalaxyZFold5: {
|
|
2469
|
+
width: 344,
|
|
2470
|
+
height: 882
|
|
2471
|
+
},
|
|
2472
|
+
AsusZenbookFold: {
|
|
2473
|
+
width: 853,
|
|
2474
|
+
height: 1280
|
|
2475
|
+
},
|
|
2476
|
+
SamsungGalaxyA51A71: {
|
|
2477
|
+
width: 412,
|
|
2478
|
+
height: 914
|
|
2479
|
+
},
|
|
2480
|
+
NestHub: {
|
|
2481
|
+
width: 1024,
|
|
2482
|
+
height: 600
|
|
2483
|
+
},
|
|
2484
|
+
NestHubMax: {
|
|
2485
|
+
width: 1280,
|
|
2486
|
+
height: 800
|
|
2487
|
+
}
|
|
2488
|
+
};
|
|
2489
|
+
const resolveBrowserViewportPreset = (presetId)=>{
|
|
2490
|
+
const size = BROWSER_VIEWPORT_PRESET_DIMENSIONS[presetId];
|
|
2491
|
+
return size ?? null;
|
|
2492
|
+
};
|
|
2493
|
+
const serializeTestFiles = (files)=>JSON.stringify(files.map((f)=>`${f.projectName}:${f.testPath}`).sort());
|
|
2494
|
+
const normalizeTestFiles = (files)=>files.map((file)=>({
|
|
2495
|
+
...file,
|
|
2496
|
+
testPath: normalize(file.testPath)
|
|
2497
|
+
}));
|
|
2498
|
+
const collectWatchTestFiles = (projectEntries)=>projectEntries.flatMap((entry)=>entry.testFiles.map((testPath)=>({
|
|
2499
|
+
testPath: normalize(testPath),
|
|
2500
|
+
projectName: entry.project.name
|
|
2501
|
+
})));
|
|
2502
|
+
const planWatchRerun = ({ projectEntries, previousTestFiles, affectedTestFiles })=>{
|
|
2503
|
+
const currentTestFiles = collectWatchTestFiles(projectEntries);
|
|
2504
|
+
const normalizedPrevious = normalizeTestFiles(previousTestFiles);
|
|
2505
|
+
const filesChanged = serializeTestFiles(currentTestFiles) !== serializeTestFiles(normalizedPrevious);
|
|
2506
|
+
const normalizedAffectedTestFiles = affectedTestFiles.map((testFile)=>normalize(testFile));
|
|
2507
|
+
const currentFileMap = new Map(currentTestFiles.map((file)=>[
|
|
2508
|
+
file.testPath,
|
|
2509
|
+
file
|
|
2510
|
+
]));
|
|
2511
|
+
const matchedAffectedFiles = normalizedAffectedTestFiles.map((testFile)=>currentFileMap.get(testFile)).filter((file)=>Boolean(file));
|
|
2512
|
+
return {
|
|
2513
|
+
currentTestFiles,
|
|
2514
|
+
filesChanged,
|
|
2515
|
+
normalizedAffectedTestFiles,
|
|
2516
|
+
affectedTestFiles: matchedAffectedFiles
|
|
2517
|
+
};
|
|
2518
|
+
};
|
|
1613
2519
|
const picomatch = __webpack_require__("../../node_modules/.pnpm/picomatch@4.0.3/node_modules/picomatch/index.js");
|
|
1614
2520
|
const { createRsbuild: createRsbuild, rspack: rspack } = rsbuild;
|
|
1615
2521
|
const hostController_dirname = dirname(fileURLToPath(import.meta.url));
|
|
@@ -1685,6 +2591,23 @@ const watchContext = {
|
|
|
1685
2591
|
chunkHashes: new Map(),
|
|
1686
2592
|
affectedTestFiles: []
|
|
1687
2593
|
};
|
|
2594
|
+
const resolveViewport = (viewport)=>{
|
|
2595
|
+
if (!viewport) return null;
|
|
2596
|
+
if ('string' == typeof viewport) return resolveBrowserViewportPreset(viewport);
|
|
2597
|
+
if ('number' == typeof viewport.width && Number.isFinite(viewport.width) && viewport.width > 0 && 'number' == typeof viewport.height && Number.isFinite(viewport.height) && viewport.height > 0) return {
|
|
2598
|
+
width: viewport.width,
|
|
2599
|
+
height: viewport.height
|
|
2600
|
+
};
|
|
2601
|
+
return null;
|
|
2602
|
+
};
|
|
2603
|
+
const mapViewportByProject = (projects)=>{
|
|
2604
|
+
const map = new Map();
|
|
2605
|
+
for (const project of projects){
|
|
2606
|
+
const viewport = resolveViewport(project.viewport);
|
|
2607
|
+
if (viewport) map.set(project.name, viewport);
|
|
2608
|
+
}
|
|
2609
|
+
return map;
|
|
2610
|
+
};
|
|
1688
2611
|
const ensureProcessExitCode = (code)=>{
|
|
1689
2612
|
if (void 0 === process.exitCode || 0 === process.exitCode) process.exitCode = code;
|
|
1690
2613
|
};
|
|
@@ -1796,6 +2719,31 @@ const getRuntimeConfigFromProject = (project)=>{
|
|
|
1796
2719
|
};
|
|
1797
2720
|
};
|
|
1798
2721
|
const getBrowserProjects = (context)=>context.projects.filter((project)=>project.normalizedConfig.browser.enabled);
|
|
2722
|
+
const getBrowserLaunchOptions = (project)=>({
|
|
2723
|
+
provider: project.normalizedConfig.browser.provider,
|
|
2724
|
+
browser: project.normalizedConfig.browser.browser,
|
|
2725
|
+
headless: project.normalizedConfig.browser.headless,
|
|
2726
|
+
port: project.normalizedConfig.browser.port,
|
|
2727
|
+
strictPort: project.normalizedConfig.browser.strictPort
|
|
2728
|
+
});
|
|
2729
|
+
const ensureConsistentBrowserLaunchOptions = (projects)=>{
|
|
2730
|
+
if (0 === projects.length) throw new Error('No browser-enabled projects found.');
|
|
2731
|
+
const firstProject = projects[0];
|
|
2732
|
+
const firstOptions = getBrowserLaunchOptions(firstProject);
|
|
2733
|
+
for (const project of projects.slice(1)){
|
|
2734
|
+
const options = getBrowserLaunchOptions(project);
|
|
2735
|
+
if (options.provider !== firstOptions.provider || options.browser !== firstOptions.browser || options.headless !== firstOptions.headless || options.port !== firstOptions.port || options.strictPort !== firstOptions.strictPort) throw new Error(`Browser launch config mismatch between projects "${firstProject.name}" and "${project.name}". All browser-enabled projects in one run must share provider/browser/headless/port/strictPort.`);
|
|
2736
|
+
}
|
|
2737
|
+
return firstOptions;
|
|
2738
|
+
};
|
|
2739
|
+
const resolveProviderForTestPath = ({ testPath, browserProjects })=>{
|
|
2740
|
+
const normalizedTestPath = normalize(testPath);
|
|
2741
|
+
const sortedProjects = [
|
|
2742
|
+
...browserProjects
|
|
2743
|
+
].sort((a, b)=>b.rootPath.length - a.rootPath.length);
|
|
2744
|
+
for (const project of sortedProjects)if (normalizedTestPath.startsWith(project.rootPath)) return project.provider;
|
|
2745
|
+
throw new Error(`Cannot resolve browser provider for test path: ${JSON.stringify(testPath)}. Known project roots: ${JSON.stringify(sortedProjects.map((p)=>p.rootPath))}`);
|
|
2746
|
+
};
|
|
1799
2747
|
const collectProjectEntries = async (context)=>{
|
|
1800
2748
|
const projectEntries = [];
|
|
1801
2749
|
const browserProjects = getBrowserProjects(context);
|
|
@@ -1909,20 +2857,6 @@ const htmlTemplate = `<!DOCTYPE html>
|
|
|
1909
2857
|
</body>
|
|
1910
2858
|
</html>
|
|
1911
2859
|
`;
|
|
1912
|
-
const fallbackSchedulerHtmlTemplate = `<!DOCTYPE html>
|
|
1913
|
-
<html lang="en">
|
|
1914
|
-
<head>
|
|
1915
|
-
<meta charset="UTF-8" />
|
|
1916
|
-
<title>Rstest Browser Scheduler</title>
|
|
1917
|
-
<script>
|
|
1918
|
-
window.__RSTEST_BROWSER_OPTIONS__ = ${OPTIONS_PLACEHOLDER};
|
|
1919
|
-
</script>
|
|
1920
|
-
</head>
|
|
1921
|
-
<body>
|
|
1922
|
-
<script type="module" src="/container-static/js/scheduler.js"></script>
|
|
1923
|
-
</body>
|
|
1924
|
-
</html>
|
|
1925
|
-
`;
|
|
1926
2860
|
const VIRTUAL_MANIFEST_FILENAME = 'virtual-manifest.ts';
|
|
1927
2861
|
const destroyBrowserRuntime = async (runtime)=>{
|
|
1928
2862
|
try {
|
|
@@ -1962,24 +2896,25 @@ const createBrowserRuntime = async ({ context, manifestPath, manifestSource, tem
|
|
|
1962
2896
|
[manifestPath]: manifestSource
|
|
1963
2897
|
});
|
|
1964
2898
|
const containerHtmlTemplate = containerDistPath ? await promises.readFile(join(containerDistPath, 'index.html'), 'utf-8') : null;
|
|
1965
|
-
const schedulerHtmlTemplate = containerDistPath ? await promises.readFile(join(containerDistPath, 'scheduler.html'), 'utf-8').catch(()=>null) : null;
|
|
1966
2899
|
let injectedContainerHtml = null;
|
|
1967
|
-
let injectedSchedulerHtml = null;
|
|
1968
2900
|
let serializedOptions = 'null';
|
|
2901
|
+
const dispatchHandlers = new Map();
|
|
1969
2902
|
const setContainerOptions = (options)=>{
|
|
1970
2903
|
serializedOptions = serializeForInlineScript(options);
|
|
1971
2904
|
if (containerHtmlTemplate) injectedContainerHtml = containerHtmlTemplate.replace(OPTIONS_PLACEHOLDER, serializedOptions);
|
|
1972
|
-
injectedSchedulerHtml = (schedulerHtmlTemplate || fallbackSchedulerHtmlTemplate).replace(OPTIONS_PLACEHOLDER, serializedOptions);
|
|
1973
2905
|
};
|
|
1974
2906
|
const browserProjects = getBrowserProjects(context);
|
|
1975
|
-
const
|
|
1976
|
-
|
|
1977
|
-
|
|
1978
|
-
|
|
2907
|
+
const projectByEnvironmentName = new Map(browserProjects.map((project)=>[
|
|
2908
|
+
project.environmentName,
|
|
2909
|
+
project
|
|
2910
|
+
]));
|
|
2911
|
+
const userPlugins = browserProjects.flatMap((project)=>project.normalizedConfig.plugins || []);
|
|
2912
|
+
const browserLaunchOptions = ensureConsistentBrowserLaunchOptions(browserProjects);
|
|
1979
2913
|
const browserRuntimePath = fileURLToPath(import.meta.resolve('@rstest/core/browser-runtime'));
|
|
1980
2914
|
const rstestInternalAliases = {
|
|
1981
2915
|
'@rstest/browser-manifest': manifestPath,
|
|
1982
2916
|
'@rstest/core': resolveBrowserFile('client/public.ts'),
|
|
2917
|
+
'@rstest/browser': resolveBrowserFile('browser.ts'),
|
|
1983
2918
|
'@rstest/core/browser-runtime': browserRuntimePath,
|
|
1984
2919
|
'@sinonjs/fake-timers': resolveBrowserFile('client/fakeTimersStub.ts')
|
|
1985
2920
|
};
|
|
@@ -1991,8 +2926,8 @@ const createBrowserRuntime = async ({ context, manifestPath, manifestSource, tem
|
|
|
1991
2926
|
plugins: userPlugins,
|
|
1992
2927
|
server: {
|
|
1993
2928
|
printUrls: false,
|
|
1994
|
-
port:
|
|
1995
|
-
strictPort:
|
|
2929
|
+
port: browserLaunchOptions.port ?? 4000,
|
|
2930
|
+
strictPort: browserLaunchOptions.strictPort
|
|
1996
2931
|
},
|
|
1997
2932
|
dev: {
|
|
1998
2933
|
client: {
|
|
@@ -2000,7 +2935,10 @@ const createBrowserRuntime = async ({ context, manifestPath, manifestSource, tem
|
|
|
2000
2935
|
}
|
|
2001
2936
|
},
|
|
2002
2937
|
environments: {
|
|
2003
|
-
[
|
|
2938
|
+
...Object.fromEntries(browserProjects.map((project)=>[
|
|
2939
|
+
project.environmentName,
|
|
2940
|
+
{}
|
|
2941
|
+
]))
|
|
2004
2942
|
}
|
|
2005
2943
|
}
|
|
2006
2944
|
});
|
|
@@ -2008,12 +2946,26 @@ const createBrowserRuntime = async ({ context, manifestPath, manifestSource, tem
|
|
|
2008
2946
|
{
|
|
2009
2947
|
name: 'rstest:browser-user-config',
|
|
2010
2948
|
setup (api) {
|
|
2949
|
+
api.expose?.('rstest:browser', {
|
|
2950
|
+
registerDispatchHandler: (namespace, handler)=>{
|
|
2951
|
+
dispatchHandlers.set(namespace, handler);
|
|
2952
|
+
}
|
|
2953
|
+
});
|
|
2011
2954
|
api.modifyEnvironmentConfig({
|
|
2012
|
-
handler: (config, { mergeEnvironmentConfig })=>{
|
|
2955
|
+
handler: (config, { mergeEnvironmentConfig, name })=>{
|
|
2956
|
+
const project = projectByEnvironmentName.get(name);
|
|
2957
|
+
if (!project) return config;
|
|
2958
|
+
const userRsbuildConfig = project.normalizedConfig;
|
|
2013
2959
|
const merged = mergeEnvironmentConfig(config, userRsbuildConfig, {
|
|
2014
2960
|
resolve: {
|
|
2015
2961
|
alias: rstestInternalAliases
|
|
2016
2962
|
},
|
|
2963
|
+
source: {
|
|
2964
|
+
define: {
|
|
2965
|
+
'process.env': 'globalThis[Symbol.for("rstest.env")]',
|
|
2966
|
+
'import.meta.env': 'globalThis[Symbol.for("rstest.env")]'
|
|
2967
|
+
}
|
|
2968
|
+
},
|
|
2017
2969
|
output: {
|
|
2018
2970
|
target: 'web',
|
|
2019
2971
|
sourceMap: {
|
|
@@ -2063,7 +3015,7 @@ const createBrowserRuntime = async ({ context, manifestPath, manifestSource, tem
|
|
|
2063
3015
|
api.onAfterDevCompile(async ({ stats })=>{
|
|
2064
3016
|
if (stats) {
|
|
2065
3017
|
const projectEntries = await collectProjectEntries(context);
|
|
2066
|
-
const entryTestFiles = new Set(projectEntries
|
|
3018
|
+
const entryTestFiles = new Set(collectWatchTestFiles(projectEntries).map((file)=>file.testPath));
|
|
2067
3019
|
const statsJson = stats.toJson({
|
|
2068
3020
|
all: true
|
|
2069
3021
|
});
|
|
@@ -2077,7 +3029,7 @@ const createBrowserRuntime = async ({ context, manifestPath, manifestSource, tem
|
|
|
2077
3029
|
}
|
|
2078
3030
|
}
|
|
2079
3031
|
]);
|
|
2080
|
-
const coverage =
|
|
3032
|
+
const coverage = browserProjects.find((project)=>project.normalizedConfig.coverage?.enabled)?.normalizedConfig.coverage;
|
|
2081
3033
|
if (coverage?.enabled && 'list' !== context.command) {
|
|
2082
3034
|
const { pluginCoverage } = await loadCoverageProvider(coverage, context.rootPath);
|
|
2083
3035
|
rsbuildInstance.addPlugins([
|
|
@@ -2157,13 +3109,8 @@ const createBrowserRuntime = async ({ context, manifestPath, manifestSource, tem
|
|
|
2157
3109
|
}
|
|
2158
3110
|
return;
|
|
2159
3111
|
}
|
|
2160
|
-
if ('/' === url.pathname
|
|
3112
|
+
if ('/' === url.pathname) {
|
|
2161
3113
|
if (await respondWithDevServerHtml(url, res)) return;
|
|
2162
|
-
if ('/scheduler.html' === url.pathname) {
|
|
2163
|
-
res.setHeader('Content-Type', 'text/html');
|
|
2164
|
-
res.end(injectedSchedulerHtml || (schedulerHtmlTemplate || fallbackSchedulerHtmlTemplate).replace(OPTIONS_PLACEHOLDER, 'null'));
|
|
2165
|
-
return;
|
|
2166
|
-
}
|
|
2167
3114
|
const html = injectedContainerHtml || containerHtmlTemplate?.replace(OPTIONS_PLACEHOLDER, 'null');
|
|
2168
3115
|
if (html) {
|
|
2169
3116
|
res.setHeader('Content-Type', 'text/html');
|
|
@@ -2197,44 +3144,32 @@ const createBrowserRuntime = async ({ context, manifestPath, manifestSource, tem
|
|
|
2197
3144
|
wss.once('error', reject);
|
|
2198
3145
|
});
|
|
2199
3146
|
const wsPort = wss.address().port;
|
|
2200
|
-
logger.debug(`[Browser UI] WebSocket server started on port ${wsPort}`);
|
|
2201
|
-
|
|
2202
|
-
const browserName = browserConfig.browser;
|
|
2203
|
-
try {
|
|
2204
|
-
const playwright = await import("playwright");
|
|
2205
|
-
browserLauncher = playwright[browserName];
|
|
2206
|
-
} catch (_error) {
|
|
2207
|
-
wss.close();
|
|
2208
|
-
await devServer.close();
|
|
2209
|
-
throw _error;
|
|
2210
|
-
}
|
|
2211
|
-
let browser;
|
|
3147
|
+
logger.debug(`[Browser UI] WebSocket server started on port ${wsPort}`);
|
|
3148
|
+
const browserName = browserLaunchOptions.browser ?? 'chromium';
|
|
2212
3149
|
try {
|
|
2213
|
-
|
|
2214
|
-
|
|
2215
|
-
|
|
2216
|
-
|
|
2217
|
-
'--no-first-run',
|
|
2218
|
-
'--no-default-browser-check'
|
|
2219
|
-
] : void 0
|
|
3150
|
+
const providerImplementation = getBrowserProviderImplementation(browserLaunchOptions.provider);
|
|
3151
|
+
const runtime = await providerImplementation.launchRuntime({
|
|
3152
|
+
browserName,
|
|
3153
|
+
headless: forceHeadless ?? browserLaunchOptions.headless
|
|
2220
3154
|
});
|
|
3155
|
+
return {
|
|
3156
|
+
rsbuildInstance,
|
|
3157
|
+
devServer,
|
|
3158
|
+
browser: runtime.browser,
|
|
3159
|
+
port,
|
|
3160
|
+
wsPort,
|
|
3161
|
+
manifestPath,
|
|
3162
|
+
tempDir,
|
|
3163
|
+
manifestPlugin: virtualManifestPlugin,
|
|
3164
|
+
setContainerOptions,
|
|
3165
|
+
dispatchHandlers,
|
|
3166
|
+
wss
|
|
3167
|
+
};
|
|
2221
3168
|
} catch (_error) {
|
|
2222
3169
|
wss.close();
|
|
2223
3170
|
await devServer.close();
|
|
2224
3171
|
throw _error;
|
|
2225
3172
|
}
|
|
2226
|
-
return {
|
|
2227
|
-
rsbuildInstance,
|
|
2228
|
-
devServer,
|
|
2229
|
-
browser,
|
|
2230
|
-
port,
|
|
2231
|
-
wsPort,
|
|
2232
|
-
manifestPath,
|
|
2233
|
-
tempDir,
|
|
2234
|
-
manifestPlugin: virtualManifestPlugin,
|
|
2235
|
-
setContainerOptions,
|
|
2236
|
-
wss
|
|
2237
|
-
};
|
|
2238
3173
|
};
|
|
2239
3174
|
async function resolveProjectEntries(context, shardedEntries) {
|
|
2240
3175
|
if (shardedEntries) {
|
|
@@ -2259,8 +3194,36 @@ const runBrowserController = async (context, options)=>{
|
|
|
2259
3194
|
const { skipOnTestRunEnd = false } = options ?? {};
|
|
2260
3195
|
const buildStart = Date.now();
|
|
2261
3196
|
const browserProjects = getBrowserProjects(context);
|
|
2262
|
-
const
|
|
2263
|
-
const
|
|
3197
|
+
const useHeadlessDirect = browserProjects.every((project)=>project.normalizedConfig.browser.headless);
|
|
3198
|
+
const browserSourceMapCache = new Map();
|
|
3199
|
+
const isHttpLikeFile = (file)=>/^https?:\/\//.test(file);
|
|
3200
|
+
const resolveBrowserSourcemap = async (sourcePath)=>{
|
|
3201
|
+
if (!isHttpLikeFile(sourcePath)) return {
|
|
3202
|
+
handled: false,
|
|
3203
|
+
sourcemap: null
|
|
3204
|
+
};
|
|
3205
|
+
const normalizedUrl = normalizeJavaScriptUrl(sourcePath);
|
|
3206
|
+
if (!normalizedUrl) return {
|
|
3207
|
+
handled: true,
|
|
3208
|
+
sourcemap: null
|
|
3209
|
+
};
|
|
3210
|
+
if (browserSourceMapCache.has(normalizedUrl)) return {
|
|
3211
|
+
handled: true,
|
|
3212
|
+
sourcemap: browserSourceMapCache.get(normalizedUrl) ?? null
|
|
3213
|
+
};
|
|
3214
|
+
return {
|
|
3215
|
+
handled: true,
|
|
3216
|
+
sourcemap: await loadSourceMapWithCache({
|
|
3217
|
+
jsUrl: normalizedUrl,
|
|
3218
|
+
cache: browserSourceMapCache
|
|
3219
|
+
})
|
|
3220
|
+
};
|
|
3221
|
+
};
|
|
3222
|
+
const getBrowserSourcemap = async (sourcePath)=>{
|
|
3223
|
+
const result = await resolveBrowserSourcemap(sourcePath);
|
|
3224
|
+
return result.handled ? result.sourcemap : null;
|
|
3225
|
+
};
|
|
3226
|
+
const buildErrorResult = async (error, close)=>{
|
|
2264
3227
|
const elapsed = Math.max(0, Date.now() - buildStart);
|
|
2265
3228
|
const errorResult = {
|
|
2266
3229
|
results: [],
|
|
@@ -2273,14 +3236,17 @@ const runBrowserController = async (context, options)=>{
|
|
|
2273
3236
|
hasFailure: true,
|
|
2274
3237
|
unhandledErrors: [
|
|
2275
3238
|
error
|
|
2276
|
-
]
|
|
3239
|
+
],
|
|
3240
|
+
getSourcemap: getBrowserSourcemap,
|
|
3241
|
+
resolveSourcemap: resolveBrowserSourcemap,
|
|
3242
|
+
close
|
|
2277
3243
|
};
|
|
2278
3244
|
if (!skipOnTestRunEnd) for (const reporter of context.reporters)await reporter.onTestRunEnd?.({
|
|
2279
3245
|
results: [],
|
|
2280
3246
|
testResults: [],
|
|
2281
3247
|
duration: errorResult.duration,
|
|
2282
3248
|
snapshotSummary: context.snapshotManager.summary,
|
|
2283
|
-
getSourcemap:
|
|
3249
|
+
getSourcemap: getBrowserSourcemap,
|
|
2284
3250
|
unhandledErrors: errorResult.unhandledErrors
|
|
2285
3251
|
});
|
|
2286
3252
|
return errorResult;
|
|
@@ -2288,24 +3254,51 @@ const runBrowserController = async (context, options)=>{
|
|
|
2288
3254
|
const toError = (error)=>error instanceof Error ? error : new Error(String(error));
|
|
2289
3255
|
const failWithError = async (error, cleanup)=>{
|
|
2290
3256
|
ensureProcessExitCode(1);
|
|
2291
|
-
|
|
2292
|
-
return buildErrorResult(
|
|
3257
|
+
const normalizedError = toError(error);
|
|
3258
|
+
if (cleanup && skipOnTestRunEnd) return buildErrorResult(normalizedError, cleanup);
|
|
3259
|
+
try {
|
|
3260
|
+
return await buildErrorResult(normalizedError);
|
|
3261
|
+
} finally{
|
|
3262
|
+
await cleanup?.();
|
|
3263
|
+
}
|
|
3264
|
+
};
|
|
3265
|
+
const collectDeletedTestPaths = (previous, current)=>{
|
|
3266
|
+
const currentPathSet = new Set(current.map((file)=>file.testPath));
|
|
3267
|
+
return previous.map((file)=>file.testPath).filter((testPath)=>!currentPathSet.has(testPath));
|
|
3268
|
+
};
|
|
3269
|
+
const notifyTestRunStart = async ()=>{
|
|
3270
|
+
if (skipOnTestRunEnd) return;
|
|
3271
|
+
for (const reporter of context.reporters)await reporter.onTestRunStart?.();
|
|
3272
|
+
};
|
|
3273
|
+
const notifyTestRunEnd = async ({ duration, unhandledErrors, filterRerunTestPaths })=>{
|
|
3274
|
+
if (skipOnTestRunEnd) return;
|
|
3275
|
+
for (const reporter of context.reporters)await reporter.onTestRunEnd?.({
|
|
3276
|
+
results: context.reporterResults.results,
|
|
3277
|
+
testResults: context.reporterResults.testResults,
|
|
3278
|
+
duration,
|
|
3279
|
+
snapshotSummary: context.snapshotManager.summary,
|
|
3280
|
+
getSourcemap: getBrowserSourcemap,
|
|
3281
|
+
unhandledErrors,
|
|
3282
|
+
filterRerunTestPaths
|
|
3283
|
+
});
|
|
2293
3284
|
};
|
|
2294
3285
|
const containerDevServerEnv = process.env.RSTEST_CONTAINER_DEV_SERVER;
|
|
2295
3286
|
let containerDevServer;
|
|
2296
3287
|
let containerDistPath;
|
|
2297
|
-
if (
|
|
2298
|
-
|
|
2299
|
-
|
|
2300
|
-
|
|
2301
|
-
|
|
2302
|
-
|
|
2303
|
-
|
|
2304
|
-
|
|
2305
|
-
|
|
2306
|
-
|
|
2307
|
-
|
|
2308
|
-
|
|
3288
|
+
if (!useHeadlessDirect) {
|
|
3289
|
+
if (containerDevServerEnv) try {
|
|
3290
|
+
containerDevServer = new URL(containerDevServerEnv).toString();
|
|
3291
|
+
logger.debug(`[Browser UI] Using dev server for container: ${containerDevServer}`);
|
|
3292
|
+
} catch (error) {
|
|
3293
|
+
const originalError = toError(error);
|
|
3294
|
+
originalError.message = `Invalid RSTEST_CONTAINER_DEV_SERVER value: ${originalError.message}`;
|
|
3295
|
+
return failWithError(originalError);
|
|
3296
|
+
}
|
|
3297
|
+
if (!containerDevServer) try {
|
|
3298
|
+
containerDistPath = resolveContainerDist();
|
|
3299
|
+
} catch (error) {
|
|
3300
|
+
return failWithError(error);
|
|
3301
|
+
}
|
|
2309
3302
|
}
|
|
2310
3303
|
const projectEntries = await resolveProjectEntries(context, options?.shardedEntries);
|
|
2311
3304
|
const totalTests = projectEntries.reduce((total, item)=>total + item.testFiles.length, 0);
|
|
@@ -2319,6 +3312,7 @@ const runBrowserController = async (context, options)=>{
|
|
|
2319
3312
|
if (0 !== code) ensureProcessExitCode(code);
|
|
2320
3313
|
return;
|
|
2321
3314
|
}
|
|
3315
|
+
await notifyTestRunStart();
|
|
2322
3316
|
const isWatchMode = 'watch' === context.command;
|
|
2323
3317
|
const tempDir = isWatchMode && watchContext.runtime ? watchContext.runtime.tempDir : isWatchMode ? join(context.rootPath, TEMP_RSTEST_OUTPUT_DIR, 'browser', 'watch') : join(context.rootPath, TEMP_RSTEST_OUTPUT_DIR, 'browser', Date.now().toString());
|
|
2324
3318
|
const manifestPath = join(tempDir, VIRTUAL_MANIFEST_FILENAME);
|
|
@@ -2326,10 +3320,7 @@ const runBrowserController = async (context, options)=>{
|
|
|
2326
3320
|
manifestPath,
|
|
2327
3321
|
entries: projectEntries
|
|
2328
3322
|
});
|
|
2329
|
-
if (isWatchMode) watchContext.lastTestFiles = projectEntries
|
|
2330
|
-
testPath,
|
|
2331
|
-
projectName: entry.project.name
|
|
2332
|
-
})));
|
|
3323
|
+
if (isWatchMode) watchContext.lastTestFiles = collectWatchTestFiles(projectEntries);
|
|
2333
3324
|
let runtime = isWatchMode ? watchContext.runtime : null;
|
|
2334
3325
|
let triggerRerun;
|
|
2335
3326
|
if (!runtime) {
|
|
@@ -2384,11 +3375,442 @@ const runBrowserController = async (context, options)=>{
|
|
|
2384
3375
|
debug: isDebug(),
|
|
2385
3376
|
rpcTimeout: maxTestTimeoutForRpc
|
|
2386
3377
|
};
|
|
3378
|
+
const browserProviderProjects = browserProjects.map((project)=>({
|
|
3379
|
+
rootPath: normalize(project.rootPath),
|
|
3380
|
+
provider: project.normalizedConfig.browser.provider
|
|
3381
|
+
}));
|
|
3382
|
+
const implementationByProvider = new Map();
|
|
3383
|
+
for (const browserProject of browserProviderProjects)if (!implementationByProvider.has(browserProject.provider)) implementationByProvider.set(browserProject.provider, getBrowserProviderImplementation(browserProject.provider));
|
|
3384
|
+
let activeContainerPage = null;
|
|
3385
|
+
let getHeadlessRunnerPageBySessionId;
|
|
3386
|
+
const dispatchBrowserRpcRequest = async ({ request, target })=>{
|
|
3387
|
+
const timeoutFallbackMs = maxTestTimeoutForRpc;
|
|
3388
|
+
const provider = resolveProviderForTestPath({
|
|
3389
|
+
testPath: request.testPath,
|
|
3390
|
+
browserProjects: browserProviderProjects
|
|
3391
|
+
});
|
|
3392
|
+
const implementation = implementationByProvider.get(provider);
|
|
3393
|
+
if (!implementation) throw new Error(`Browser provider implementation not found: ${provider}`);
|
|
3394
|
+
const runnerPage = target?.sessionId ? getHeadlessRunnerPageBySessionId?.(target.sessionId) : void 0;
|
|
3395
|
+
if (target?.sessionId && !runnerPage) throw new Error(`Runner page session not found for browser dispatch: ${target.sessionId}`);
|
|
3396
|
+
if (!runnerPage && !activeContainerPage) throw new Error('Browser container page is not initialized');
|
|
3397
|
+
try {
|
|
3398
|
+
return await implementation.dispatchRpc({
|
|
3399
|
+
containerPage: runnerPage ? void 0 : activeContainerPage ?? void 0,
|
|
3400
|
+
runnerPage,
|
|
3401
|
+
request,
|
|
3402
|
+
timeoutFallbackMs
|
|
3403
|
+
});
|
|
3404
|
+
} catch (error) {
|
|
3405
|
+
if (error instanceof Error) throw error.message;
|
|
3406
|
+
throw String(error);
|
|
3407
|
+
}
|
|
3408
|
+
};
|
|
3409
|
+
runtime.dispatchHandlers.set('browser', async (dispatchRequest)=>{
|
|
3410
|
+
const request = validateBrowserRpcRequest(dispatchRequest.args);
|
|
3411
|
+
return dispatchBrowserRpcRequest({
|
|
3412
|
+
request,
|
|
3413
|
+
target: dispatchRequest.target
|
|
3414
|
+
});
|
|
3415
|
+
});
|
|
2387
3416
|
runtime.setContainerOptions(hostOptions);
|
|
2388
3417
|
const reporterResults = [];
|
|
2389
3418
|
const caseResults = [];
|
|
2390
|
-
let completedTests = 0;
|
|
2391
3419
|
let fatalError = null;
|
|
3420
|
+
const snapshotRpcMethods = {
|
|
3421
|
+
async resolveSnapshotPath (testPath) {
|
|
3422
|
+
const snapExtension = '.snap';
|
|
3423
|
+
const resolver = context.normalizedConfig.resolveSnapshotPath || (()=>join(dirname(testPath), '__snapshots__', `${basename(testPath)}${snapExtension}`));
|
|
3424
|
+
return resolver(testPath, snapExtension);
|
|
3425
|
+
},
|
|
3426
|
+
async readSnapshotFile (filepath) {
|
|
3427
|
+
try {
|
|
3428
|
+
return await promises.readFile(filepath, 'utf-8');
|
|
3429
|
+
} catch {
|
|
3430
|
+
return null;
|
|
3431
|
+
}
|
|
3432
|
+
},
|
|
3433
|
+
async saveSnapshotFile (filepath, content) {
|
|
3434
|
+
const dir = dirname(filepath);
|
|
3435
|
+
await promises.mkdir(dir, {
|
|
3436
|
+
recursive: true
|
|
3437
|
+
});
|
|
3438
|
+
await promises.writeFile(filepath, content, 'utf-8');
|
|
3439
|
+
},
|
|
3440
|
+
async removeSnapshotFile (filepath) {
|
|
3441
|
+
try {
|
|
3442
|
+
await promises.unlink(filepath);
|
|
3443
|
+
} catch {}
|
|
3444
|
+
}
|
|
3445
|
+
};
|
|
3446
|
+
const handleTestFileStart = async (payload)=>{
|
|
3447
|
+
await Promise.all(context.reporters.map((reporter)=>reporter.onTestFileStart?.({
|
|
3448
|
+
testPath: payload.testPath,
|
|
3449
|
+
tests: []
|
|
3450
|
+
})));
|
|
3451
|
+
};
|
|
3452
|
+
const handleTestFileReady = async (payload)=>{
|
|
3453
|
+
await Promise.all(context.reporters.map((reporter)=>reporter.onTestFileReady?.(payload)));
|
|
3454
|
+
};
|
|
3455
|
+
const handleTestSuiteStart = async (payload)=>{
|
|
3456
|
+
await Promise.all(context.reporters.map((reporter)=>reporter.onTestSuiteStart?.(payload)));
|
|
3457
|
+
};
|
|
3458
|
+
const handleTestSuiteResult = async (payload)=>{
|
|
3459
|
+
await Promise.all(context.reporters.map((reporter)=>reporter.onTestSuiteResult?.(payload)));
|
|
3460
|
+
};
|
|
3461
|
+
const handleTestCaseStart = async (payload)=>{
|
|
3462
|
+
await Promise.all(context.reporters.map((reporter)=>reporter.onTestCaseStart?.(payload)));
|
|
3463
|
+
};
|
|
3464
|
+
const handleTestCaseResult = async (payload)=>{
|
|
3465
|
+
caseResults.push(payload);
|
|
3466
|
+
await Promise.all(context.reporters.map((reporter)=>reporter.onTestCaseResult?.(payload)));
|
|
3467
|
+
};
|
|
3468
|
+
const handleTestFileComplete = async (payload)=>{
|
|
3469
|
+
reporterResults.push(payload);
|
|
3470
|
+
context.updateReporterResultState([
|
|
3471
|
+
payload
|
|
3472
|
+
], payload.results);
|
|
3473
|
+
if (payload.snapshotResult) context.snapshotManager.add(payload.snapshotResult);
|
|
3474
|
+
await Promise.all(context.reporters.map((reporter)=>reporter.onTestFileResult?.(payload)));
|
|
3475
|
+
if ('fail' === payload.status) ensureProcessExitCode(1);
|
|
3476
|
+
};
|
|
3477
|
+
const handleLog = async (payload)=>{
|
|
3478
|
+
const log = {
|
|
3479
|
+
content: payload.content,
|
|
3480
|
+
name: payload.level,
|
|
3481
|
+
testPath: payload.testPath,
|
|
3482
|
+
type: payload.type,
|
|
3483
|
+
trace: payload.trace
|
|
3484
|
+
};
|
|
3485
|
+
const shouldLog = context.normalizedConfig.onConsoleLog?.(log.content) ?? true;
|
|
3486
|
+
if (shouldLog) await Promise.all(context.reporters.map((reporter)=>reporter.onUserConsoleLog?.(log)));
|
|
3487
|
+
};
|
|
3488
|
+
const handleFatal = async (payload)=>{
|
|
3489
|
+
const error = new Error(payload.message);
|
|
3490
|
+
error.stack = payload.stack;
|
|
3491
|
+
fatalError = error;
|
|
3492
|
+
ensureProcessExitCode(1);
|
|
3493
|
+
};
|
|
3494
|
+
const runSnapshotRpc = async (request)=>{
|
|
3495
|
+
switch(request.method){
|
|
3496
|
+
case 'resolveSnapshotPath':
|
|
3497
|
+
return snapshotRpcMethods.resolveSnapshotPath(request.args.testPath);
|
|
3498
|
+
case 'readSnapshotFile':
|
|
3499
|
+
return snapshotRpcMethods.readSnapshotFile(request.args.filepath);
|
|
3500
|
+
case 'saveSnapshotFile':
|
|
3501
|
+
return snapshotRpcMethods.saveSnapshotFile(request.args.filepath, request.args.content);
|
|
3502
|
+
case 'removeSnapshotFile':
|
|
3503
|
+
return snapshotRpcMethods.removeSnapshotFile(request.args.filepath);
|
|
3504
|
+
default:
|
|
3505
|
+
return;
|
|
3506
|
+
}
|
|
3507
|
+
};
|
|
3508
|
+
const createDispatchRouter = (options)=>createHostDispatchRouter({
|
|
3509
|
+
routerOptions: options,
|
|
3510
|
+
runnerCallbacks: {
|
|
3511
|
+
onTestFileStart: handleTestFileStart,
|
|
3512
|
+
onTestFileReady: handleTestFileReady,
|
|
3513
|
+
onTestSuiteStart: handleTestSuiteStart,
|
|
3514
|
+
onTestSuiteResult: handleTestSuiteResult,
|
|
3515
|
+
onTestCaseStart: handleTestCaseStart,
|
|
3516
|
+
onTestCaseResult: handleTestCaseResult,
|
|
3517
|
+
onTestFileComplete: handleTestFileComplete,
|
|
3518
|
+
onLog: handleLog,
|
|
3519
|
+
onFatal: handleFatal
|
|
3520
|
+
},
|
|
3521
|
+
runSnapshotRpc,
|
|
3522
|
+
extensionHandlers: runtime.dispatchHandlers,
|
|
3523
|
+
onDuplicateNamespace: (namespace)=>{
|
|
3524
|
+
logger.debug(`[Dispatch] Skip registering dispatch namespace "${namespace}" because it is already reserved`);
|
|
3525
|
+
}
|
|
3526
|
+
});
|
|
3527
|
+
if (useHeadlessDirect) {
|
|
3528
|
+
const viewportByProject = mapViewportByProject(projectRuntimeConfigs);
|
|
3529
|
+
const runLifecycle = new RunSessionLifecycle();
|
|
3530
|
+
const sessionRegistry = new RunnerSessionRegistry();
|
|
3531
|
+
getHeadlessRunnerPageBySessionId = (sessionId)=>sessionRegistry.getById(sessionId)?.page;
|
|
3532
|
+
let dispatchRequestCounter = 0;
|
|
3533
|
+
const nextDispatchRequestId = (namespace)=>`${namespace}-${++dispatchRequestCounter}`;
|
|
3534
|
+
const closeContextSafely = async (browserContext)=>{
|
|
3535
|
+
try {
|
|
3536
|
+
await browserContext.close();
|
|
3537
|
+
} catch {}
|
|
3538
|
+
};
|
|
3539
|
+
const cancelRun = async (run, waitForDone = true)=>{
|
|
3540
|
+
await runLifecycle.cancel(run, {
|
|
3541
|
+
waitForDone,
|
|
3542
|
+
onCancel: async (session)=>{
|
|
3543
|
+
await Promise.all(Array.from(session.contexts).map((browserContext)=>closeContextSafely(browserContext)));
|
|
3544
|
+
}
|
|
3545
|
+
});
|
|
3546
|
+
};
|
|
3547
|
+
const dispatchRouter = createDispatchRouter({
|
|
3548
|
+
isRunTokenStale: (runToken)=>runLifecycle.isTokenStale(runToken),
|
|
3549
|
+
onStale: (request)=>{
|
|
3550
|
+
if (request.namespace === DISPATCH_NAMESPACE_RUNNER) logger.debug(`[Headless] Dropped stale message "${request.method}" for ${request.target?.testFile ?? 'unknown'}`);
|
|
3551
|
+
}
|
|
3552
|
+
});
|
|
3553
|
+
const dispatchRunnerMessage = async (run, file, sessionId, message)=>{
|
|
3554
|
+
const response = await dispatchRouter.dispatch({
|
|
3555
|
+
requestId: nextDispatchRequestId('runner'),
|
|
3556
|
+
runToken: run.token,
|
|
3557
|
+
namespace: DISPATCH_NAMESPACE_RUNNER,
|
|
3558
|
+
method: message.type,
|
|
3559
|
+
args: 'payload' in message ? message.payload : void 0,
|
|
3560
|
+
target: {
|
|
3561
|
+
sessionId,
|
|
3562
|
+
testFile: file.testPath,
|
|
3563
|
+
projectName: file.projectName
|
|
3564
|
+
}
|
|
3565
|
+
});
|
|
3566
|
+
if (response.stale) return;
|
|
3567
|
+
if (response.error) throw new Error(response.error);
|
|
3568
|
+
};
|
|
3569
|
+
const runSingleFile = async (run, file)=>{
|
|
3570
|
+
if (run.cancelled || runLifecycle.isTokenStale(run.token)) return;
|
|
3571
|
+
const viewport = viewportByProject.get(file.projectName);
|
|
3572
|
+
const browserContext = await browser.newContext({
|
|
3573
|
+
viewport: viewport ?? null
|
|
3574
|
+
});
|
|
3575
|
+
run.contexts.add(browserContext);
|
|
3576
|
+
let page = null;
|
|
3577
|
+
let sessionId = null;
|
|
3578
|
+
let settled = false;
|
|
3579
|
+
let resolveDone = null;
|
|
3580
|
+
const markDone = ()=>{
|
|
3581
|
+
if (!settled) {
|
|
3582
|
+
settled = true;
|
|
3583
|
+
resolveDone?.();
|
|
3584
|
+
}
|
|
3585
|
+
};
|
|
3586
|
+
const donePromise = new Promise((resolve)=>{
|
|
3587
|
+
resolveDone = resolve;
|
|
3588
|
+
});
|
|
3589
|
+
const projectRuntime = projectRuntimeConfigs.find((project)=>project.name === file.projectName);
|
|
3590
|
+
const perFileTimeoutMs = (projectRuntime?.runtimeConfig.testTimeout ?? maxTestTimeoutForRpc) + 30000;
|
|
3591
|
+
let timeoutId;
|
|
3592
|
+
try {
|
|
3593
|
+
page = await browserContext.newPage();
|
|
3594
|
+
const session = sessionRegistry.register({
|
|
3595
|
+
testFile: file.testPath,
|
|
3596
|
+
projectName: file.projectName,
|
|
3597
|
+
runToken: run.token,
|
|
3598
|
+
mode: 'headless-page',
|
|
3599
|
+
context: browserContext,
|
|
3600
|
+
page
|
|
3601
|
+
});
|
|
3602
|
+
sessionId = session.id;
|
|
3603
|
+
await attachHeadlessRunnerTransport(page, {
|
|
3604
|
+
onDispatchMessage: async (message)=>{
|
|
3605
|
+
try {
|
|
3606
|
+
await dispatchRunnerMessage(run, file, session.id, message);
|
|
3607
|
+
if ('file-complete' === message.type || 'complete' === message.type) markDone();
|
|
3608
|
+
else if ('fatal' === message.type) {
|
|
3609
|
+
markDone();
|
|
3610
|
+
await cancelRun(run, false);
|
|
3611
|
+
}
|
|
3612
|
+
} catch (error) {
|
|
3613
|
+
const formatted = toError(error);
|
|
3614
|
+
await handleFatal({
|
|
3615
|
+
message: formatted.message,
|
|
3616
|
+
stack: formatted.stack
|
|
3617
|
+
});
|
|
3618
|
+
markDone();
|
|
3619
|
+
await cancelRun(run, false);
|
|
3620
|
+
}
|
|
3621
|
+
},
|
|
3622
|
+
onDispatchRpc: async (request)=>dispatchRouter.dispatch({
|
|
3623
|
+
...request,
|
|
3624
|
+
runToken: run.token,
|
|
3625
|
+
target: {
|
|
3626
|
+
sessionId: session.id,
|
|
3627
|
+
testFile: file.testPath,
|
|
3628
|
+
projectName: file.projectName,
|
|
3629
|
+
...request.target
|
|
3630
|
+
}
|
|
3631
|
+
})
|
|
3632
|
+
});
|
|
3633
|
+
const inlineOptions = {
|
|
3634
|
+
...hostOptions,
|
|
3635
|
+
testFile: file.testPath,
|
|
3636
|
+
runId: `${run.token}:${session.id}`
|
|
3637
|
+
};
|
|
3638
|
+
const serializedOptions = serializeForInlineScript(inlineOptions);
|
|
3639
|
+
await page.addInitScript(`window.__RSTEST_BROWSER_OPTIONS__ = ${serializedOptions};`);
|
|
3640
|
+
await page.goto(`http://localhost:${port}/runner.html`, {
|
|
3641
|
+
waitUntil: 'load'
|
|
3642
|
+
});
|
|
3643
|
+
const timeoutPromise = new Promise((resolve)=>{
|
|
3644
|
+
timeoutId = setTimeout(()=>resolve('timeout'), perFileTimeoutMs);
|
|
3645
|
+
});
|
|
3646
|
+
const state = await Promise.race([
|
|
3647
|
+
donePromise.then(()=>'done'),
|
|
3648
|
+
timeoutPromise,
|
|
3649
|
+
run.cancelSignal.then(()=>'cancelled')
|
|
3650
|
+
]);
|
|
3651
|
+
if ('cancelled' === state) return;
|
|
3652
|
+
if ('timeout' === state && runLifecycle.isTokenActive(run.token) && !run.cancelled) {
|
|
3653
|
+
await handleFatal({
|
|
3654
|
+
message: `Test execution timeout after ${perFileTimeoutMs / 1000}s for ${file.testPath}.`
|
|
3655
|
+
});
|
|
3656
|
+
await cancelRun(run, false);
|
|
3657
|
+
}
|
|
3658
|
+
} catch (error) {
|
|
3659
|
+
if (runLifecycle.isTokenActive(run.token) && !run.cancelled) {
|
|
3660
|
+
const formatted = toError(error);
|
|
3661
|
+
await handleFatal({
|
|
3662
|
+
message: formatted.message,
|
|
3663
|
+
stack: formatted.stack
|
|
3664
|
+
});
|
|
3665
|
+
await cancelRun(run, false);
|
|
3666
|
+
}
|
|
3667
|
+
} finally{
|
|
3668
|
+
if (timeoutId) clearTimeout(timeoutId);
|
|
3669
|
+
if (page) try {
|
|
3670
|
+
await page.close();
|
|
3671
|
+
} catch {}
|
|
3672
|
+
if (sessionId) sessionRegistry.deleteById(sessionId);
|
|
3673
|
+
run.contexts.delete(browserContext);
|
|
3674
|
+
await closeContextSafely(browserContext);
|
|
3675
|
+
}
|
|
3676
|
+
};
|
|
3677
|
+
const runFilesWithPool = async (files)=>{
|
|
3678
|
+
if (0 === files.length) return;
|
|
3679
|
+
const previous = runLifecycle.activeSession;
|
|
3680
|
+
if (previous) await cancelRun(previous);
|
|
3681
|
+
const run = runLifecycle.createSession((token)=>({
|
|
3682
|
+
...createRunSession(token),
|
|
3683
|
+
contexts: new Set()
|
|
3684
|
+
}));
|
|
3685
|
+
const queue = [
|
|
3686
|
+
...files
|
|
3687
|
+
];
|
|
3688
|
+
const concurrency = getHeadlessConcurrency(context, queue.length);
|
|
3689
|
+
const worker = async ()=>{
|
|
3690
|
+
while(queue.length > 0 && !run.cancelled && runLifecycle.isTokenActive(run.token)){
|
|
3691
|
+
const next = queue.shift();
|
|
3692
|
+
if (!next) return;
|
|
3693
|
+
await runSingleFile(run, next);
|
|
3694
|
+
}
|
|
3695
|
+
};
|
|
3696
|
+
run.done = Promise.all(Array.from({
|
|
3697
|
+
length: Math.min(queue.length, Math.max(concurrency, 1))
|
|
3698
|
+
}, ()=>worker())).then(()=>{});
|
|
3699
|
+
await run.done;
|
|
3700
|
+
runLifecycle.clearIfActive(run);
|
|
3701
|
+
};
|
|
3702
|
+
const latestRerunScheduler = createHeadlessLatestRerunScheduler({
|
|
3703
|
+
getActiveRun: ()=>runLifecycle.activeSession,
|
|
3704
|
+
isRunCancelled: (run)=>run.cancelled,
|
|
3705
|
+
invalidateActiveRun: ()=>{
|
|
3706
|
+
runLifecycle.invalidateActiveToken();
|
|
3707
|
+
},
|
|
3708
|
+
interruptActiveRun: async (run)=>{
|
|
3709
|
+
await cancelRun(run, false);
|
|
3710
|
+
},
|
|
3711
|
+
runFiles: async (files)=>{
|
|
3712
|
+
await notifyTestRunStart();
|
|
3713
|
+
const rerunStartTime = Date.now();
|
|
3714
|
+
const fatalErrorBeforeRun = fatalError;
|
|
3715
|
+
let rerunError;
|
|
3716
|
+
try {
|
|
3717
|
+
await runFilesWithPool(files);
|
|
3718
|
+
} catch (error) {
|
|
3719
|
+
rerunError = toError(error);
|
|
3720
|
+
throw error;
|
|
3721
|
+
} finally{
|
|
3722
|
+
const testTime = Math.max(0, Date.now() - rerunStartTime);
|
|
3723
|
+
const rerunFatalError = fatalError && fatalError !== fatalErrorBeforeRun ? fatalError : void 0;
|
|
3724
|
+
await notifyTestRunEnd({
|
|
3725
|
+
duration: {
|
|
3726
|
+
totalTime: testTime,
|
|
3727
|
+
buildTime: 0,
|
|
3728
|
+
testTime
|
|
3729
|
+
},
|
|
3730
|
+
filterRerunTestPaths: files.map((file)=>file.testPath),
|
|
3731
|
+
unhandledErrors: rerunError ? [
|
|
3732
|
+
rerunError
|
|
3733
|
+
] : rerunFatalError ? [
|
|
3734
|
+
rerunFatalError
|
|
3735
|
+
] : void 0
|
|
3736
|
+
});
|
|
3737
|
+
}
|
|
3738
|
+
},
|
|
3739
|
+
onError: async (error)=>{
|
|
3740
|
+
const formatted = toError(error);
|
|
3741
|
+
await handleFatal({
|
|
3742
|
+
message: formatted.message,
|
|
3743
|
+
stack: formatted.stack
|
|
3744
|
+
});
|
|
3745
|
+
},
|
|
3746
|
+
onInterrupt: (run)=>{
|
|
3747
|
+
logger.debug(`[Headless] Interrupting active run token ${run.token} before scheduling latest rerun`);
|
|
3748
|
+
}
|
|
3749
|
+
});
|
|
3750
|
+
const testStart = Date.now();
|
|
3751
|
+
await runFilesWithPool(allTestFiles);
|
|
3752
|
+
const testTime = Date.now() - testStart;
|
|
3753
|
+
if (isWatchMode) triggerRerun = async ()=>{
|
|
3754
|
+
const newProjectEntries = await collectProjectEntries(context);
|
|
3755
|
+
const rerunPlan = planWatchRerun({
|
|
3756
|
+
projectEntries: newProjectEntries,
|
|
3757
|
+
previousTestFiles: watchContext.lastTestFiles,
|
|
3758
|
+
affectedTestFiles: watchContext.affectedTestFiles
|
|
3759
|
+
});
|
|
3760
|
+
watchContext.affectedTestFiles = [];
|
|
3761
|
+
if (rerunPlan.filesChanged) {
|
|
3762
|
+
const deletedTestPaths = collectDeletedTestPaths(watchContext.lastTestFiles, rerunPlan.currentTestFiles);
|
|
3763
|
+
if (deletedTestPaths.length > 0) context.updateReporterResultState([], [], deletedTestPaths);
|
|
3764
|
+
watchContext.lastTestFiles = rerunPlan.currentTestFiles;
|
|
3765
|
+
if (0 === rerunPlan.currentTestFiles.length) {
|
|
3766
|
+
await latestRerunScheduler.enqueueLatest([]);
|
|
3767
|
+
logger.log(color.cyan('No browser test files remain after update.\n'));
|
|
3768
|
+
return;
|
|
3769
|
+
}
|
|
3770
|
+
logger.log(color.cyan(`Test file set changed, re-running ${rerunPlan.currentTestFiles.length} file(s)...\n`));
|
|
3771
|
+
latestRerunScheduler.enqueueLatest(rerunPlan.currentTestFiles);
|
|
3772
|
+
return;
|
|
3773
|
+
}
|
|
3774
|
+
if (0 === rerunPlan.affectedTestFiles.length) return void logger.log(color.cyan('No affected browser test files detected, skipping re-run.\n'));
|
|
3775
|
+
logger.log(color.cyan(`Re-running ${rerunPlan.affectedTestFiles.length} affected test file(s)...\n`));
|
|
3776
|
+
latestRerunScheduler.enqueueLatest(rerunPlan.affectedTestFiles);
|
|
3777
|
+
};
|
|
3778
|
+
const closeHeadlessRuntime = isWatchMode ? void 0 : async ()=>{
|
|
3779
|
+
sessionRegistry.clear();
|
|
3780
|
+
await destroyBrowserRuntime(runtime);
|
|
3781
|
+
};
|
|
3782
|
+
if (fatalError) return failWithError(fatalError, closeHeadlessRuntime);
|
|
3783
|
+
const duration = {
|
|
3784
|
+
totalTime: buildTime + testTime,
|
|
3785
|
+
buildTime,
|
|
3786
|
+
testTime
|
|
3787
|
+
};
|
|
3788
|
+
context.updateReporterResultState(reporterResults, caseResults);
|
|
3789
|
+
const isFailure = reporterResults.some((result)=>'fail' === result.status);
|
|
3790
|
+
if (isFailure) ensureProcessExitCode(1);
|
|
3791
|
+
const result = {
|
|
3792
|
+
results: reporterResults,
|
|
3793
|
+
testResults: caseResults,
|
|
3794
|
+
duration,
|
|
3795
|
+
hasFailure: isFailure,
|
|
3796
|
+
getSourcemap: getBrowserSourcemap,
|
|
3797
|
+
resolveSourcemap: resolveBrowserSourcemap,
|
|
3798
|
+
close: skipOnTestRunEnd ? closeHeadlessRuntime : void 0
|
|
3799
|
+
};
|
|
3800
|
+
if (!skipOnTestRunEnd) try {
|
|
3801
|
+
await notifyTestRunEnd({
|
|
3802
|
+
duration
|
|
3803
|
+
});
|
|
3804
|
+
} finally{
|
|
3805
|
+
await closeHeadlessRuntime?.();
|
|
3806
|
+
}
|
|
3807
|
+
if (isWatchMode && triggerRerun) {
|
|
3808
|
+
watchContext.hooksEnabled = true;
|
|
3809
|
+
logger.log(color.cyan('\nWatch mode enabled - will re-run tests on file changes\n'));
|
|
3810
|
+
}
|
|
3811
|
+
return result;
|
|
3812
|
+
}
|
|
3813
|
+
let completedTests = 0;
|
|
2392
3814
|
let resolveAllTests;
|
|
2393
3815
|
const allTestsPromise = new Promise((resolve)=>{
|
|
2394
3816
|
resolveAllTests = resolve;
|
|
@@ -2418,9 +3840,11 @@ const runBrowserController = async (context, options)=>{
|
|
|
2418
3840
|
}
|
|
2419
3841
|
containerPage.on('console', (msg)=>{
|
|
2420
3842
|
const text = msg.text();
|
|
2421
|
-
if (text.startsWith('[Container]') || text.startsWith('[Runner]')
|
|
3843
|
+
if (text.startsWith('[Container]') || text.startsWith('[Runner]')) logger.log(color.gray(`[Browser Console] ${text}`));
|
|
2422
3844
|
});
|
|
2423
3845
|
}
|
|
3846
|
+
activeContainerPage = containerPage;
|
|
3847
|
+
const dispatchRouter = createDispatchRouter();
|
|
2424
3848
|
const createRpcMethods = ()=>({
|
|
2425
3849
|
async rerunTest (testFile, testNamePattern) {
|
|
2426
3850
|
const projectName = context.normalizedConfig.name || 'project';
|
|
@@ -2433,61 +3857,25 @@ const runBrowserController = async (context, options)=>{
|
|
|
2433
3857
|
return allTestFiles;
|
|
2434
3858
|
},
|
|
2435
3859
|
async onTestFileStart (payload) {
|
|
2436
|
-
await
|
|
2437
|
-
testPath: payload.testPath,
|
|
2438
|
-
tests: []
|
|
2439
|
-
})));
|
|
3860
|
+
await handleTestFileStart(payload);
|
|
2440
3861
|
},
|
|
2441
3862
|
async onTestCaseResult (payload) {
|
|
2442
|
-
|
|
2443
|
-
await Promise.all(context.reporters.map((reporter)=>reporter.onTestCaseResult?.(payload)));
|
|
3863
|
+
await handleTestCaseResult(payload);
|
|
2444
3864
|
},
|
|
2445
3865
|
async onTestFileComplete (payload) {
|
|
2446
|
-
|
|
2447
|
-
if (payload.snapshotResult) context.snapshotManager.add(payload.snapshotResult);
|
|
2448
|
-
await Promise.all(context.reporters.map((reporter)=>reporter.onTestFileResult?.(payload)));
|
|
3866
|
+
await handleTestFileComplete(payload);
|
|
2449
3867
|
completedTests++;
|
|
2450
3868
|
if (completedTests >= allTestFiles.length && resolveAllTests) resolveAllTests();
|
|
2451
3869
|
},
|
|
2452
3870
|
async onLog (payload) {
|
|
2453
|
-
|
|
2454
|
-
content: payload.content,
|
|
2455
|
-
name: payload.level,
|
|
2456
|
-
testPath: payload.testPath,
|
|
2457
|
-
type: payload.type,
|
|
2458
|
-
trace: payload.trace
|
|
2459
|
-
};
|
|
2460
|
-
const shouldLog = context.normalizedConfig.onConsoleLog?.(log.content) ?? true;
|
|
2461
|
-
if (shouldLog) await Promise.all(context.reporters.map((reporter)=>reporter.onUserConsoleLog?.(log)));
|
|
3871
|
+
await handleLog(payload);
|
|
2462
3872
|
},
|
|
2463
3873
|
async onFatal (payload) {
|
|
2464
|
-
|
|
2465
|
-
fatalError.stack = payload.stack;
|
|
3874
|
+
await handleFatal(payload);
|
|
2466
3875
|
if (resolveAllTests) resolveAllTests();
|
|
2467
3876
|
},
|
|
2468
|
-
async
|
|
2469
|
-
|
|
2470
|
-
const resolver = context.normalizedConfig.resolveSnapshotPath || (()=>join(dirname(testPath), '__snapshots__', `${basename(testPath)}${snapExtension}`));
|
|
2471
|
-
return resolver(testPath, snapExtension);
|
|
2472
|
-
},
|
|
2473
|
-
async readSnapshotFile (filepath) {
|
|
2474
|
-
try {
|
|
2475
|
-
return await promises.readFile(filepath, 'utf-8');
|
|
2476
|
-
} catch {
|
|
2477
|
-
return null;
|
|
2478
|
-
}
|
|
2479
|
-
},
|
|
2480
|
-
async saveSnapshotFile (filepath, content) {
|
|
2481
|
-
const dir = dirname(filepath);
|
|
2482
|
-
await promises.mkdir(dir, {
|
|
2483
|
-
recursive: true
|
|
2484
|
-
});
|
|
2485
|
-
await promises.writeFile(filepath, content, 'utf-8');
|
|
2486
|
-
},
|
|
2487
|
-
async removeSnapshotFile (filepath) {
|
|
2488
|
-
try {
|
|
2489
|
-
await promises.unlink(filepath);
|
|
2490
|
-
} catch {}
|
|
3877
|
+
async dispatch (request) {
|
|
3878
|
+
return dispatchRouter.dispatch(request);
|
|
2491
3879
|
}
|
|
2492
3880
|
});
|
|
2493
3881
|
let rpcManager;
|
|
@@ -2501,11 +3889,7 @@ const runBrowserController = async (context, options)=>{
|
|
|
2501
3889
|
if (isWatchMode) runtime.rpcManager = rpcManager;
|
|
2502
3890
|
}
|
|
2503
3891
|
if (isNewPage) {
|
|
2504
|
-
const pagePath =
|
|
2505
|
-
if (useSchedulerPage) {
|
|
2506
|
-
const serializedOptions = serializeForInlineScript(hostOptions);
|
|
2507
|
-
await containerPage.addInitScript(`window.__RSTEST_BROWSER_OPTIONS__ = ${serializedOptions};`);
|
|
2508
|
-
}
|
|
3892
|
+
const pagePath = '/';
|
|
2509
3893
|
await containerPage.goto(`http://localhost:${port}${pagePath}`, {
|
|
2510
3894
|
waitUntil: 'load'
|
|
2511
3895
|
});
|
|
@@ -2529,24 +3913,49 @@ const runBrowserController = async (context, options)=>{
|
|
|
2529
3913
|
const testTime = Date.now() - testStart;
|
|
2530
3914
|
if (isWatchMode) triggerRerun = async ()=>{
|
|
2531
3915
|
const newProjectEntries = await collectProjectEntries(context);
|
|
2532
|
-
const
|
|
2533
|
-
|
|
2534
|
-
|
|
2535
|
-
|
|
2536
|
-
|
|
2537
|
-
const filesChanged = serialize(currentTestFiles) !== serialize(watchContext.lastTestFiles);
|
|
2538
|
-
if (filesChanged) {
|
|
2539
|
-
watchContext.lastTestFiles = currentTestFiles;
|
|
2540
|
-
await rpcManager.notifyTestFileUpdate(currentTestFiles);
|
|
2541
|
-
}
|
|
2542
|
-
const affectedFiles = watchContext.affectedTestFiles;
|
|
3916
|
+
const rerunPlan = planWatchRerun({
|
|
3917
|
+
projectEntries: newProjectEntries,
|
|
3918
|
+
previousTestFiles: watchContext.lastTestFiles,
|
|
3919
|
+
affectedTestFiles: watchContext.affectedTestFiles
|
|
3920
|
+
});
|
|
2543
3921
|
watchContext.affectedTestFiles = [];
|
|
2544
|
-
if (
|
|
2545
|
-
|
|
2546
|
-
|
|
2547
|
-
|
|
3922
|
+
if (rerunPlan.filesChanged) {
|
|
3923
|
+
const deletedTestPaths = collectDeletedTestPaths(watchContext.lastTestFiles, rerunPlan.currentTestFiles);
|
|
3924
|
+
if (deletedTestPaths.length > 0) context.updateReporterResultState([], [], deletedTestPaths);
|
|
3925
|
+
watchContext.lastTestFiles = rerunPlan.currentTestFiles;
|
|
3926
|
+
await rpcManager.notifyTestFileUpdate(rerunPlan.currentTestFiles);
|
|
3927
|
+
}
|
|
3928
|
+
if (rerunPlan.normalizedAffectedTestFiles.length > 0) {
|
|
3929
|
+
logger.log(color.cyan(`Re-running ${rerunPlan.normalizedAffectedTestFiles.length} affected test file(s)...\n`));
|
|
3930
|
+
await notifyTestRunStart();
|
|
3931
|
+
const rerunStartTime = Date.now();
|
|
3932
|
+
const fatalErrorBeforeRun = fatalError;
|
|
3933
|
+
let rerunError;
|
|
3934
|
+
try {
|
|
3935
|
+
for (const testFile of rerunPlan.normalizedAffectedTestFiles)await rpcManager.reloadTestFile(testFile);
|
|
3936
|
+
} catch (error) {
|
|
3937
|
+
rerunError = toError(error);
|
|
3938
|
+
throw error;
|
|
3939
|
+
} finally{
|
|
3940
|
+
const testTime = Math.max(0, Date.now() - rerunStartTime);
|
|
3941
|
+
const rerunFatalError = fatalError && fatalError !== fatalErrorBeforeRun ? fatalError : void 0;
|
|
3942
|
+
await notifyTestRunEnd({
|
|
3943
|
+
duration: {
|
|
3944
|
+
totalTime: testTime,
|
|
3945
|
+
buildTime: 0,
|
|
3946
|
+
testTime
|
|
3947
|
+
},
|
|
3948
|
+
filterRerunTestPaths: rerunPlan.normalizedAffectedTestFiles,
|
|
3949
|
+
unhandledErrors: rerunError ? [
|
|
3950
|
+
rerunError
|
|
3951
|
+
] : rerunFatalError ? [
|
|
3952
|
+
rerunFatalError
|
|
3953
|
+
] : void 0
|
|
3954
|
+
});
|
|
3955
|
+
}
|
|
3956
|
+
} else if (!rerunPlan.filesChanged) logger.log(color.cyan('Tests will be re-executed automatically\n'));
|
|
2548
3957
|
};
|
|
2549
|
-
|
|
3958
|
+
const closeContainerRuntime = isWatchMode ? void 0 : async ()=>{
|
|
2550
3959
|
try {
|
|
2551
3960
|
await containerPage.close();
|
|
2552
3961
|
} catch {}
|
|
@@ -2554,8 +3963,8 @@ const runBrowserController = async (context, options)=>{
|
|
|
2554
3963
|
await containerContext.close();
|
|
2555
3964
|
} catch {}
|
|
2556
3965
|
await destroyBrowserRuntime(runtime);
|
|
2557
|
-
}
|
|
2558
|
-
if (fatalError) return failWithError(fatalError);
|
|
3966
|
+
};
|
|
3967
|
+
if (fatalError) return failWithError(fatalError, closeContainerRuntime);
|
|
2559
3968
|
const duration = {
|
|
2560
3969
|
totalTime: buildTime + testTime,
|
|
2561
3970
|
buildTime,
|
|
@@ -2568,15 +3977,18 @@ const runBrowserController = async (context, options)=>{
|
|
|
2568
3977
|
results: reporterResults,
|
|
2569
3978
|
testResults: caseResults,
|
|
2570
3979
|
duration,
|
|
2571
|
-
hasFailure: isFailure
|
|
3980
|
+
hasFailure: isFailure,
|
|
3981
|
+
getSourcemap: getBrowserSourcemap,
|
|
3982
|
+
resolveSourcemap: resolveBrowserSourcemap,
|
|
3983
|
+
close: skipOnTestRunEnd ? closeContainerRuntime : void 0
|
|
2572
3984
|
};
|
|
2573
|
-
if (!skipOnTestRunEnd)
|
|
2574
|
-
|
|
2575
|
-
|
|
2576
|
-
|
|
2577
|
-
|
|
2578
|
-
|
|
2579
|
-
}
|
|
3985
|
+
if (!skipOnTestRunEnd) try {
|
|
3986
|
+
await notifyTestRunEnd({
|
|
3987
|
+
duration
|
|
3988
|
+
});
|
|
3989
|
+
} finally{
|
|
3990
|
+
await closeContainerRuntime?.();
|
|
3991
|
+
}
|
|
2580
3992
|
if (isWatchMode && triggerRerun) {
|
|
2581
3993
|
watchContext.hooksEnabled = true;
|
|
2582
3994
|
logger.log(color.cyan('\nWatch mode enabled - will re-run tests on file changes\n'));
|
|
@@ -2596,6 +4008,7 @@ const listBrowserTests = async (context, options)=>{
|
|
|
2596
4008
|
manifestPath,
|
|
2597
4009
|
entries: projectEntries
|
|
2598
4010
|
});
|
|
4011
|
+
const browserProjects = getBrowserProjects(context);
|
|
2599
4012
|
let runtime;
|
|
2600
4013
|
try {
|
|
2601
4014
|
runtime = await createBrowserRuntime({
|
|
@@ -2609,11 +4022,13 @@ const listBrowserTests = async (context, options)=>{
|
|
|
2609
4022
|
forceHeadless: true
|
|
2610
4023
|
});
|
|
2611
4024
|
} catch (error) {
|
|
2612
|
-
|
|
4025
|
+
const providers = [
|
|
4026
|
+
...new Set(browserProjects.map((p)=>p.normalizedConfig.browser.provider))
|
|
4027
|
+
];
|
|
4028
|
+
logger.error(color.red(`Failed to initialize browser provider runtime (${providers.join(', ')}).`), error);
|
|
2613
4029
|
throw error;
|
|
2614
4030
|
}
|
|
2615
4031
|
const { browser, port } = runtime;
|
|
2616
|
-
const browserProjects = getBrowserProjects(context);
|
|
2617
4032
|
const projectRuntimeConfigs = browserProjects.map((project)=>({
|
|
2618
4033
|
name: project.name,
|
|
2619
4034
|
environmentName: project.environmentName,
|
|
@@ -2644,7 +4059,7 @@ const listBrowserTests = async (context, options)=>{
|
|
|
2644
4059
|
viewport: null
|
|
2645
4060
|
});
|
|
2646
4061
|
const page = await browserContext.newPage();
|
|
2647
|
-
await page.exposeFunction(
|
|
4062
|
+
await page.exposeFunction(DISPATCH_MESSAGE_TYPE, (message)=>{
|
|
2648
4063
|
switch(message.type){
|
|
2649
4064
|
case 'collect-result':
|
|
2650
4065
|
{
|
|
@@ -2726,99 +4141,6 @@ const listBrowserTests = async (context, options)=>{
|
|
|
2726
4141
|
close: cleanup
|
|
2727
4142
|
};
|
|
2728
4143
|
};
|
|
2729
|
-
const BROWSER_VIEWPORT_PRESET_IDS = [
|
|
2730
|
-
'iPhoneSE',
|
|
2731
|
-
'iPhoneXR',
|
|
2732
|
-
'iPhone12Pro',
|
|
2733
|
-
'iPhone14ProMax',
|
|
2734
|
-
'Pixel7',
|
|
2735
|
-
'SamsungGalaxyS8Plus',
|
|
2736
|
-
'SamsungGalaxyS20Ultra',
|
|
2737
|
-
'iPadMini',
|
|
2738
|
-
'iPadAir',
|
|
2739
|
-
'iPadPro',
|
|
2740
|
-
'SurfacePro7',
|
|
2741
|
-
'SurfaceDuo',
|
|
2742
|
-
'GalaxyZFold5',
|
|
2743
|
-
'AsusZenbookFold',
|
|
2744
|
-
'SamsungGalaxyA51A71',
|
|
2745
|
-
'NestHub',
|
|
2746
|
-
'NestHubMax'
|
|
2747
|
-
];
|
|
2748
|
-
const BROWSER_VIEWPORT_PRESET_DIMENSIONS = {
|
|
2749
|
-
iPhoneSE: {
|
|
2750
|
-
width: 375,
|
|
2751
|
-
height: 667
|
|
2752
|
-
},
|
|
2753
|
-
iPhoneXR: {
|
|
2754
|
-
width: 414,
|
|
2755
|
-
height: 896
|
|
2756
|
-
},
|
|
2757
|
-
iPhone12Pro: {
|
|
2758
|
-
width: 390,
|
|
2759
|
-
height: 844
|
|
2760
|
-
},
|
|
2761
|
-
iPhone14ProMax: {
|
|
2762
|
-
width: 430,
|
|
2763
|
-
height: 932
|
|
2764
|
-
},
|
|
2765
|
-
Pixel7: {
|
|
2766
|
-
width: 412,
|
|
2767
|
-
height: 915
|
|
2768
|
-
},
|
|
2769
|
-
SamsungGalaxyS8Plus: {
|
|
2770
|
-
width: 360,
|
|
2771
|
-
height: 740
|
|
2772
|
-
},
|
|
2773
|
-
SamsungGalaxyS20Ultra: {
|
|
2774
|
-
width: 412,
|
|
2775
|
-
height: 915
|
|
2776
|
-
},
|
|
2777
|
-
iPadMini: {
|
|
2778
|
-
width: 768,
|
|
2779
|
-
height: 1024
|
|
2780
|
-
},
|
|
2781
|
-
iPadAir: {
|
|
2782
|
-
width: 820,
|
|
2783
|
-
height: 1180
|
|
2784
|
-
},
|
|
2785
|
-
iPadPro: {
|
|
2786
|
-
width: 1024,
|
|
2787
|
-
height: 1366
|
|
2788
|
-
},
|
|
2789
|
-
SurfacePro7: {
|
|
2790
|
-
width: 912,
|
|
2791
|
-
height: 1368
|
|
2792
|
-
},
|
|
2793
|
-
SurfaceDuo: {
|
|
2794
|
-
width: 540,
|
|
2795
|
-
height: 720
|
|
2796
|
-
},
|
|
2797
|
-
GalaxyZFold5: {
|
|
2798
|
-
width: 344,
|
|
2799
|
-
height: 882
|
|
2800
|
-
},
|
|
2801
|
-
AsusZenbookFold: {
|
|
2802
|
-
width: 853,
|
|
2803
|
-
height: 1280
|
|
2804
|
-
},
|
|
2805
|
-
SamsungGalaxyA51A71: {
|
|
2806
|
-
width: 412,
|
|
2807
|
-
height: 914
|
|
2808
|
-
},
|
|
2809
|
-
NestHub: {
|
|
2810
|
-
width: 1024,
|
|
2811
|
-
height: 600
|
|
2812
|
-
},
|
|
2813
|
-
NestHubMax: {
|
|
2814
|
-
width: 1280,
|
|
2815
|
-
height: 800
|
|
2816
|
-
}
|
|
2817
|
-
};
|
|
2818
|
-
const resolveBrowserViewportPreset = (presetId)=>{
|
|
2819
|
-
const size = BROWSER_VIEWPORT_PRESET_DIMENSIONS[presetId];
|
|
2820
|
-
return size ?? null;
|
|
2821
|
-
};
|
|
2822
4144
|
const SUPPORTED_PROVIDERS = [
|
|
2823
4145
|
'playwright'
|
|
2824
4146
|
];
|