@rstest/browser 0.8.3 → 0.8.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -1417,15 +1417,32 @@ __webpack_require__.add({
1417
1417
  });
1418
1418
  const TYPE_REQUEST = "q";
1419
1419
  const TYPE_RESPONSE = "s";
1420
- const DEFAULT_TIMEOUT = 6e4;
1421
- function defaultSerialize(i) {
1422
- return i;
1420
+ function createPromiseWithResolvers() {
1421
+ let resolve;
1422
+ let reject;
1423
+ return {
1424
+ promise: new Promise((res, rej)=>{
1425
+ resolve = res;
1426
+ reject = rej;
1427
+ }),
1428
+ resolve,
1429
+ reject
1430
+ };
1423
1431
  }
1432
+ const random = Math.random.bind(Math);
1433
+ const urlAlphabet = "useandom-26T198340PX75pxJACKVERYMINDBUSHWOLF_GQZbfghjklqvwyzrict";
1434
+ function nanoid(size = 21) {
1435
+ let id = "";
1436
+ let i = size;
1437
+ while(i--)id += urlAlphabet[64 * random() | 0];
1438
+ return id;
1439
+ }
1440
+ const DEFAULT_TIMEOUT = 6e4;
1441
+ const defaultSerialize = (i)=>i;
1424
1442
  const defaultDeserialize = defaultSerialize;
1425
1443
  const { clearTimeout: dist_clearTimeout, setTimeout: dist_setTimeout } = globalThis;
1426
- const random = Math.random.bind(Math);
1427
1444
  function createBirpc($functions, options) {
1428
- const { post, on, off = ()=>{}, eventNames = [], serialize = defaultSerialize, deserialize = defaultDeserialize, resolver, bind = "rpc", timeout = DEFAULT_TIMEOUT } = options;
1445
+ const { post, on, off = ()=>{}, eventNames = [], serialize = defaultSerialize, deserialize = defaultDeserialize, resolver, bind = "rpc", timeout = DEFAULT_TIMEOUT, proxify = true } = options;
1429
1446
  let $closed = false;
1430
1447
  const _rpcPromiseMap = /* @__PURE__ */ new Map();
1431
1448
  let _promiseInit;
@@ -1453,8 +1470,7 @@ function createBirpc($functions, options) {
1453
1470
  if (timeout >= 0) {
1454
1471
  timeoutId = dist_setTimeout(()=>{
1455
1472
  try {
1456
- const handleResult = options.onTimeoutError?.call(rpc, method, args);
1457
- if (true !== handleResult) throw new Error(`[birpc] timeout on calling "${method}"`);
1473
+ if (options.onTimeoutError?.call(rpc, method, args) !== true) throw new Error(`[birpc] timeout on calling "${method}"`);
1458
1474
  } catch (e) {
1459
1475
  reject(e);
1460
1476
  }
@@ -1483,15 +1499,11 @@ function createBirpc($functions, options) {
1483
1499
  }
1484
1500
  return promise;
1485
1501
  }
1486
- const $call = (method, ...args)=>_call(method, args, false);
1487
- const $callOptional = (method, ...args)=>_call(method, args, false, true);
1488
- const $callEvent = (method, ...args)=>_call(method, args, true);
1489
- const $callRaw = (options2)=>_call(options2.method, options2.args, options2.event, options2.optional);
1490
1502
  const builtinMethods = {
1491
- $call,
1492
- $callOptional,
1493
- $callEvent,
1494
- $callRaw,
1503
+ $call: (method, ...args)=>_call(method, args, false),
1504
+ $callOptional: (method, ...args)=>_call(method, args, false, true),
1505
+ $callEvent: (method, ...args)=>_call(method, args, true),
1506
+ $callRaw: (options$1)=>_call(options$1.method, options$1.args, options$1.event, options$1.optional),
1495
1507
  $rejectPendingCalls,
1496
1508
  get $closed () {
1497
1509
  return $closed;
@@ -1502,7 +1514,7 @@ function createBirpc($functions, options) {
1502
1514
  $close,
1503
1515
  $functions
1504
1516
  };
1505
- rpc = new Proxy({}, {
1517
+ rpc = proxify ? new Proxy({}, {
1506
1518
  get (_, method) {
1507
1519
  if (Object.prototype.hasOwnProperty.call(builtinMethods, method)) return builtinMethods[method];
1508
1520
  if ("then" === method && !eventNames.includes("then") && !("then" in $functions)) return;
@@ -1515,11 +1527,11 @@ function createBirpc($functions, options) {
1515
1527
  sendCall.asEvent = sendEvent;
1516
1528
  return sendCall;
1517
1529
  }
1518
- });
1530
+ }) : builtinMethods;
1519
1531
  function $close(customError) {
1520
1532
  $closed = true;
1521
1533
  _rpcPromiseMap.forEach(({ reject, method })=>{
1522
- const error = new Error(`[birpc] rpc is closed, cannot call "${method}"`);
1534
+ const error = /* @__PURE__ */ new Error(`[birpc] rpc is closed, cannot call "${method}"`);
1523
1535
  if (customError) {
1524
1536
  customError.cause ??= error;
1525
1537
  return reject(customError);
@@ -1530,9 +1542,8 @@ function createBirpc($functions, options) {
1530
1542
  off(onMessage);
1531
1543
  }
1532
1544
  function $rejectPendingCalls(handler) {
1533
- const entries = Array.from(_rpcPromiseMap.values());
1534
- const handlerResults = entries.map(({ method, reject })=>{
1535
- if (!handler) return reject(new Error(`[birpc]: rejected pending call "${method}".`));
1545
+ const handlerResults = Array.from(_rpcPromiseMap.values()).map(({ method, reject })=>{
1546
+ if (!handler) return reject(/* @__PURE__ */ new Error(`[birpc]: rejected pending call "${method}".`));
1536
1547
  return handler({
1537
1548
  method,
1538
1549
  reject
@@ -1559,9 +1570,8 @@ function createBirpc($functions, options) {
1559
1570
  } catch (e) {
1560
1571
  error = e;
1561
1572
  }
1562
- else error = new Error(`[birpc] function "${method}" not found`);
1573
+ else error = /* @__PURE__ */ new Error(`[birpc] function "${method}" not found`);
1563
1574
  if (msg.i) {
1564
- if (error && options.onError) options.onError.call(rpc, error, method, args);
1565
1575
  if (error && options.onFunctionError) {
1566
1576
  if (true === options.onFunctionError.call(rpc, error, method, args)) return;
1567
1577
  }
@@ -1600,29 +1610,11 @@ function createBirpc($functions, options) {
1600
1610
  _promiseInit = on(onMessage);
1601
1611
  return rpc;
1602
1612
  }
1603
- function createPromiseWithResolvers() {
1604
- let resolve;
1605
- let reject;
1606
- const promise = new Promise((res, rej)=>{
1607
- resolve = res;
1608
- reject = rej;
1609
- });
1610
- return {
1611
- promise,
1612
- resolve,
1613
- reject
1614
- };
1615
- }
1616
- const urlAlphabet = "useandom-26T198340PX75pxJACKVERYMINDBUSHWOLF_GQZbfghjklqvwyzrict";
1617
- function nanoid(size = 21) {
1618
- let id = "";
1619
- let i = size;
1620
- while(i--)id += urlAlphabet[64 * random() | 0];
1621
- return id;
1622
- }
1623
1613
  const picomatch = __webpack_require__("../../node_modules/.pnpm/picomatch@4.0.3/node_modules/picomatch/index.js");
1624
1614
  const { createRsbuild: createRsbuild, rspack: rspack } = rsbuild;
1625
1615
  const hostController_dirname = dirname(fileURLToPath(import.meta.url));
1616
+ const OPTIONS_PLACEHOLDER = '__RSTEST_OPTIONS_PLACEHOLDER__';
1617
+ const serializeForInlineScript = (value)=>JSON.stringify(value).replace(/</g, '\\u003c').replace(/\u2028/g, '\\u2028').replace(/\u2029/g, '\\u2029');
1626
1618
  class ContainerRpcManager {
1627
1619
  wss;
1628
1620
  ws = null;
@@ -1917,6 +1909,20 @@ const htmlTemplate = `<!DOCTYPE html>
1917
1909
  </body>
1918
1910
  </html>
1919
1911
  `;
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
+ `;
1920
1926
  const VIRTUAL_MANIFEST_FILENAME = 'virtual-manifest.ts';
1921
1927
  const destroyBrowserRuntime = async (runtime)=>{
1922
1928
  try {
@@ -1955,13 +1961,15 @@ const createBrowserRuntime = async ({ context, manifestPath, manifestSource, tem
1955
1961
  const virtualManifestPlugin = new rspack.experiments.VirtualModulesPlugin({
1956
1962
  [manifestPath]: manifestSource
1957
1963
  });
1958
- const optionsPlaceholder = '__RSTEST_OPTIONS_PLACEHOLDER__';
1959
- const containerHtmlTemplate = containerDistPath ? await promises.readFile(join(containerDistPath, 'container.html'), 'utf-8') : null;
1964
+ 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;
1960
1966
  let injectedContainerHtml = null;
1967
+ let injectedSchedulerHtml = null;
1961
1968
  let serializedOptions = 'null';
1962
1969
  const setContainerOptions = (options)=>{
1963
- serializedOptions = JSON.stringify(options).replace(/</g, '\\u003c');
1964
- if (containerHtmlTemplate) injectedContainerHtml = containerHtmlTemplate.replace(optionsPlaceholder, serializedOptions);
1970
+ serializedOptions = serializeForInlineScript(options);
1971
+ if (containerHtmlTemplate) injectedContainerHtml = containerHtmlTemplate.replace(OPTIONS_PLACEHOLDER, serializedOptions);
1972
+ injectedSchedulerHtml = (schedulerHtmlTemplate || fallbackSchedulerHtmlTemplate).replace(OPTIONS_PLACEHOLDER, serializedOptions);
1965
1973
  };
1966
1974
  const browserProjects = getBrowserProjects(context);
1967
1975
  const firstProject = browserProjects[0];
@@ -2081,7 +2089,7 @@ const createBrowserRuntime = async ({ context, manifestPath, manifestSource, tem
2081
2089
  });
2082
2090
  const serveContainer = containerDistPath ? sirv(containerDistPath, {
2083
2091
  dev: false,
2084
- single: 'container.html'
2092
+ single: 'index.html'
2085
2093
  }) : null;
2086
2094
  const containerDevBase = containerDevServer ? new URL(containerDevServer) : null;
2087
2095
  const respondWithDevServerHtml = async (url, res)=>{
@@ -2091,7 +2099,7 @@ const createBrowserRuntime = async ({ context, manifestPath, manifestSource, tem
2091
2099
  const response = await fetch(target);
2092
2100
  if (!response.ok) return false;
2093
2101
  let html = await response.text();
2094
- html = html.replace(optionsPlaceholder, serializedOptions);
2102
+ html = html.replace(OPTIONS_PLACEHOLDER, serializedOptions);
2095
2103
  res.statusCode = response.status;
2096
2104
  response.headers.forEach((value, key)=>{
2097
2105
  if ('content-length' === key.toLowerCase()) return;
@@ -2149,9 +2157,14 @@ const createBrowserRuntime = async ({ context, manifestPath, manifestSource, tem
2149
2157
  }
2150
2158
  return;
2151
2159
  }
2152
- if ('/' === url.pathname) {
2160
+ if ('/' === url.pathname || '/scheduler.html' === url.pathname) {
2153
2161
  if (await respondWithDevServerHtml(url, res)) return;
2154
- const html = injectedContainerHtml || containerHtmlTemplate?.replace(optionsPlaceholder, 'null');
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
+ const html = injectedContainerHtml || containerHtmlTemplate?.replace(OPTIONS_PLACEHOLDER, 'null');
2155
2168
  if (html) {
2156
2169
  res.setHeader('Content-Type', 'text/html');
2157
2170
  res.end(html);
@@ -2245,6 +2258,39 @@ async function resolveProjectEntries(context, shardedEntries) {
2245
2258
  const runBrowserController = async (context, options)=>{
2246
2259
  const { skipOnTestRunEnd = false } = options ?? {};
2247
2260
  const buildStart = Date.now();
2261
+ const browserProjects = getBrowserProjects(context);
2262
+ const useSchedulerPage = browserProjects.every((project)=>project.normalizedConfig.browser.headless);
2263
+ const buildErrorResult = async (error)=>{
2264
+ const elapsed = Math.max(0, Date.now() - buildStart);
2265
+ const errorResult = {
2266
+ results: [],
2267
+ testResults: [],
2268
+ duration: {
2269
+ totalTime: elapsed,
2270
+ buildTime: elapsed,
2271
+ testTime: 0
2272
+ },
2273
+ hasFailure: true,
2274
+ unhandledErrors: [
2275
+ error
2276
+ ]
2277
+ };
2278
+ if (!skipOnTestRunEnd) for (const reporter of context.reporters)await reporter.onTestRunEnd?.({
2279
+ results: [],
2280
+ testResults: [],
2281
+ duration: errorResult.duration,
2282
+ snapshotSummary: context.snapshotManager.summary,
2283
+ getSourcemap: async ()=>null,
2284
+ unhandledErrors: errorResult.unhandledErrors
2285
+ });
2286
+ return errorResult;
2287
+ };
2288
+ const toError = (error)=>error instanceof Error ? error : new Error(String(error));
2289
+ const failWithError = async (error, cleanup)=>{
2290
+ ensureProcessExitCode(1);
2291
+ await cleanup?.();
2292
+ return buildErrorResult(toError(error));
2293
+ };
2248
2294
  const containerDevServerEnv = process.env.RSTEST_CONTAINER_DEV_SERVER;
2249
2295
  let containerDevServer;
2250
2296
  let containerDistPath;
@@ -2252,16 +2298,14 @@ const runBrowserController = async (context, options)=>{
2252
2298
  containerDevServer = new URL(containerDevServerEnv).toString();
2253
2299
  logger.debug(`[Browser UI] Using dev server for container: ${containerDevServer}`);
2254
2300
  } catch (error) {
2255
- logger.error(color.red(`Invalid RSTEST_CONTAINER_DEV_SERVER value: ${String(error)}`));
2256
- ensureProcessExitCode(1);
2257
- return;
2301
+ const originalError = toError(error);
2302
+ originalError.message = `Invalid RSTEST_CONTAINER_DEV_SERVER value: ${originalError.message}`;
2303
+ return failWithError(originalError);
2258
2304
  }
2259
2305
  if (!containerDevServer) try {
2260
2306
  containerDistPath = resolveContainerDist();
2261
2307
  } catch (error) {
2262
- logger.error(color.red(String(error)));
2263
- ensureProcessExitCode(1);
2264
- return;
2308
+ return failWithError(error);
2265
2309
  }
2266
2310
  const projectEntries = await resolveProjectEntries(context, options?.shardedEntries);
2267
2311
  const totalTests = projectEntries.reduce((total, item)=>total + item.testFiles.length, 0);
@@ -2303,13 +2347,12 @@ const runBrowserController = async (context, options)=>{
2303
2347
  containerDevServer
2304
2348
  });
2305
2349
  } catch (error) {
2306
- logger.error(error instanceof Error ? error : new Error(String(error)));
2307
- ensureProcessExitCode(1);
2308
- await promises.rm(tempDir, {
2309
- recursive: true,
2310
- force: true
2311
- }).catch(()=>{});
2312
- return;
2350
+ return failWithError(error, async ()=>{
2351
+ await promises.rm(tempDir, {
2352
+ recursive: true,
2353
+ force: true
2354
+ }).catch(()=>{});
2355
+ });
2313
2356
  }
2314
2357
  if (isWatchMode) {
2315
2358
  watchContext.runtime = runtime;
@@ -2322,14 +2365,14 @@ const runBrowserController = async (context, options)=>{
2322
2365
  testPath: normalize(testPath),
2323
2366
  projectName: entry.project.name
2324
2367
  })));
2325
- const browserProjectsForRuntime = getBrowserProjects(context);
2326
- const projectRuntimeConfigs = browserProjectsForRuntime.map((project)=>({
2368
+ const projectRuntimeConfigs = browserProjects.map((project)=>({
2327
2369
  name: project.name,
2328
2370
  environmentName: project.environmentName,
2329
2371
  projectRoot: normalize(project.rootPath),
2330
- runtimeConfig: serializableConfig(getRuntimeConfigFromProject(project))
2372
+ runtimeConfig: serializableConfig(getRuntimeConfigFromProject(project)),
2373
+ viewport: project.normalizedConfig.browser.viewport
2331
2374
  }));
2332
- const maxTestTimeoutForRpc = Math.max(...browserProjectsForRuntime.map((p)=>p.normalizedConfig.testTimeout ?? 5000));
2375
+ const maxTestTimeoutForRpc = Math.max(...browserProjects.map((p)=>p.normalizedConfig.testTimeout ?? 5000));
2333
2376
  const hostOptions = {
2334
2377
  rootPath: normalize(context.rootPath),
2335
2378
  projects: projectRuntimeConfigs,
@@ -2375,7 +2418,7 @@ const runBrowserController = async (context, options)=>{
2375
2418
  }
2376
2419
  containerPage.on('console', (msg)=>{
2377
2420
  const text = msg.text();
2378
- if (text.includes('[Container]') || text.includes('[Runner]')) logger.log(color.gray(`[Browser Console] ${text}`));
2421
+ if (text.startsWith('[Container]') || text.startsWith('[Runner]') || text.startsWith('[Scheduler]')) logger.log(color.gray(`[Browser Console] ${text}`));
2379
2422
  });
2380
2423
  }
2381
2424
  const createRpcMethods = ()=>({
@@ -2458,12 +2501,17 @@ const runBrowserController = async (context, options)=>{
2458
2501
  if (isWatchMode) runtime.rpcManager = rpcManager;
2459
2502
  }
2460
2503
  if (isNewPage) {
2461
- await containerPage.goto(`http://localhost:${port}/`, {
2504
+ const pagePath = useSchedulerPage ? '/scheduler.html' : '/';
2505
+ if (useSchedulerPage) {
2506
+ const serializedOptions = serializeForInlineScript(hostOptions);
2507
+ await containerPage.addInitScript(`window.__RSTEST_BROWSER_OPTIONS__ = ${serializedOptions};`);
2508
+ }
2509
+ await containerPage.goto(`http://localhost:${port}${pagePath}`, {
2462
2510
  waitUntil: 'load'
2463
2511
  });
2464
- logger.log(color.cyan(`\nBrowser mode opened at http://localhost:${port}/\n`));
2512
+ logger.log(color.cyan(`\nBrowser mode opened at http://localhost:${port}${pagePath}\n`));
2465
2513
  }
2466
- const maxTestTimeout = Math.max(...browserProjectsForRuntime.map((p)=>p.normalizedConfig.testTimeout ?? 5000));
2514
+ const maxTestTimeout = Math.max(...browserProjects.map((p)=>p.normalizedConfig.testTimeout ?? 5000));
2467
2515
  const totalTimeoutMs = maxTestTimeout * allTestFiles.length + 30000;
2468
2516
  let timeoutId;
2469
2517
  const testTimeout = new Promise((resolve)=>{
@@ -2498,12 +2546,16 @@ const runBrowserController = async (context, options)=>{
2498
2546
  for (const testFile of affectedFiles)await rpcManager.reloadTestFile(testFile);
2499
2547
  } else if (!filesChanged) logger.log(color.cyan('Tests will be re-executed automatically\n'));
2500
2548
  };
2501
- if (!isWatchMode) await destroyBrowserRuntime(runtime);
2502
- if (fatalError) {
2503
- logger.error(color.red(`Browser test run failed: ${fatalError.message}`));
2504
- ensureProcessExitCode(1);
2505
- return;
2549
+ if (!isWatchMode) {
2550
+ try {
2551
+ await containerPage.close();
2552
+ } catch {}
2553
+ try {
2554
+ await containerContext.close();
2555
+ } catch {}
2556
+ await destroyBrowserRuntime(runtime);
2506
2557
  }
2558
+ if (fatalError) return failWithError(fatalError);
2507
2559
  const duration = {
2508
2560
  totalTime: buildTime + testTime,
2509
2561
  buildTime,
@@ -2566,7 +2618,8 @@ const listBrowserTests = async (context, options)=>{
2566
2618
  name: project.name,
2567
2619
  environmentName: project.environmentName,
2568
2620
  projectRoot: normalize(project.rootPath),
2569
- runtimeConfig: serializableConfig(getRuntimeConfigFromProject(project))
2621
+ runtimeConfig: serializableConfig(getRuntimeConfigFromProject(project)),
2622
+ viewport: project.normalizedConfig.browser.viewport
2570
2623
  }));
2571
2624
  const maxTestTimeoutForRpc = Math.max(...browserProjects.map((p)=>p.normalizedConfig.testTimeout ?? 5000));
2572
2625
  const hostOptions = {
@@ -2622,7 +2675,7 @@ const listBrowserTests = async (context, options)=>{
2622
2675
  logger.debug(`[List] Unexpected message: ${message.type}`);
2623
2676
  }
2624
2677
  });
2625
- const serializedOptions = JSON.stringify(hostOptions).replace(/</g, '\\u003c');
2678
+ const serializedOptions = serializeForInlineScript(hostOptions);
2626
2679
  await page.addInitScript(`window.__RSTEST_BROWSER_OPTIONS__ = ${serializedOptions};`);
2627
2680
  await page.goto(`http://localhost:${port}/runner.html`, {
2628
2681
  waitUntil: 'load'
@@ -2673,10 +2726,134 @@ const listBrowserTests = async (context, options)=>{
2673
2726
  close: cleanup
2674
2727
  };
2675
2728
  };
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
+ const SUPPORTED_PROVIDERS = [
2823
+ 'playwright'
2824
+ ];
2825
+ const isPlainObject = (value)=>'[object Object]' === Object.prototype.toString.call(value);
2826
+ const validateViewport = (viewport)=>{
2827
+ if (null == viewport) return;
2828
+ if ('string' == typeof viewport) {
2829
+ const presetId = viewport.trim();
2830
+ if (!presetId) throw new Error('browser.viewport must be a non-empty preset id.');
2831
+ if (!resolveBrowserViewportPreset(presetId)) throw new Error(`browser.viewport must be a valid preset id. Received: ${viewport}`);
2832
+ return;
2833
+ }
2834
+ if (isPlainObject(viewport)) {
2835
+ const width = viewport.width;
2836
+ const height = viewport.height;
2837
+ if (!Number.isFinite(width) || width <= 0) throw new Error('browser.viewport.width must be a positive number.');
2838
+ if (!Number.isFinite(height) || height <= 0) throw new Error('browser.viewport.height must be a positive number.');
2839
+ return;
2840
+ }
2841
+ throw new Error('browser.viewport must be either a preset id or { width, height }.');
2842
+ };
2843
+ const validateBrowserConfig = (context)=>{
2844
+ for (const project of context.projects){
2845
+ const browser = project.normalizedConfig.browser;
2846
+ if (browser.enabled) {
2847
+ if (!browser.provider) throw new Error('browser.provider is required when browser.enabled is true.');
2848
+ if (!SUPPORTED_PROVIDERS.includes(browser.provider)) throw new Error(`browser.provider must be one of: ${SUPPORTED_PROVIDERS.join(', ')}.`);
2849
+ validateViewport(browser.viewport);
2850
+ }
2851
+ }
2852
+ };
2676
2853
  async function runBrowserTests(context, options) {
2677
2854
  return runBrowserController(context, options);
2678
2855
  }
2679
2856
  async function src_listBrowserTests(context) {
2680
2857
  return listBrowserTests(context);
2681
2858
  }
2682
- export { runBrowserTests, src_listBrowserTests as listBrowserTests };
2859
+ export { BROWSER_VIEWPORT_PRESET_DIMENSIONS, BROWSER_VIEWPORT_PRESET_IDS, resolveBrowserViewportPreset, runBrowserTests, src_listBrowserTests as listBrowserTests, validateBrowserConfig };
@@ -1,11 +1,17 @@
1
+ import type { DevicePreset } from '@rstest/core/browser';
1
2
  import type { RuntimeConfig, Test, TestFileResult, TestResult } from '@rstest/core/browser-runtime';
2
3
  import type { SnapshotUpdateState } from '@vitest/snapshot';
3
4
  export type SerializedRuntimeConfig = RuntimeConfig;
5
+ export type BrowserViewport = {
6
+ width: number;
7
+ height: number;
8
+ } | DevicePreset;
4
9
  export type BrowserProjectRuntime = {
5
10
  name: string;
6
11
  environmentName: string;
7
12
  projectRoot: string;
8
13
  runtimeConfig: SerializedRuntimeConfig;
14
+ viewport?: BrowserViewport;
9
15
  };
10
16
  /**
11
17
  * Test file info with associated project name.
@@ -0,0 +1,16 @@
1
+ /**
2
+ * Runtime source of truth for browser viewport presets.
3
+ *
4
+ * IMPORTANT: Keep this list/map in sync with `DevicePreset` typing in
5
+ * `@rstest/core` (`packages/core/src/types/config.ts`) so `defineConfig`
6
+ * autocomplete and runtime validation stay consistent.
7
+ */
8
+ export declare const BROWSER_VIEWPORT_PRESET_IDS: readonly ["iPhoneSE", "iPhoneXR", "iPhone12Pro", "iPhone14ProMax", "Pixel7", "SamsungGalaxyS8Plus", "SamsungGalaxyS20Ultra", "iPadMini", "iPadAir", "iPadPro", "SurfacePro7", "SurfaceDuo", "GalaxyZFold5", "AsusZenbookFold", "SamsungGalaxyA51A71", "NestHub", "NestHubMax"];
9
+ type BrowserViewportPresetId = (typeof BROWSER_VIEWPORT_PRESET_IDS)[number];
10
+ type BrowserViewportSize = {
11
+ width: number;
12
+ height: number;
13
+ };
14
+ export declare const BROWSER_VIEWPORT_PRESET_DIMENSIONS: Record<BrowserViewportPresetId, BrowserViewportSize>;
15
+ export declare const resolveBrowserViewportPreset: (presetId: string) => BrowserViewportSize | null;
16
+ export {};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rstest/browser",
3
- "version": "0.8.3",
3
+ "version": "0.8.5",
4
4
  "description": "Browser mode support for Rstest testing framework.",
5
5
  "bugs": {
6
6
  "url": "https://github.com/web-infra-dev/rstest/issues"
@@ -48,17 +48,17 @@
48
48
  "@types/picomatch": "^4.0.2",
49
49
  "@types/ws": "^8.18.1",
50
50
  "@vitest/snapshot": "^3.2.4",
51
- "birpc": "2.9.0",
51
+ "birpc": "^4.0.0",
52
52
  "picocolors": "^1.1.1",
53
53
  "picomatch": "^4.0.3",
54
54
  "playwright": "^1.49.1",
55
55
  "@rstest/browser-ui": "0.0.0",
56
- "@rstest/core": "0.8.3",
57
- "@rstest/tsconfig": "0.0.1"
56
+ "@rstest/tsconfig": "0.0.1",
57
+ "@rstest/core": "0.8.5"
58
58
  },
59
59
  "peerDependencies": {
60
60
  "playwright": "^1.49.1",
61
- "@rstest/core": "^0.8.3"
61
+ "@rstest/core": "^0.8.5"
62
62
  },
63
63
  "peerDependenciesMeta": {
64
64
  "playwright": {
@@ -426,12 +426,13 @@ const run = async () => {
426
426
  return;
427
427
  }
428
428
 
429
- // 1. Load setup files for this project
430
- for (const loadSetup of currentSetupLoaders) {
431
- await loadSetup();
432
- }
429
+ const loadSetupFiles = async (): Promise<void> => {
430
+ for (const loadSetup of currentSetupLoaders) {
431
+ await loadSetup();
432
+ }
433
+ };
433
434
 
434
- // 2. Determine which test files to run
435
+ // 1. Determine which test files to run
435
436
  let testKeysToRun: string[];
436
437
 
437
438
  if (targetTestFile) {
@@ -478,6 +479,9 @@ const run = async () => {
478
479
  }
479
480
 
480
481
  try {
482
+ // Load setup files for this project after runtime is ready.
483
+ await loadSetupFiles();
484
+
481
485
  // Load the test file dynamically (registers tests without running)
482
486
  await currentTestContext.loadTest(key);
483
487
 
@@ -512,7 +516,7 @@ const run = async () => {
512
516
  return;
513
517
  }
514
518
 
515
- // 3. Run tests for each file
519
+ // 2. Run tests for each file
516
520
  for (const key of testKeysToRun) {
517
521
  const testPath = toAbsolutePath(key, currentProject.projectRoot);
518
522
 
@@ -573,6 +577,9 @@ const run = async () => {
573
577
  });
574
578
 
575
579
  try {
580
+ // Load setup files for this project after runtime is ready.
581
+ await loadSetupFiles();
582
+
576
583
  // Record script URLs before loading the test file
577
584
  const beforeScripts = getScriptUrls();
578
585