@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.
Files changed (79) hide show
  1. package/LICENSE-APACHE-2.0 +202 -0
  2. package/NOTICE +11 -0
  3. package/dist/361.js +8 -0
  4. package/dist/augmentExpect.d.ts +73 -0
  5. package/dist/browser-container/container-static/css/index.5c72297783.css +1 -0
  6. package/dist/browser-container/container-static/js/{565.226c9ef5.js → 101.36a8ccdf84.js} +4024 -3856
  7. package/dist/browser-container/container-static/js/101.36a8ccdf84.js.LICENSE.txt +1 -0
  8. package/dist/browser-container/container-static/js/{index.c1d17467.js → index.0687a8142a.js} +742 -692
  9. package/dist/browser-container/container-static/js/{lib-react.97ee79b0.js → lib-react.dcf2a5e57a.js} +10 -10
  10. package/dist/browser-container/container-static/js/lib-react.dcf2a5e57a.js.LICENSE.txt +1 -0
  11. package/dist/browser-container/index.html +1 -1
  12. package/dist/browser.d.ts +2 -0
  13. package/dist/browser.js +583 -0
  14. package/dist/browserRpcRegistry.d.ts +18 -0
  15. package/dist/client/api.d.ts +3 -0
  16. package/dist/client/browserRpc.d.ts +2 -0
  17. package/dist/client/dispatchTransport.d.ts +11 -0
  18. package/dist/client/entry.d.ts +1 -5
  19. package/dist/client/locator.d.ts +125 -0
  20. package/dist/client/snapshot.d.ts +0 -6
  21. package/dist/concurrency.d.ts +12 -0
  22. package/dist/dispatchCapabilities.d.ts +34 -0
  23. package/dist/dispatchRouter.d.ts +20 -0
  24. package/dist/headlessLatestRerunScheduler.d.ts +19 -0
  25. package/dist/headlessTransport.d.ts +12 -0
  26. package/dist/index.js +1580 -258
  27. package/dist/protocol.d.ts +44 -33
  28. package/dist/providers/index.d.ts +79 -0
  29. package/dist/providers/playwright/compileLocator.d.ts +3 -0
  30. package/dist/providers/playwright/dispatchBrowserRpc.d.ts +13 -0
  31. package/dist/providers/playwright/expectUtils.d.ts +24 -0
  32. package/dist/providers/playwright/implementation.d.ts +2 -0
  33. package/dist/providers/playwright/index.d.ts +1 -0
  34. package/dist/providers/playwright/runtime.d.ts +5 -0
  35. package/dist/providers/playwright/textMatcher.d.ts +8 -0
  36. package/dist/rpcProtocol.d.ts +145 -0
  37. package/dist/runSession.d.ts +33 -0
  38. package/dist/sessionRegistry.d.ts +34 -0
  39. package/dist/sourceMap/sourceMapLoader.d.ts +14 -0
  40. package/dist/watchRerunPlanner.d.ts +21 -0
  41. package/package.json +15 -10
  42. package/src/AGENTS.md +128 -0
  43. package/src/augmentExpect.ts +62 -0
  44. package/src/browser.ts +3 -0
  45. package/src/browserRpcRegistry.ts +57 -0
  46. package/src/client/AGENTS.md +82 -0
  47. package/src/client/api.ts +213 -0
  48. package/src/client/browserRpc.ts +86 -0
  49. package/src/client/dispatchTransport.ts +178 -0
  50. package/src/client/entry.ts +96 -33
  51. package/src/client/locator.ts +452 -0
  52. package/src/client/snapshot.ts +32 -97
  53. package/src/client/sourceMapSupport.ts +26 -37
  54. package/src/concurrency.ts +62 -0
  55. package/src/dispatchCapabilities.ts +162 -0
  56. package/src/dispatchRouter.ts +82 -0
  57. package/src/env.d.ts +8 -1
  58. package/src/headlessLatestRerunScheduler.ts +76 -0
  59. package/src/headlessTransport.ts +28 -0
  60. package/src/hostController.ts +1292 -367
  61. package/src/protocol.ts +66 -31
  62. package/src/providers/index.ts +103 -0
  63. package/src/providers/playwright/compileLocator.ts +130 -0
  64. package/src/providers/playwright/dispatchBrowserRpc.ts +372 -0
  65. package/src/providers/playwright/expectUtils.ts +57 -0
  66. package/src/providers/playwright/implementation.ts +33 -0
  67. package/src/providers/playwright/index.ts +1 -0
  68. package/src/providers/playwright/runtime.ts +32 -0
  69. package/src/providers/playwright/textMatcher.ts +10 -0
  70. package/src/rpcProtocol.ts +220 -0
  71. package/src/runSession.ts +110 -0
  72. package/src/sessionRegistry.ts +89 -0
  73. package/src/sourceMap/sourceMapLoader.ts +96 -0
  74. package/src/watchRerunPlanner.ts +77 -0
  75. package/dist/browser-container/container-static/css/index.5a71c757.css +0 -1
  76. package/dist/browser-container/container-static/js/565.226c9ef5.js.LICENSE.txt +0 -1
  77. package/dist/browser-container/container-static/js/lib-react.97ee79b0.js.LICENSE.txt +0 -1
  78. package/dist/browser-container/container-static/js/scheduler.5accca0c.js +0 -407
  79. 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 firstProject = browserProjects[0];
