@rstest/browser 0.8.2 → 0.8.4

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
@@ -1623,6 +1623,8 @@ function nanoid(size = 21) {
1623
1623
  const picomatch = __webpack_require__("../../node_modules/.pnpm/picomatch@4.0.3/node_modules/picomatch/index.js");
1624
1624
  const { createRsbuild: createRsbuild, rspack: rspack } = rsbuild;
1625
1625
  const hostController_dirname = dirname(fileURLToPath(import.meta.url));
1626
+ const OPTIONS_PLACEHOLDER = '__RSTEST_OPTIONS_PLACEHOLDER__';
1627
+ const serializeForInlineScript = (value)=>JSON.stringify(value).replace(/</g, '\\u003c').replace(/\u2028/g, '\\u2028').replace(/\u2029/g, '\\u2029');
1626
1628
  class ContainerRpcManager {
1627
1629
  wss;
1628
1630
  ws = null;
@@ -1917,6 +1919,21 @@ const htmlTemplate = `<!DOCTYPE html>
1917
1919
  </body>
1918
1920
  </html>
1919
1921
  `;
1922
+ const fallbackSchedulerHtmlTemplate = `<!DOCTYPE html>
1923
+ <html lang="en">
1924
+ <head>
1925
+ <meta charset="UTF-8" />
1926
+ <title>Rstest Browser Scheduler</title>
1927
+ <script>
1928
+ window.__RSTEST_BROWSER_OPTIONS__ = ${OPTIONS_PLACEHOLDER};
1929
+ </script>
1930
+ </head>
1931
+ <body>
1932
+ <script type="module" src="/container-static/js/scheduler.js"></script>
1933
+ </body>
1934
+ </html>
1935
+ `;
1936
+ const VIRTUAL_MANIFEST_FILENAME = 'virtual-manifest.ts';
1920
1937
  const destroyBrowserRuntime = async (runtime)=>{
1921
1938
  try {
1922
1939
  await runtime.browser?.close?.();
@@ -1954,13 +1971,15 @@ const createBrowserRuntime = async ({ context, manifestPath, manifestSource, tem
1954
1971
  const virtualManifestPlugin = new rspack.experiments.VirtualModulesPlugin({
1955
1972
  [manifestPath]: manifestSource
1956
1973
  });
1957
- const optionsPlaceholder = '__RSTEST_OPTIONS_PLACEHOLDER__';
1958
- const containerHtmlTemplate = containerDistPath ? await promises.readFile(join(containerDistPath, 'container.html'), 'utf-8') : null;
1974
+ const containerHtmlTemplate = containerDistPath ? await promises.readFile(join(containerDistPath, 'index.html'), 'utf-8') : null;
1975
+ const schedulerHtmlTemplate = containerDistPath ? await promises.readFile(join(containerDistPath, 'scheduler.html'), 'utf-8').catch(()=>null) : null;
1959
1976
  let injectedContainerHtml = null;
1977
+ let injectedSchedulerHtml = null;
1960
1978
  let serializedOptions = 'null';
1961
1979
  const setContainerOptions = (options)=>{
1962
- serializedOptions = JSON.stringify(options).replace(/</g, '\\u003c');
1963
- if (containerHtmlTemplate) injectedContainerHtml = containerHtmlTemplate.replace(optionsPlaceholder, serializedOptions);
1980
+ serializedOptions = serializeForInlineScript(options);
1981
+ if (containerHtmlTemplate) injectedContainerHtml = containerHtmlTemplate.replace(OPTIONS_PLACEHOLDER, serializedOptions);
1982
+ injectedSchedulerHtml = (schedulerHtmlTemplate || fallbackSchedulerHtmlTemplate).replace(OPTIONS_PLACEHOLDER, serializedOptions);
1964
1983
  };
1965
1984
  const browserProjects = getBrowserProjects(context);
1966
1985
  const firstProject = browserProjects[0];
@@ -2080,7 +2099,7 @@ const createBrowserRuntime = async ({ context, manifestPath, manifestSource, tem
2080
2099
  });
2081
2100
  const serveContainer = containerDistPath ? sirv(containerDistPath, {
2082
2101
  dev: false,
2083
- single: 'container.html'
2102
+ single: 'index.html'
2084
2103
  }) : null;
2085
2104
  const containerDevBase = containerDevServer ? new URL(containerDevServer) : null;
2086
2105
  const respondWithDevServerHtml = async (url, res)=>{
@@ -2090,7 +2109,7 @@ const createBrowserRuntime = async ({ context, manifestPath, manifestSource, tem
2090
2109
  const response = await fetch(target);
2091
2110
  if (!response.ok) return false;
2092
2111
  let html = await response.text();
2093
- html = html.replace(optionsPlaceholder, serializedOptions);
2112
+ html = html.replace(OPTIONS_PLACEHOLDER, serializedOptions);
2094
2113
  res.statusCode = response.status;
2095
2114
  response.headers.forEach((value, key)=>{
2096
2115
  if ('content-length' === key.toLowerCase()) return;
@@ -2148,9 +2167,14 @@ const createBrowserRuntime = async ({ context, manifestPath, manifestSource, tem
2148
2167
  }
2149
2168
  return;
2150
2169
  }
2151
- if ('/' === url.pathname) {
2170
+ if ('/' === url.pathname || '/scheduler.html' === url.pathname) {
2152
2171
  if (await respondWithDevServerHtml(url, res)) return;
2153
- const html = injectedContainerHtml || containerHtmlTemplate?.replace(optionsPlaceholder, 'null');
2172
+ if ('/scheduler.html' === url.pathname) {
2173
+ res.setHeader('Content-Type', 'text/html');
2174
+ res.end(injectedSchedulerHtml || (schedulerHtmlTemplate || fallbackSchedulerHtmlTemplate).replace(OPTIONS_PLACEHOLDER, 'null'));
2175
+ return;
2176
+ }
2177
+ const html = injectedContainerHtml || containerHtmlTemplate?.replace(OPTIONS_PLACEHOLDER, 'null');
2154
2178
  if (html) {
2155
2179
  res.setHeader('Content-Type', 'text/html');
2156
2180
  res.end(html);
@@ -2244,6 +2268,39 @@ async function resolveProjectEntries(context, shardedEntries) {
2244
2268
  const runBrowserController = async (context, options)=>{
2245
2269
  const { skipOnTestRunEnd = false } = options ?? {};
2246
2270
  const buildStart = Date.now();
2271
+ const browserProjects = getBrowserProjects(context);
2272
+ const useSchedulerPage = browserProjects.every((project)=>project.normalizedConfig.browser.headless);
2273
+ const buildErrorResult = async (error)=>{
2274
+ const elapsed = Math.max(0, Date.now() - buildStart);
2275
+ const errorResult = {
2276
+ results: [],
2277
+ testResults: [],
2278
+ duration: {
2279
+ totalTime: elapsed,
2280
+ buildTime: elapsed,
2281
+ testTime: 0
2282
+ },
2283
+ hasFailure: true,
2284
+ unhandledErrors: [
2285
+ error
2286
+ ]
2287
+ };
2288
+ if (!skipOnTestRunEnd) for (const reporter of context.reporters)await reporter.onTestRunEnd?.({
2289
+ results: [],
2290
+ testResults: [],
2291
+ duration: errorResult.duration,
2292
+ snapshotSummary: context.snapshotManager.summary,
2293
+ getSourcemap: async ()=>null,
2294
+ unhandledErrors: errorResult.unhandledErrors
2295
+ });
2296
+ return errorResult;
2297
+ };
2298
+ const toError = (error)=>error instanceof Error ? error : new Error(String(error));
2299
+ const failWithError = async (error, cleanup)=>{
2300
+ ensureProcessExitCode(1);
2301
+ await cleanup?.();
2302
+ return buildErrorResult(toError(error));
2303
+ };
2247
2304
  const containerDevServerEnv = process.env.RSTEST_CONTAINER_DEV_SERVER;
2248
2305
  let containerDevServer;
2249
2306
  let containerDistPath;
@@ -2251,16 +2308,14 @@ const runBrowserController = async (context, options)=>{
2251
2308
  containerDevServer = new URL(containerDevServerEnv).toString();
2252
2309
  logger.debug(`[Browser UI] Using dev server for container: ${containerDevServer}`);
2253
2310
  } catch (error) {
2254
- logger.error(color.red(`Invalid RSTEST_CONTAINER_DEV_SERVER value: ${String(error)}`));
2255
- ensureProcessExitCode(1);
2256
- return;
2311
+ const originalError = toError(error);
2312
+ originalError.message = `Invalid RSTEST_CONTAINER_DEV_SERVER value: ${originalError.message}`;
2313
+ return failWithError(originalError);
2257
2314
  }
2258
2315
  if (!containerDevServer) try {
2259
2316
  containerDistPath = resolveContainerDist();
2260
2317
  } catch (error) {
2261
- logger.error(color.red(String(error)));
2262
- ensureProcessExitCode(1);
2263
- return;
2318
+ return failWithError(error);
2264
2319
  }
2265
2320
  const projectEntries = await resolveProjectEntries(context, options?.shardedEntries);
2266
2321
  const totalTests = projectEntries.reduce((total, item)=>total + item.testFiles.length, 0);
@@ -2276,7 +2331,7 @@ const runBrowserController = async (context, options)=>{
2276
2331
  }
2277
2332
  const isWatchMode = 'watch' === context.command;
2278
2333
  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());
2279
- const manifestPath = join(tempDir, 'manifest.ts');
2334
+ const manifestPath = join(tempDir, VIRTUAL_MANIFEST_FILENAME);
2280
2335
  const manifestSource = generateManifestModule({
2281
2336
  manifestPath,
2282
2337
  entries: projectEntries
@@ -2302,13 +2357,12 @@ const runBrowserController = async (context, options)=>{
2302
2357
  containerDevServer
2303
2358
  });
2304
2359
  } catch (error) {
2305
- logger.error(error instanceof Error ? error : new Error(String(error)));
2306
- ensureProcessExitCode(1);
2307
- await promises.rm(tempDir, {
2308
- recursive: true,
2309
- force: true
2310
- }).catch(()=>{});
2311
- return;
2360
+ return failWithError(error, async ()=>{
2361
+ await promises.rm(tempDir, {
2362
+ recursive: true,
2363
+ force: true
2364
+ }).catch(()=>{});
2365
+ });
2312
2366
  }
2313
2367
  if (isWatchMode) {
2314
2368
  watchContext.runtime = runtime;
@@ -2321,14 +2375,14 @@ const runBrowserController = async (context, options)=>{
2321
2375
  testPath: normalize(testPath),
2322
2376
  projectName: entry.project.name
2323
2377
  })));
2324
- const browserProjectsForRuntime = getBrowserProjects(context);
2325
- const projectRuntimeConfigs = browserProjectsForRuntime.map((project)=>({
2378
+ const projectRuntimeConfigs = browserProjects.map((project)=>({
2326
2379
  name: project.name,
2327
2380
  environmentName: project.environmentName,
2328
2381
  projectRoot: normalize(project.rootPath),
2329
- runtimeConfig: serializableConfig(getRuntimeConfigFromProject(project))
2382
+ runtimeConfig: serializableConfig(getRuntimeConfigFromProject(project)),
2383
+ viewport: project.normalizedConfig.browser.viewport
2330
2384
  }));
2331
- const maxTestTimeoutForRpc = Math.max(...browserProjectsForRuntime.map((p)=>p.normalizedConfig.testTimeout ?? 5000));
2385
+ const maxTestTimeoutForRpc = Math.max(...browserProjects.map((p)=>p.normalizedConfig.testTimeout ?? 5000));
2332
2386
  const hostOptions = {
2333
2387
  rootPath: normalize(context.rootPath),
2334
2388
  projects: projectRuntimeConfigs,
@@ -2374,7 +2428,7 @@ const runBrowserController = async (context, options)=>{
2374
2428
  }
2375
2429
  containerPage.on('console', (msg)=>{
2376
2430
  const text = msg.text();
2377
- if (text.includes('[Container]') || text.includes('[Runner]')) logger.log(color.gray(`[Browser Console] ${text}`));
2431
+ if (text.startsWith('[Container]') || text.startsWith('[Runner]') || text.startsWith('[Scheduler]')) logger.log(color.gray(`[Browser Console] ${text}`));
2378
2432
  });
2379
2433
  }
2380
2434
  const createRpcMethods = ()=>({
@@ -2457,12 +2511,17 @@ const runBrowserController = async (context, options)=>{
2457
2511
  if (isWatchMode) runtime.rpcManager = rpcManager;
2458
2512
  }
2459
2513
  if (isNewPage) {
2460
- await containerPage.goto(`http://localhost:${port}/`, {
2514
+ const pagePath = useSchedulerPage ? '/scheduler.html' : '/';
2515
+ if (useSchedulerPage) {
2516
+ const serializedOptions = serializeForInlineScript(hostOptions);
2517
+ await containerPage.addInitScript(`window.__RSTEST_BROWSER_OPTIONS__ = ${serializedOptions};`);
2518
+ }
2519
+ await containerPage.goto(`http://localhost:${port}${pagePath}`, {
2461
2520
  waitUntil: 'load'
2462
2521
  });
2463
- logger.log(color.cyan(`\nBrowser mode opened at http://localhost:${port}/\n`));
2522
+ logger.log(color.cyan(`\nBrowser mode opened at http://localhost:${port}${pagePath}\n`));
2464
2523
  }
2465
- const maxTestTimeout = Math.max(...browserProjectsForRuntime.map((p)=>p.normalizedConfig.testTimeout ?? 5000));
2524
+ const maxTestTimeout = Math.max(...browserProjects.map((p)=>p.normalizedConfig.testTimeout ?? 5000));
2466
2525
  const totalTimeoutMs = maxTestTimeout * allTestFiles.length + 30000;
2467
2526
  let timeoutId;
2468
2527
  const testTimeout = new Promise((resolve)=>{
@@ -2497,12 +2556,16 @@ const runBrowserController = async (context, options)=>{
2497
2556
  for (const testFile of affectedFiles)await rpcManager.reloadTestFile(testFile);
2498
2557
  } else if (!filesChanged) logger.log(color.cyan('Tests will be re-executed automatically\n'));
2499
2558
  };
2500
- if (!isWatchMode) await destroyBrowserRuntime(runtime);
2501
- if (fatalError) {
2502
- logger.error(color.red(`Browser test run failed: ${fatalError.message}`));
2503
- ensureProcessExitCode(1);
2504
- return;
2559
+ if (!isWatchMode) {
2560
+ try {
2561
+ await containerPage.close();
2562
+ } catch {}
2563
+ try {
2564
+ await containerContext.close();
2565
+ } catch {}
2566
+ await destroyBrowserRuntime(runtime);
2505
2567
  }
2568
+ if (fatalError) return failWithError(fatalError);
2506
2569
  const duration = {
2507
2570
  totalTime: buildTime + testTime,
2508
2571
  buildTime,
@@ -2538,7 +2601,7 @@ const listBrowserTests = async (context, options)=>{
2538
2601
  close: async ()=>{}
2539
2602
  };
2540
2603
  const tempDir = join(context.rootPath, TEMP_RSTEST_OUTPUT_DIR, 'browser', `list-${Date.now()}`);
2541
- const manifestPath = join(tempDir, 'manifest.ts');
2604
+ const manifestPath = join(tempDir, VIRTUAL_MANIFEST_FILENAME);
2542
2605
  const manifestSource = generateManifestModule({
2543
2606
  manifestPath,
2544
2607
  entries: projectEntries
@@ -2565,7 +2628,8 @@ const listBrowserTests = async (context, options)=>{
2565
2628
  name: project.name,
2566
2629
  environmentName: project.environmentName,
2567
2630
  projectRoot: normalize(project.rootPath),
2568
- runtimeConfig: serializableConfig(getRuntimeConfigFromProject(project))
2631
+ runtimeConfig: serializableConfig(getRuntimeConfigFromProject(project)),
2632
+ viewport: project.normalizedConfig.browser.viewport
2569
2633
  }));
2570
2634
  const maxTestTimeoutForRpc = Math.max(...browserProjects.map((p)=>p.normalizedConfig.testTimeout ?? 5000));
2571
2635
  const hostOptions = {
@@ -2621,7 +2685,7 @@ const listBrowserTests = async (context, options)=>{
2621
2685
  logger.debug(`[List] Unexpected message: ${message.type}`);
2622
2686
  }
2623
2687
  });
2624
- const serializedOptions = JSON.stringify(hostOptions).replace(/</g, '\\u003c');
2688
+ const serializedOptions = serializeForInlineScript(hostOptions);
2625
2689
  await page.addInitScript(`window.__RSTEST_BROWSER_OPTIONS__ = ${serializedOptions};`);
2626
2690
  await page.goto(`http://localhost:${port}/runner.html`, {
2627
2691
  waitUntil: 'load'
@@ -2672,10 +2736,134 @@ const listBrowserTests = async (context, options)=>{
2672
2736
  close: cleanup
2673
2737
  };
2674
2738
  };
2739
+ const BROWSER_VIEWPORT_PRESET_IDS = [
2740
+ 'iPhoneSE',
2741
+ 'iPhoneXR',
2742
+ 'iPhone12Pro',
2743
+ 'iPhone14ProMax',
2744
+ 'Pixel7',
2745
+ 'SamsungGalaxyS8Plus',
2746
+ 'SamsungGalaxyS20Ultra',
2747
+ 'iPadMini',
2748
+ 'iPadAir',
2749
+ 'iPadPro',
2750
+ 'SurfacePro7',
2751
+ 'SurfaceDuo',
2752
+ 'GalaxyZFold5',
2753
+ 'AsusZenbookFold',
2754
+ 'SamsungGalaxyA51A71',
2755
+ 'NestHub',
2756
+ 'NestHubMax'
2757
+ ];
2758
+ const BROWSER_VIEWPORT_PRESET_DIMENSIONS = {
2759
+ iPhoneSE: {
2760
+ width: 375,
2761
+ height: 667
2762
+ },
2763
+ iPhoneXR: {
2764
+ width: 414,
2765
+ height: 896
2766
+ },
2767
+ iPhone12Pro: {
2768
+ width: 390,
2769
+ height: 844
2770
+ },
2771
+ iPhone14ProMax: {
2772
+ width: 430,
2773
+ height: 932
2774
+ },
2775
+ Pixel7: {
2776
+ width: 412,
2777
+ height: 915
2778
+ },
2779
+ SamsungGalaxyS8Plus: {
2780
+ width: 360,
2781
+ height: 740
2782
+ },
2783
+ SamsungGalaxyS20Ultra: {
2784
+ width: 412,
2785
+ height: 915
2786
+ },
2787
+ iPadMini: {
2788
+ width: 768,
2789
+ height: 1024
2790
+ },
2791
+ iPadAir: {
2792
+ width: 820,
2793
+ height: 1180
2794
+ },
2795
+ iPadPro: {
2796
+ width: 1024,
2797
+ height: 1366
2798
+ },
2799
+ SurfacePro7: {
2800
+ width: 912,
2801
+ height: 1368
2802
+ },
2803
+ SurfaceDuo: {
2804
+ width: 540,
2805
+ height: 720
2806
+ },
2807
+ GalaxyZFold5: {
2808
+ width: 344,
2809
+ height: 882
2810
+ },
2811
+ AsusZenbookFold: {
2812
+ width: 853,
2813
+ height: 1280
2814
+ },
2815
+ SamsungGalaxyA51A71: {
2816
+ width: 412,
2817
+ height: 914
2818
+ },
2819
+ NestHub: {
2820
+ width: 1024,
2821
+ height: 600
2822
+ },
2823
+ NestHubMax: {
2824
+ width: 1280,
2825
+ height: 800
2826
+ }
2827
+ };
2828
+ const resolveBrowserViewportPreset = (presetId)=>{
2829
+ const size = BROWSER_VIEWPORT_PRESET_DIMENSIONS[presetId];
2830
+ return size ?? null;
2831
+ };
2832
+ const SUPPORTED_PROVIDERS = [
2833
+ 'playwright'
2834
+ ];
2835
+ const isPlainObject = (value)=>'[object Object]' === Object.prototype.toString.call(value);
2836
+ const validateViewport = (viewport)=>{
2837
+ if (null == viewport) return;
2838
+ if ('string' == typeof viewport) {
2839
+ const presetId = viewport.trim();
2840
+ if (!presetId) throw new Error('browser.viewport must be a non-empty preset id.');
2841
+ if (!resolveBrowserViewportPreset(presetId)) throw new Error(`browser.viewport must be a valid preset id. Received: ${viewport}`);
2842
+ return;
2843
+ }
2844
+ if (isPlainObject(viewport)) {
2845
+ const width = viewport.width;
2846
+ const height = viewport.height;
2847
+ if (!Number.isFinite(width) || width <= 0) throw new Error('browser.viewport.width must be a positive number.');
2848
+ if (!Number.isFinite(height) || height <= 0) throw new Error('browser.viewport.height must be a positive number.');
2849
+ return;
2850
+ }
2851
+ throw new Error('browser.viewport must be either a preset id or { width, height }.');
2852
+ };
2853
+ const validateBrowserConfig = (context)=>{
2854
+ for (const project of context.projects){
2855
+ const browser = project.normalizedConfig.browser;
2856
+ if (browser.enabled) {
2857
+ if (!browser.provider) throw new Error('browser.provider is required when browser.enabled is true.');
2858
+ if (!SUPPORTED_PROVIDERS.includes(browser.provider)) throw new Error(`browser.provider must be one of: ${SUPPORTED_PROVIDERS.join(', ')}.`);
2859
+ validateViewport(browser.viewport);
2860
+ }
2861
+ }
2862
+ };
2675
2863
  async function runBrowserTests(context, options) {
2676
2864
  return runBrowserController(context, options);
2677
2865
  }
2678
2866
  async function src_listBrowserTests(context) {
2679
2867
  return listBrowserTests(context);
2680
2868
  }
2681
- export { runBrowserTests, src_listBrowserTests as listBrowserTests };
2869
+ 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.2",
3
+ "version": "0.8.4",
4
4
  "description": "Browser mode support for Rstest testing framework.",
5
5
  "bugs": {
6
6
  "url": "https://github.com/web-infra-dev/rstest/issues"
@@ -52,13 +52,13 @@
52
52
  "picocolors": "^1.1.1",
53
53
  "picomatch": "^4.0.3",
54
54
  "playwright": "^1.49.1",
55
- "@rstest/core": "0.8.2",
55
+ "@rstest/browser-ui": "0.0.0",
56
56
  "@rstest/tsconfig": "0.0.1",
57
- "@rstest/browser-ui": "0.0.0"
57
+ "@rstest/core": "0.8.4"
58
58
  },
59
59
  "peerDependencies": {
60
60
  "playwright": "^1.49.1",
61
- "@rstest/core": "^0.8.2"
61
+ "@rstest/core": "^0.8.4"
62
62
  },
63
63
  "peerDependenciesMeta": {
64
64
  "playwright": {
@@ -0,0 +1,66 @@
1
+ import type { Rstest } from '@rstest/core/browser';
2
+ import { resolveBrowserViewportPreset } from './viewportPresets';
3
+
4
+ const SUPPORTED_PROVIDERS = ['playwright'] as const;
5
+
6
+ const isPlainObject = (value: unknown): value is Record<string, unknown> => {
7
+ return Object.prototype.toString.call(value) === '[object Object]';
8
+ };
9
+
10
+ const validateViewport = (viewport: unknown): void => {
11
+ if (viewport == null) {
12
+ return;
13
+ }
14
+
15
+ if (typeof viewport === 'string') {
16
+ const presetId = viewport.trim();
17
+ if (!presetId) {
18
+ throw new Error('browser.viewport must be a non-empty preset id.');
19
+ }
20
+ if (!resolveBrowserViewportPreset(presetId)) {
21
+ throw new Error(
22
+ `browser.viewport must be a valid preset id. Received: ${viewport}`,
23
+ );
24
+ }
25
+ return;
26
+ }
27
+
28
+ if (isPlainObject(viewport)) {
29
+ const width = (viewport as any).width;
30
+ const height = (viewport as any).height;
31
+ if (!Number.isFinite(width) || width <= 0) {
32
+ throw new Error('browser.viewport.width must be a positive number.');
33
+ }
34
+ if (!Number.isFinite(height) || height <= 0) {
35
+ throw new Error('browser.viewport.height must be a positive number.');
36
+ }
37
+ return;
38
+ }
39
+
40
+ throw new Error(
41
+ 'browser.viewport must be either a preset id or { width, height }.',
42
+ );
43
+ };
44
+
45
+ export const validateBrowserConfig = (context: Rstest): void => {
46
+ for (const project of context.projects) {
47
+ const browser = project.normalizedConfig.browser;
48
+ if (!browser.enabled) {
49
+ continue;
50
+ }
51
+
52
+ if (!browser.provider) {
53
+ throw new Error(
54
+ 'browser.provider is required when browser.enabled is true.',
55
+ );
56
+ }
57
+
58
+ if (!SUPPORTED_PROVIDERS.includes(browser.provider)) {
59
+ throw new Error(
60
+ `browser.provider must be one of: ${SUPPORTED_PROVIDERS.join(', ')}.`,
61
+ );
62
+ }
63
+
64
+ validateViewport(browser.viewport);
65
+ }
66
+ };