@rstest/browser 0.9.2 → 0.9.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.
@@ -1,12 +1,12 @@
1
- /*! LICENSE: lib-react.ce60b6aea5.js.LICENSE.txt */
2
- /*! LICENSE: lib-react.ce60b6aea5.js.LICENSE.txt */ "use strict";
1
+ /*! LICENSE: lib-react.f905279759.js.LICENSE.txt */
2
+ /*! LICENSE: lib-react.f905279759.js.LICENSE.txt */ "use strict";
3
3
  (self.rspackChunk_rstest_browser_ui = self.rspackChunk_rstest_browser_ui || []).push([
4
4
  [
5
5
  "783"
6
6
  ],
7
7
  {
8
8
  7816 (e, t, n) {
9
- var r, l = n(1099), a = n(162), o = n(3085);
9
+ var r, l = n(1099), a = n(162), o = n(5466);
10
10
  function i(e) {
11
11
  var t = "https://react.dev/errors/" + e;
12
12
  if (1 < arguments.length) {
@@ -7947,7 +7947,7 @@
7947
7947
  }
7948
7948
  })(), e.exports = n(7816);
7949
7949
  },
7950
- 3085 (e, t, n) {
7950
+ 5466 (e, t, n) {
7951
7951
  (function e() {
7952
7952
  if ("u" > typeof __REACT_DEVTOOLS_GLOBAL_HOOK__ && "function" == typeof __REACT_DEVTOOLS_GLOBAL_HOOK__.checkDCE) try {
7953
7953
  __REACT_DEVTOOLS_GLOBAL_HOOK__.checkDCE(e);
@@ -0,0 +1 @@
1
+ /*! LICENSE: lib-react.f905279759.js.LICENSE.txt */
@@ -12,7 +12,7 @@
12
12
  <script>
13
13
  window.__RSTEST_BROWSER_OPTIONS__ = __RSTEST_OPTIONS_PLACEHOLDER__;
14
14
  </script>
15
- <script defer src="/container-static/js/lib-react.ce60b6aea5.js"></script><script defer src="/container-static/js/101.82cdbbe145.js"></script><script defer src="/container-static/js/index.602d6770fe.js"></script><link href="/container-static/css/index.5c72297783.css" rel="stylesheet"></head>
15
+ <script defer src="/container-static/js/lib-react.f905279759.js"></script><script defer src="/container-static/js/927.514b181bd2.js"></script><script defer src="/container-static/js/index.5acf502b10.js"></script><link href="/container-static/css/index.5c72297783.css" rel="stylesheet"></head>
16
16
  <body>
17
17
  <div id="root"></div>
18
18
  </body>
package/dist/index.js CHANGED
@@ -2,6 +2,7 @@ import { __webpack_require__ } from "./rslib-runtime.js";
2
2
  import { existsSync } from "node:fs";
3
3
  import promises from "node:fs/promises";
4
4
  import { fileURLToPath } from "node:url";
5
+ import { isDeepStrictEqual } from "node:util";
5
6
  import { TEMP_RSTEST_OUTPUT_DIR, color, getSetupFiles, getTestEntries, isDebug, loadCoverageProvider, logger, rsbuild, serializableConfig } from "@rstest/core/browser";
6
7
  import open_editor from "open-editor";
7
8
  import { basename, dirname, join, normalize, relative, resolve as external_pathe_resolve } from "pathe";
@@ -2214,27 +2215,42 @@ async function dispatchPlaywrightBrowserRpc({ containerPage, runnerPage, request
2214
2215
  }
2215
2216
  throw new Error(`Unknown browser rpc kind: ${request.kind}`);
2216
2217
  }
2217
- async function launchPlaywrightBrowser({ browserName, headless }) {
2218
+ async function launchPlaywrightBrowser({ browserName, headless, providerOptions }) {
2218
2219
  const playwright = await import("playwright");
2219
2220
  const browserType = playwright[browserName];
2221
+ const launchOptions = providerOptions.launch;
2222
+ const launchArgs = Array.isArray(launchOptions?.args) ? launchOptions.args : 'chromium' === browserName ? [
2223
+ '--disable-popup-blocking',
2224
+ '--no-first-run',
2225
+ '--no-default-browser-check'
2226
+ ] : void 0;
2220
2227
  const browser = await browserType.launch({
2228
+ ...launchOptions,
2221
2229
  headless,
2222
- args: 'chromium' === browserName ? [
2223
- '--disable-popup-blocking',
2224
- '--no-first-run',
2225
- '--no-default-browser-check'
2226
- ] : void 0
2230
+ args: launchArgs
2227
2231
  });
2232
+ const wrappedBrowser = {
2233
+ close: async ()=>browser.close(),
2234
+ newContext: async ({ providerOptions: contextProviderOptions, viewport })=>{
2235
+ const contextOptions = contextProviderOptions?.context;
2236
+ const context = await browser.newContext({
2237
+ ...contextOptions,
2238
+ viewport
2239
+ });
2240
+ return context;
2241
+ }
2242
+ };
2228
2243
  return {
2229
- browser: browser
2244
+ browser: wrappedBrowser
2230
2245
  };
2231
2246
  }
2232
2247
  const playwrightProviderImplementation = {
2233
2248
  name: 'playwright',
2234
- async launchRuntime ({ browserName, headless }) {
2249
+ async launchRuntime ({ browserName, headless, providerOptions }) {
2235
2250
  return launchPlaywrightBrowser({
2236
2251
  browserName,
2237
- headless
2252
+ headless,
2253
+ providerOptions
2238
2254
  });
2239
2255
  },
2240
2256
  async dispatchRpc ({ containerPage, runnerPage, request, timeoutFallbackMs }) {
@@ -2567,22 +2583,44 @@ const planWatchRerun = ({ projectEntries, previousTestFiles, affectedTestFiles }
2567
2583
  };
2568
2584
  };
2569
2585
  const picomatch = __webpack_require__("../../node_modules/.pnpm/picomatch@4.0.3/node_modules/picomatch/index.js");
2586
+ var picomatch_default = /*#__PURE__*/ __webpack_require__.n(picomatch);
2570
2587
  const { createRsbuild: createRsbuild, rspack: rspack } = rsbuild;
2571
2588
  const hostController_dirname = dirname(fileURLToPath(import.meta.url));
2572
2589
  const OPTIONS_PLACEHOLDER = '__RSTEST_OPTIONS_PLACEHOLDER__';
2573
2590
  const serializeForInlineScript = (value)=>JSON.stringify(value).replace(/</g, '\\u003c').replace(/\u2028/g, '\\u2028').replace(/\u2029/g, '\\u2029');
2591
+ const getBrowserProviderOptions = (project)=>{
2592
+ const browserConfig = project.normalizedConfig.browser;
2593
+ return browserConfig.providerOptions ?? {};
2594
+ };
2595
+ const createDeferredPromise = ()=>{
2596
+ let resolve;
2597
+ let reject;
2598
+ const promise = new Promise((res, rej)=>{
2599
+ resolve = res;
2600
+ reject = rej;
2601
+ });
2602
+ return {
2603
+ promise,
2604
+ resolve,
2605
+ reject
2606
+ };
2607
+ };
2574
2608
  class ContainerRpcManager {
2575
2609
  wss;
2576
2610
  ws = null;
2577
2611
  rpc = null;
2578
2612
  methods;
2579
- constructor(wss, methods){
2613
+ onDisconnect;
2614
+ detachActiveSocketListeners = null;
2615
+ constructor(wss, methods, onDisconnect){
2580
2616
  this.wss = wss;
2581
2617
  this.methods = methods;
2618
+ this.onDisconnect = onDisconnect;
2582
2619
  this.setupConnectionHandler();
2583
2620
  }
2584
- updateMethods(methods) {
2621
+ updateMethods(methods, onDisconnect) {
2585
2622
  this.methods = methods;
2623
+ this.onDisconnect = onDisconnect;
2586
2624
  if (this.ws && this.ws.readyState === this.ws.OPEN) this.attachWebSocket(this.ws);
2587
2625
  }
2588
2626
  setupConnectionHandler() {
@@ -2593,26 +2631,47 @@ class ContainerRpcManager {
2593
2631
  });
2594
2632
  }
2595
2633
  attachWebSocket(ws) {
2634
+ this.detachActiveSocketListeners?.();
2635
+ if (this.rpc && !this.rpc.$closed) this.rpc.$close(new Error('Container RPC transport reattached'));
2596
2636
  this.ws = ws;
2637
+ const messageHandlers = new WeakMap();
2597
2638
  this.rpc = createBirpc(this.methods, {
2639
+ timeout: -1,
2598
2640
  post: (data)=>{
2599
2641
  if (ws.readyState === ws.OPEN) ws.send(JSON.stringify(data));
2600
2642
  },
2601
2643
  on: (fn)=>{
2602
- ws.on('message', (message)=>{
2644
+ const handler = (message)=>{
2603
2645
  try {
2604
2646
  const data = JSON.parse(message.toString());
2605
2647
  fn(data);
2606
2648
  } catch {}
2607
- });
2649
+ };
2650
+ messageHandlers.set(fn, handler);
2651
+ ws.on('message', handler);
2652
+ },
2653
+ off: (fn)=>{
2654
+ const handler = messageHandlers.get(fn);
2655
+ if (!handler) return;
2656
+ ws.off('message', handler);
2657
+ messageHandlers.delete(fn);
2608
2658
  }
2609
2659
  });
2610
- ws.on('close', ()=>{
2611
- if (this.ws === ws) {
2612
- this.ws = null;
2613
- this.rpc = null;
2660
+ const handleClose = ()=>{
2661
+ if (this.ws === ws) this.ws = null;
2662
+ this.detachActiveSocketListeners?.();
2663
+ this.detachActiveSocketListeners = null;
2664
+ if (this.rpc && !this.rpc.$closed) {
2665
+ const disconnectError = new Error('Browser UI WebSocket disconnected before reload completed');
2666
+ this.rpc.$close(disconnectError);
2667
+ this.onDisconnect?.(disconnectError);
2614
2668
  }
2615
- });
2669
+ this.rpc = null;
2670
+ };
2671
+ ws.on('close', handleClose);
2672
+ this.detachActiveSocketListeners = ()=>{
2673
+ ws.off('close', handleClose);
2674
+ };
2616
2675
  }
2617
2676
  get isConnected() {
2618
2677
  return null !== this.ws && this.ws.readyState === this.ws.OPEN;
@@ -2628,9 +2687,9 @@ class ContainerRpcManager {
2628
2687
  }
2629
2688
  async reloadTestFile(testFile, testNamePattern) {
2630
2689
  logger.debug(`[Browser UI] reloadTestFile called, rpc: ${this.rpc ? 'exists' : 'null'}, ws: ${this.ws ? 'exists' : 'null'}`);
2631
- if (!this.rpc) return void logger.debug('[Browser UI] RPC not available, skipping reloadTestFile');
2690
+ if (!this.rpc) throw new Error('Browser UI RPC not available for reloadTestFile');
2632
2691
  logger.debug(`[Browser UI] Calling reloadTestFile: ${testFile}`);
2633
- await this.rpc.reloadTestFile(testFile, testNamePattern);
2692
+ return this.rpc.reloadTestFile(testFile, testNamePattern);
2634
2693
  }
2635
2694
  }
2636
2695
  const watchContext = {
@@ -2701,7 +2760,7 @@ const createBrowserRsbuildDevConfig = (isWatchMode)=>({
2701
2760
  }
2702
2761
  });
2703
2762
  const globToRegexp = (glob)=>{
2704
- const regex = picomatch.makeRe(glob, {
2763
+ const regex = picomatch_default().makeRe(glob, {
2705
2764
  fastpaths: false,
2706
2765
  noglobstar: false,
2707
2766
  bash: false
@@ -2813,7 +2872,8 @@ const getBrowserLaunchOptions = (project)=>({
2813
2872
  browser: project.normalizedConfig.browser.browser,
2814
2873
  headless: project.normalizedConfig.browser.headless,
2815
2874
  port: project.normalizedConfig.browser.port,
2816
- strictPort: project.normalizedConfig.browser.strictPort
2875
+ strictPort: project.normalizedConfig.browser.strictPort,
2876
+ providerOptions: getBrowserProviderOptions(project)
2817
2877
  });
2818
2878
  const ensureConsistentBrowserLaunchOptions = (projects)=>{
2819
2879
  if (0 === projects.length) throw new Error('No browser-enabled projects found.');
@@ -2821,7 +2881,7 @@ const ensureConsistentBrowserLaunchOptions = (projects)=>{
2821
2881
  const firstOptions = getBrowserLaunchOptions(firstProject);
2822
2882
  for (const project of projects.slice(1)){
2823
2883
  const options = getBrowserLaunchOptions(project);
2824
- 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.`);
2884
+ if (options.provider !== firstOptions.provider || options.browser !== firstOptions.browser || options.headless !== firstOptions.headless || options.port !== firstOptions.port || options.strictPort !== firstOptions.strictPort || !isDeepStrictEqual(options.providerOptions, firstOptions.providerOptions)) 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/providerOptions.`);
2825
2885
  }
2826
2886
  return firstOptions;
2827
2887
  };
@@ -2834,9 +2894,8 @@ const resolveProviderForTestPath = ({ testPath, browserProjects })=>{
2834
2894
  throw new Error(`Cannot resolve browser provider for test path: ${JSON.stringify(testPath)}. Known project roots: ${JSON.stringify(sortedProjects.map((p)=>p.rootPath))}`);
2835
2895
  };
2836
2896
  const collectProjectEntries = async (context)=>{
2837
- const projectEntries = [];
2838
2897
  const browserProjects = getBrowserProjects(context);
2839
- for (const project of browserProjects){
2898
+ return Promise.all(browserProjects.map(async (project)=>{
2840
2899
  const { normalizedConfig: { include, exclude, includeSource, setupFiles } } = project;
2841
2900
  const tests = await getTestEntries({
2842
2901
  include,
@@ -2847,13 +2906,12 @@ const collectProjectEntries = async (context)=>{
2847
2906
  fileFilters: context.fileFilters || []
2848
2907
  });
2849
2908
  const setup = getSetupFiles(setupFiles, project.rootPath);
2850
- projectEntries.push({
2909
+ return {
2851
2910
  project,
2852
2911
  setupFiles: Object.values(setup),
2853
2912
  testFiles: Object.values(tests)
2854
- });
2855
- }
2856
- return projectEntries;
2913
+ };
2914
+ }));
2857
2915
  };
2858
2916
  const resolveBrowserFile = (relativePath)=>{
2859
2917
  const candidates = [
@@ -3241,12 +3299,14 @@ const createBrowserRuntime = async ({ context, manifestPath, manifestSource, tem
3241
3299
  const providerImplementation = getBrowserProviderImplementation(browserLaunchOptions.provider);
3242
3300
  const runtime = await providerImplementation.launchRuntime({
3243
3301
  browserName,
3244
- headless: forceHeadless ?? browserLaunchOptions.headless
3302
+ headless: forceHeadless ?? browserLaunchOptions.headless,
3303
+ providerOptions: browserLaunchOptions.providerOptions
3245
3304
  });
3246
3305
  return {
3247
3306
  rsbuildInstance,
3248
3307
  devServer,
3249
3308
  browser: runtime.browser,
3309
+ browserLaunchOptions,
3250
3310
  port,
3251
3311
  wsPort,
3252
3312
  manifestPath,
@@ -3445,7 +3505,7 @@ const runBrowserController = async (context, options)=>{
3445
3505
  });
3446
3506
  }
3447
3507
  }
3448
- const { browser, port, wsPort, wss } = runtime;
3508
+ const { browser, browserLaunchOptions, port, wsPort, wss } = runtime;
3449
3509
  const buildTime = Date.now() - buildStart;
3450
3510
  const allTestFiles = projectEntries.flatMap((entry)=>entry.testFiles.map((testPath)=>({
3451
3511
  testPath: normalize(testPath),
@@ -3665,6 +3725,7 @@ const runBrowserController = async (context, options)=>{
3665
3725
  if (run.cancelled || runLifecycle.isTokenStale(run.token)) return;
3666
3726
  const viewport = viewportByProject.get(file.projectName);
3667
3727
  const browserContext = await browser.newContext({
3728
+ providerOptions: browserLaunchOptions.providerOptions,
3668
3729
  viewport: viewport ?? null
3669
3730
  });
3670
3731
  run.contexts.add(browserContext);
@@ -3970,6 +4031,7 @@ const runBrowserController = async (context, options)=>{
3970
4031
  } else {
3971
4032
  isNewPage = true;
3972
4033
  containerContext = await browser.newContext({
4034
+ providerOptions: browserLaunchOptions.providerOptions,
3973
4035
  viewport: null
3974
4036
  });
3975
4037
  containerPage = await containerContext.newPage();
@@ -3991,21 +4053,61 @@ const runBrowserController = async (context, options)=>{
3991
4053
  activeContainerPage = containerPage;
3992
4054
  const dispatchRouter = createDispatchRouter();
3993
4055
  const headedReloadQueue = createHeadedSerialTaskQueue();
4056
+ const pendingHeadedReloads = new Map();
3994
4057
  let enqueueHeadedReload = async (_file, _testNamePattern)=>{
3995
4058
  throw new Error('Headed reload queue is not initialized');
3996
4059
  };
4060
+ const rejectPendingHeadedReload = (testPath, error, runId)=>{
4061
+ const pending = pendingHeadedReloads.get(testPath);
4062
+ if (!pending) return;
4063
+ if (runId && pending.runId !== runId) return;
4064
+ pendingHeadedReloads.delete(testPath);
4065
+ pending.deferred.reject(error);
4066
+ };
4067
+ const rejectAllPendingHeadedReloads = (error)=>{
4068
+ for (const [testPath, pending] of pendingHeadedReloads){
4069
+ pendingHeadedReloads.delete(testPath);
4070
+ pending.deferred.reject(error);
4071
+ }
4072
+ };
4073
+ const registerPendingHeadedReload = (testPath, runId)=>{
4074
+ const previousPending = pendingHeadedReloads.get(testPath);
4075
+ if (previousPending) {
4076
+ previousPending.deferred.reject(new Error(`Reload for "${testPath}" was superseded by a newer request.`));
4077
+ pendingHeadedReloads.delete(testPath);
4078
+ }
4079
+ const deferred = createDeferredPromise();
4080
+ pendingHeadedReloads.set(testPath, {
4081
+ runId,
4082
+ deferred
4083
+ });
4084
+ return deferred.promise;
4085
+ };
4086
+ const resolvePendingHeadedReload = (testPath, runId)=>{
4087
+ const pending = pendingHeadedReloads.get(testPath);
4088
+ if (!pending) return;
4089
+ if (runId && pending.runId !== runId) return void logger.debug(`[Browser UI] Ignoring stale file-complete for ${testPath}. current=${pending.runId}, incoming=${runId}`);
4090
+ pendingHeadedReloads.delete(testPath);
4091
+ pending.deferred.resolve();
4092
+ };
3997
4093
  const reloadTestFileWithTimeout = async (file, testNamePattern)=>{
3998
4094
  const timeoutMs = getHeadedPerFileTimeoutMs(file);
3999
4095
  let timeoutId;
4096
+ let reloadAck;
4000
4097
  try {
4098
+ reloadAck = await rpcManager.reloadTestFile(file.testPath, testNamePattern);
4099
+ const completionPromise = registerPendingHeadedReload(file.testPath, reloadAck.runId);
4001
4100
  await Promise.race([
4002
- rpcManager.reloadTestFile(file.testPath, testNamePattern),
4101
+ completionPromise,
4003
4102
  new Promise((_, reject)=>{
4004
4103
  timeoutId = setTimeout(()=>{
4005
4104
  reject(new Error(`Headed test execution timeout after ${timeoutMs / 1000}s for ${file.testPath}.`));
4006
4105
  }, timeoutMs);
4007
4106
  })
4008
4107
  ]);
4108
+ } catch (error) {
4109
+ if (reloadAck?.runId) rejectPendingHeadedReload(file.testPath, toError(error), reloadAck.runId);
4110
+ throw error;
4009
4111
  } finally{
4010
4112
  if (timeoutId) clearTimeout(timeoutId);
4011
4113
  }
@@ -4031,12 +4133,21 @@ const runBrowserController = async (context, options)=>{
4031
4133
  await handleTestCaseResult(payload);
4032
4134
  },
4033
4135
  async onTestFileComplete (payload) {
4034
- await handleTestFileComplete(payload);
4136
+ try {
4137
+ await handleTestFileComplete(payload);
4138
+ resolvePendingHeadedReload(payload.testPath, payload.runId);
4139
+ } catch (error) {
4140
+ rejectPendingHeadedReload(payload.testPath, toError(error), payload.runId);
4141
+ throw error;
4142
+ }
4035
4143
  },
4036
4144
  async onLog (payload) {
4037
4145
  await handleLog(payload);
4038
4146
  },
4039
4147
  async onFatal (payload) {
4148
+ const error = new Error(payload.message);
4149
+ error.stack = payload.stack;
4150
+ rejectAllPendingHeadedReloads(error);
4040
4151
  await handleFatal(payload);
4041
4152
  },
4042
4153
  async dispatch (request) {
@@ -4046,11 +4157,11 @@ const runBrowserController = async (context, options)=>{
4046
4157
  let rpcManager;
4047
4158
  if (isWatchMode && runtime.rpcManager) {
4048
4159
  rpcManager = runtime.rpcManager;
4049
- rpcManager.updateMethods(createRpcMethods());
4160
+ rpcManager.updateMethods(createRpcMethods(), rejectAllPendingHeadedReloads);
4050
4161
  const existingWs = rpcManager.currentWebSocket;
4051
4162
  if (existingWs) rpcManager.reattach(existingWs);
4052
4163
  } else {
4053
- rpcManager = new ContainerRpcManager(wss, createRpcMethods());
4164
+ rpcManager = new ContainerRpcManager(wss, createRpcMethods(), rejectAllPendingHeadedReloads);
4054
4165
  if (isWatchMode) runtime.rpcManager = rpcManager;
4055
4166
  }
4056
4167
  if (isNewPage) {
@@ -4200,7 +4311,7 @@ const listBrowserTests = async (context, options)=>{
4200
4311
  logger.error(color.red(`Failed to initialize browser provider runtime (${providers.join(', ')}).`), error);
4201
4312
  throw error;
4202
4313
  }
4203
- const { browser, port } = runtime;
4314
+ const { browser, browserLaunchOptions, port } = runtime;
4204
4315
  const projectRuntimeConfigs = browserProjects.map((project)=>({
4205
4316
  name: project.name,
4206
4317
  environmentName: project.environmentName,
@@ -4228,6 +4339,7 @@ const listBrowserTests = async (context, options)=>{
4228
4339
  resolveCollect = resolve;
4229
4340
  });
4230
4341
  const browserContext = await browser.newContext({
4342
+ providerOptions: browserLaunchOptions.providerOptions,
4231
4343
  viewport: null
4232
4344
  });
4233
4345
  const page = await browserContext.newPage();
@@ -4341,6 +4453,7 @@ const validateBrowserConfig = (context)=>{
4341
4453
  if (!browser.provider) throw new Error('browser.provider is required when browser.enabled is true.');
4342
4454
  if (!SUPPORTED_PROVIDERS.includes(browser.provider)) throw new Error(`browser.provider must be one of: ${SUPPORTED_PROVIDERS.join(', ')}.`);
4343
4455
  validateViewport(browser.viewport);
4456
+ if (!isPlainObject(browser.providerOptions)) throw new Error('browser.providerOptions must be a plain object.');
4344
4457
  }
4345
4458
  }
4346
4459
  };
@@ -4,6 +4,14 @@ import type { BrowserRpcRequest } from '../rpcProtocol';
4
4
  *
5
5
  * When adding a new built-in provider, implement `BrowserProviderImplementation`
6
6
  * and register it in `providerImplementations` below.
7
+ *
8
+ * Provider-agnostic policy:
9
+ * - keep shared contracts and behavior provider-neutral
10
+ * - `browser.providerOptions` stays opaque at the framework boundary
11
+ * - do not export provider-owned config types from `@rstest/browser`
12
+ * - do not reference optional peer provider types from public declarations
13
+ * - keep provider-specific behavior and config decoding inside provider implementations
14
+ * - prefer direct passthrough to provider APIs over provider-specific translation
7
15
  */
8
16
  export type BrowserProvider = 'playwright';
9
17
  /** Minimal console shape needed by host logging bridge. */
@@ -46,6 +54,7 @@ export type BrowserProviderBrowser = {
46
54
  width: number;
47
55
  height: number;
48
56
  } | null;
57
+ providerOptions?: Record<string, unknown>;
49
58
  }) => Promise<BrowserProviderContext>;
50
59
  };
51
60
  /** Provider launch result consumed by hostController. */
@@ -56,6 +65,7 @@ export type BrowserProviderRuntime = {
56
65
  export type LaunchBrowserInput = {
57
66
  browserName: 'chromium' | 'firefox' | 'webkit';
58
67
  headless: boolean | undefined;
68
+ providerOptions: Record<string, unknown>;
59
69
  };
60
70
  /** Input contract for provider-side browser RPC dispatch. */
61
71
  export type DispatchBrowserRpcInput = {
@@ -1,5 +1,6 @@
1
1
  import type { BrowserProviderRuntime } from '../index';
2
- export declare function launchPlaywrightBrowser({ browserName, headless, }: {
2
+ export declare function launchPlaywrightBrowser({ browserName, headless, providerOptions, }: {
3
3
  browserName: 'chromium' | 'firefox' | 'webkit';
4
4
  headless: boolean | undefined;
5
+ providerOptions: Record<string, unknown>;
5
6
  }): Promise<BrowserProviderRuntime>;
@@ -10,9 +10,29 @@ function __webpack_require__(moduleId) {
10
10
  return module.exports;
11
11
  }
12
12
  __webpack_require__.m = __webpack_modules__;
13
+ (()=>{
14
+ __webpack_require__.n = (module)=>{
15
+ var getter = module && module.__esModule ? ()=>module['default'] : ()=>module;
16
+ __webpack_require__.d(getter, {
17
+ a: getter
18
+ });
19
+ return getter;
20
+ };
21
+ })();
22
+ (()=>{
23
+ __webpack_require__.d = (exports, definition)=>{
24
+ for(var key in definition)if (__webpack_require__.o(definition, key) && !__webpack_require__.o(exports, key)) Object.defineProperty(exports, key, {
25
+ enumerable: true,
26
+ get: definition[key]
27
+ });
28
+ };
29
+ })();
13
30
  (()=>{
14
31
  __webpack_require__.add = function(modules) {
15
32
  Object.assign(__webpack_require__.m, modules);
16
33
  };
17
34
  })();
35
+ (()=>{
36
+ __webpack_require__.o = (obj, prop)=>Object.prototype.hasOwnProperty.call(obj, prop);
37
+ })();
18
38
  export { __webpack_require__ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rstest/browser",
3
- "version": "0.9.2",
3
+ "version": "0.9.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,18 +52,19 @@
52
52
  "@types/convert-source-map": "^2.0.3",
53
53
  "@types/picomatch": "^4.0.2",
54
54
  "@types/ws": "^8.18.1",
55
+ "@typescript/native-preview": "7.0.0-dev.20260317.1",
55
56
  "@vitest/snapshot": "^3.2.4",
56
57
  "birpc": "^4.0.0",
57
58
  "picocolors": "^1.1.1",
58
59
  "picomatch": "^4.0.3",
59
60
  "playwright": "^1.58.2",
60
- "@rstest/core": "0.9.2",
61
61
  "@rstest/tsconfig": "0.0.1",
62
+ "@rstest/core": "0.9.4",
62
63
  "@rstest/browser-ui": "0.0.0"
63
64
  },
64
65
  "peerDependencies": {
65
66
  "playwright": "^1.49.1",
66
- "@rstest/core": "^0.9.2"
67
+ "@rstest/core": "^0.9.4"
67
68
  },
68
69
  "peerDependenciesMeta": {
69
70
  "playwright": {
package/src/AGENTS.md CHANGED
@@ -42,7 +42,7 @@ flowchart LR
42
42
  RPC --> CH
43
43
  CH --> UR
44
44
 
45
- HC -."headless bridge:\nexposeFunction(\_\_rstest_dispatch\_\_, \_\_rstest_dispatch_rpc\_\_)".-> Runner
45
+ HC -."headless bridge:\nexposeFunction(__rstest_dispatch__, __rstest_dispatch_rpc__)".-> Runner
46
46
  ```
47
47
 
48
48
  ## Headed transport path
@@ -6,7 +6,7 @@ This document is architecture-only and focuses on the browser runner runtime in
6
6
 
7
7
  ```mermaid
8
8
  flowchart TD
9
- A["waitForConfig()"] --> B["read \_\_RSTEST_BROWSER_OPTIONS\_\_ + URL overrides"]
9
+ A["waitForConfig()"] --> B["read __RSTEST_BROWSER_OPTIONS__ + URL overrides"]
10
10
  B --> R["send ready"]
11
11
  R --> C["setRealTimers()"]
12
12
  C --> D["preloadRunnerSourceMap()"]
@@ -23,7 +23,7 @@ flowchart TD
23
23
  L --> N["send file-complete per file"]
24
24
  N --> O["send complete after all files"]
25
25
 
26
- H --> M["window.\_\_RSTEST_DONE\_\_ = true"]
26
+ H --> M["window.__RSTEST_DONE__ = true"]
27
27
  O --> M
28
28
  ```
29
29
 
@@ -32,19 +32,19 @@ flowchart TD
32
32
  ```mermaid
33
33
  flowchart LR
34
34
  subgraph IframePath["Iframe path (headed)"]
35
- S1["send()"] --> P1["parent.postMessage(\_\_rstest_dispatch\_\_)"]
35
+ S1["send()"] --> P1["parent.postMessage(__rstest_dispatch__)"]
36
36
  R1["dispatchRunnerLifecycle()"] --> P2["postMessage dispatch-rpc-request"]
37
- SN1["snapshot.sendRpcRequest()"] --> P3["postMessage dispatch-rpc-request + wait \_\_rstest_dispatch_response\_\_"]
37
+ SN1["snapshot.sendRpcRequest()"] --> P3["postMessage dispatch-rpc-request + wait __rstest_dispatch_response__"]
38
38
  end
39
39
 
40
40
  subgraph TopLevelRunPath["Top-level page path (headless run)"]
41
- S2["send()"] --> D1["window.\_\_rstest_dispatch\_\_"]
42
- R2["dispatchRunnerLifecycle()"] --> D2["window.\_\_rstest_dispatch_rpc\_\_"]
43
- SN2["snapshot.sendRpcRequest()"] --> D3["window.\_\_rstest_dispatch_rpc\_\_"]
41
+ S2["send()"] --> D1["window.__rstest_dispatch__"]
42
+ R2["dispatchRunnerLifecycle()"] --> D2["window.__rstest_dispatch_rpc__"]
43
+ SN2["snapshot.sendRpcRequest()"] --> D3["window.__rstest_dispatch_rpc__"]
44
44
  end
45
45
 
46
46
  subgraph TopLevelCollectPath["Top-level page path (list collect)"]
47
- S3["send()"] --> C1["window.\_\_rstest_dispatch\_\_"]
47
+ S3["send()"] --> C1["window.__rstest_dispatch__"]
48
48
  end
49
49
  ```
50
50
 
@@ -62,5 +62,9 @@ export const validateBrowserConfig = (context: Rstest): void => {
62
62
  }
63
63
 
64
64
  validateViewport(browser.viewport);
65
+
66
+ if (!isPlainObject(browser.providerOptions)) {
67
+ throw new Error('browser.providerOptions must be a plain object.');
68
+ }
65
69
  }
66
70
  };