1976
- const userPlugins = firstProject?.normalizedConfig.plugins || [];
1977
- const userRsbuildConfig = firstProject?.normalizedConfig ?? {};
1978
- const browserConfig = firstProject?.normalizedConfig.browser ?? context.normalizedConfig.browser;
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: browserConfig.port ?? 4000,
1995
- strictPort: browserConfig.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
- [firstProject?.environmentName || 'web']: {}
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.flatMap((entry)=>entry.testFiles.map((f)=>normalize(f))));
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 = firstProject?.normalizedConfig.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 || '/scheduler.html' === 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
- let browserLauncher;
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
- browser = await browserLauncher.launch({
2214
- headless: forceHeadless ?? browserConfig.headless,
2215
- args: 'chromium' === browserName ? [
2216
- '--disable-popup-blocking',
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 useSchedulerPage = browserProjects.every((project)=>project.normalizedConfig.browser.headless);
2263
- const buildErrorResult = async (error)=>{
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: async ()=>null,
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
- await cleanup?.();
2292
- return buildErrorResult(toError(error));
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 (containerDevServerEnv) try {
2298
- containerDevServer = new URL(containerDevServerEnv).toString();
2299
- logger.debug(`[Browser UI] Using dev server for container: ${containerDevServer}`);
2300
- } catch (error) {
2301
- const originalError = toError(error);
2302
- originalError.message = `Invalid RSTEST_CONTAINER_DEV_SERVER value: ${originalError.message}`;
2303
- return failWithError(originalError);
2304
- }
2305
- if (!containerDevServer) try {
2306
- containerDistPath = resolveContainerDist();
2307
- } catch (error) {
2308
- return failWithError(error);
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.flatMap((entry)=>entry.testFiles.map((testPath)=>({
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]') || text.startsWith('[Scheduler]')) logger.log(color.gray(`[Browser Console] ${text}`));
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 Promise.all(context.reporters.map((reporter)=>reporter.onTestFileStart?.({
2437
- testPath: payload.testPath,
2438
- tests: []
2439
- })));
3860
+ await handleTestFileStart(payload);
2440
3861
  },
2441
3862
  async onTestCaseResult (payload) {
2442
- caseResults.push(payload);
2443
- await Promise.all(context.reporters.map((reporter)=>reporter.onTestCaseResult?.(payload)));
3863
+ await handleTestCaseResult(payload);
2444
3864
  },
2445
3865
  async onTestFileComplete (payload) {
2446
- reporterResults.push(payload);
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
- const log = {
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
- fatalError = new Error(payload.message);
2465
- fatalError.stack = payload.stack;
3874
+ await handleFatal(payload);
2466
3875
  if (resolveAllTests) resolveAllTests();
2467
3876
  },
2468
- async resolveSnapshotPath (testPath) {
2469
- const snapExtension = '.snap';
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 = useSchedulerPage ? '/scheduler.html' : '/';
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 currentTestFiles = newProjectEntries.flatMap((entry)=>entry.testFiles.map((testPath)=>({
2533
- testPath: normalize(testPath),
2534
- projectName: entry.project.name
2535
- })));
2536
- const serialize = (files)=>JSON.stringify(files.map((f)=>`${f.projectName}:${f.testPath}`).sort());
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 (affectedFiles.length > 0) {
2545
- logger.log(color.cyan(`Re-running ${affectedFiles.length} affected test file(s)...\n`));
2546
- for (const testFile of affectedFiles)await rpcManager.reloadTestFile(testFile);
2547
- } else if (!filesChanged) logger.log(color.cyan('Tests will be re-executed automatically\n'));
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
- if (!isWatchMode) {
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) for (const reporter of context.reporters)await reporter.onTestRunEnd?.({
2574
- results: context.reporterResults.results,
2575
- testResults: context.reporterResults.testResults,
2576
- duration,
2577
- snapshotSummary: context.snapshotManager.summary,
2578
- getSourcemap: async ()=>null
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
- logger.error(color.red('Failed to load Playwright. Please install "playwright" to use browser mode.'), error);
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('__rstest_dispatch__', (message)=>{
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
  ];