diff --git a/AGENTS.md b/AGENTS.md index aa5d867e3..4dd7b5ed3 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -93,6 +93,7 @@ When validating changes through `examples/` or `e2e/`, remember that package sou - Don't mix CommonJS and ESM in the same module - Don't add heavy dependencies without discussion +- Don't use namespace imports like `import * as foo from 'foo'` unless the module shape requires it - Don't commit without running lint - Don't make repo-wide rewrites unless explicitly asked diff --git a/packages/browser/AGENTS.md b/packages/browser/AGENTS.md index 24032806f..0c8aa3045 100644 --- a/packages/browser/AGENTS.md +++ b/packages/browser/AGENTS.md @@ -60,6 +60,19 @@ Contract ownership: - `@rstest/browser-ui` owns transport bridging and UI state projection only. - Runner runtime (`src/client`) owns test execution and emits protocol messages, but does not own filesystem access. +## Provider-agnostic design + +Browser mode must stay provider-neutral at the framework boundary. + +- Keep shared config, protocol, scheduling, and public APIs provider-agnostic. +- Treat `browser.providerOptions` as an opaque passthrough at the framework boundary. +- Do not export provider-owned config types from `@rstest/browser` public entrypoints. +- Do not reference optional peer provider modules from public declarations, including `import type` and `import('pkg')` in type positions. +- Keep provider-specific behavior, config decoding, and runtime quirks inside provider implementations whenever possible. +- Prefer direct passthrough to provider APIs over provider-specific post-init translation layers. If a capability cannot be expressed as passthrough, only promote it when the behavior is meaningful across multiple providers. +- Do not introduce new shared abstractions for a single provider convenience; promote behavior into shared contracts only when it is meaningful across multiple providers. +- When richer DX is needed later, prefer provider-owned helpers or separate optional type entrypoints over coupling the main package surface to a specific provider. + ## Module structure - `src/index.ts` — Package entry, exports runBrowserTests and listBrowserTests diff --git a/packages/browser/src/configValidation.ts b/packages/browser/src/configValidation.ts index be34ab30b..00d1e49d5 100644 --- a/packages/browser/src/configValidation.ts +++ b/packages/browser/src/configValidation.ts @@ -62,5 +62,9 @@ export const validateBrowserConfig = (context: Rstest): void => { } validateViewport(browser.viewport); + + if (!isPlainObject(browser.providerOptions)) { + throw new Error('browser.providerOptions must be a plain object.'); + } } }; diff --git a/packages/browser/src/hostController.ts b/packages/browser/src/hostController.ts index e54d6696c..d73ee3055 100644 --- a/packages/browser/src/hostController.ts +++ b/packages/browser/src/hostController.ts @@ -3,6 +3,7 @@ import fs from 'node:fs/promises'; import type { IncomingMessage, ServerResponse } from 'node:http'; import type { AddressInfo } from 'node:net'; import { fileURLToPath } from 'node:url'; +import { isDeepStrictEqual } from 'node:util'; import type { Rspack } from '@rstest/core'; import { type BrowserTestRunOptions, @@ -123,10 +124,24 @@ type BrowserProviderProject = { provider: BrowserProvider; }; -type BrowserLaunchOptions = Pick< - ProjectContext['normalizedConfig']['browser'], - 'provider' | 'browser' | 'headless' | 'port' | 'strictPort' ->; +type BrowserLaunchOptions = { + provider: BrowserProvider; + browser: ProjectContext['normalizedConfig']['browser']['browser']; + headless: ProjectContext['normalizedConfig']['browser']['headless']; + port: ProjectContext['normalizedConfig']['browser']['port']; + strictPort: ProjectContext['normalizedConfig']['browser']['strictPort']; + providerOptions: Record; +}; + +const getBrowserProviderOptions = ( + project: ProjectContext, +): Record => { + const browserConfig = project.normalizedConfig.browser as { + providerOptions?: Record; + }; + + return browserConfig.providerOptions ?? {}; +}; /** Payload for test file start event */ type TestFileStartPayload = { @@ -372,6 +387,7 @@ type BrowserRuntime = { rsbuildInstance: RsbuildInstance; devServer: RsbuildDevServer; browser: BrowserProviderBrowser; + browserLaunchOptions: BrowserLaunchOptions; port: number; wsPort: number; manifestPath: string; @@ -797,6 +813,7 @@ const getBrowserLaunchOptions = ( headless: project.normalizedConfig.browser.headless, port: project.normalizedConfig.browser.port, strictPort: project.normalizedConfig.browser.strictPort, + providerOptions: getBrowserProviderOptions(project), }); const ensureConsistentBrowserLaunchOptions = ( @@ -816,11 +833,12 @@ const ensureConsistentBrowserLaunchOptions = ( options.browser !== firstOptions.browser || options.headless !== firstOptions.headless || options.port !== firstOptions.port || - options.strictPort !== firstOptions.strictPort + 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.', + 'All browser-enabled projects in one run must share provider/browser/headless/port/strictPort/providerOptions.', ); } } @@ -1542,11 +1560,13 @@ const createBrowserRuntime = async ({ const runtime = await providerImplementation.launchRuntime({ browserName, headless: forceHeadless ?? browserLaunchOptions.headless, + providerOptions: browserLaunchOptions.providerOptions, }); return { rsbuildInstance, devServer, browser: runtime.browser, + browserLaunchOptions, port, wsPort, manifestPath, @@ -1875,7 +1895,7 @@ export const runBrowserController = async ( } } - const { browser, port, wsPort, wss } = runtime; + const { browser, browserLaunchOptions, port, wsPort, wss } = runtime; const buildTime = Date.now() - buildStart; // Collect all test files from project entries with project info @@ -2281,6 +2301,7 @@ export const runBrowserController = async ( const viewport = viewportByProject.get(file.projectName); const browserContext = await browser.newContext({ + providerOptions: browserLaunchOptions.providerOptions, viewport: viewport ?? null, }); run.contexts.add(browserContext); @@ -2746,6 +2767,7 @@ export const runBrowserController = async ( } else { isNewPage = true; containerContext = await browser.newContext({ + providerOptions: browserLaunchOptions.providerOptions, viewport: null, }); containerPage = await containerContext.newPage(); @@ -3245,7 +3267,7 @@ export const listBrowserTests = async ( throw error; } - const { browser, port } = runtime; + const { browser, browserLaunchOptions, port } = runtime; // Get browser projects for runtime config // Normalize projectRoot to posix format for cross-platform compatibility @@ -3289,7 +3311,10 @@ export const listBrowserTests = async ( }); // Create a headless page to run collection - const browserContext = await browser.newContext({ viewport: null }); + const browserContext = await browser.newContext({ + providerOptions: browserLaunchOptions.providerOptions, + viewport: null, + }); const page = await browserContext.newPage(); // Expose dispatch function for browser client to send messages diff --git a/packages/browser/src/index.ts b/packages/browser/src/index.ts index b11c3e228..e3a3bb5af 100644 --- a/packages/browser/src/index.ts +++ b/packages/browser/src/index.ts @@ -10,7 +10,6 @@ import { } from './hostController'; export { validateBrowserConfig } from './configValidation'; - export { BROWSER_VIEWPORT_PRESET_DIMENSIONS, BROWSER_VIEWPORT_PRESET_IDS, diff --git a/packages/browser/src/providers/index.ts b/packages/browser/src/providers/index.ts index eb7ec0095..e33f76cae 100644 --- a/packages/browser/src/providers/index.ts +++ b/packages/browser/src/providers/index.ts @@ -6,6 +6,14 @@ import { playwrightProviderImplementation } from './playwright'; * * When adding a new built-in provider, implement `BrowserProviderImplementation` * and register it in `providerImplementations` below. + * + * Provider-agnostic policy: + * - keep shared contracts and behavior provider-neutral + * - `browser.providerOptions` stays opaque at the framework boundary + * - do not export provider-owned config types from `@rstest/browser` + * - do not reference optional peer provider types from public declarations + * - keep provider-specific behavior and config decoding inside provider implementations + * - prefer direct passthrough to provider APIs over provider-specific translation */ export type BrowserProvider = 'playwright'; @@ -50,6 +58,7 @@ export type BrowserProviderBrowser = { close: () => Promise; newContext: (options: { viewport: { width: number; height: number } | null; + providerOptions?: Record; }) => Promise; }; @@ -62,6 +71,7 @@ export type BrowserProviderRuntime = { export type LaunchBrowserInput = { browserName: 'chromium' | 'firefox' | 'webkit'; headless: boolean | undefined; + providerOptions: Record; }; /** Input contract for provider-side browser RPC dispatch. */ diff --git a/packages/browser/src/providers/playwright/implementation.ts b/packages/browser/src/providers/playwright/implementation.ts index 46f397a1b..0f0ab3a5b 100644 --- a/packages/browser/src/providers/playwright/implementation.ts +++ b/packages/browser/src/providers/playwright/implementation.ts @@ -11,10 +11,12 @@ export const playwrightProviderImplementation: BrowserProviderImplementation = { async launchRuntime({ browserName, headless, + providerOptions, }): Promise { return launchPlaywrightBrowser({ browserName, headless, + providerOptions, }); }, async dispatchRpc({ diff --git a/packages/browser/src/providers/playwright/runtime.ts b/packages/browser/src/providers/playwright/runtime.ts index 124793051..4ac4261f6 100644 --- a/packages/browser/src/providers/playwright/runtime.ts +++ b/packages/browser/src/providers/playwright/runtime.ts @@ -1,32 +1,54 @@ -import type { BrowserProviderRuntime } from '../index'; - -type PlaywrightModule = typeof import('playwright'); -type PlaywrightBrowserType = PlaywrightModule['chromium']; +import type { BrowserProviderContext, BrowserProviderRuntime } from '../index'; export async function launchPlaywrightBrowser({ browserName, headless, + providerOptions, }: { browserName: 'chromium' | 'firefox' | 'webkit'; headless: boolean | undefined; + providerOptions: Record; }): Promise { const playwright = await import('playwright'); - const browserType = playwright[browserName] as PlaywrightBrowserType; + const browserType = playwright[browserName]; + const launchOptions = providerOptions.launch as + | Record + | undefined; + const launchArgs = Array.isArray(launchOptions?.args) + ? launchOptions.args + : browserName === 'chromium' + ? [ + '--disable-popup-blocking', + '--no-first-run', + '--no-default-browser-check', + ] + : undefined; const browser = await browserType.launch({ + ...launchOptions, headless, - // Chromium-specific args (ignored by other browsers) - args: - browserName === 'chromium' - ? [ - '--disable-popup-blocking', - '--no-first-run', - '--no-default-browser-check', - ] - : undefined, + args: launchArgs, }); + const wrappedBrowser: BrowserProviderRuntime['browser'] = { + close: async () => browser.close(), + newContext: async ({ + providerOptions: contextProviderOptions, + viewport, + }) => { + const contextOptions = contextProviderOptions?.context as + | Record + | undefined; + const context = await browser.newContext({ + ...contextOptions, + viewport, + }); + + return context as unknown as BrowserProviderContext; + }, + }; + return { - browser: browser as unknown as BrowserProviderRuntime['browser'], + browser: wrappedBrowser, }; } diff --git a/packages/browser/tests/playwrightRuntime.test.ts b/packages/browser/tests/playwrightRuntime.test.ts new file mode 100644 index 000000000..4252284b8 --- /dev/null +++ b/packages/browser/tests/playwrightRuntime.test.ts @@ -0,0 +1,130 @@ +import { afterEach, describe, expect, it } from '@rstest/core'; +import { chromium } from 'playwright'; +import { launchPlaywrightBrowser } from '../src/providers/playwright/runtime'; + +type FakeContext = { + newPage: () => Promise; + on: () => void; + close: () => Promise; + setDefaultTimeout: (...args: unknown[]) => void; + setDefaultNavigationTimeout: (...args: unknown[]) => void; +}; + +describe('launchPlaywrightBrowser', () => { + const originalChromiumLaunch = chromium.launch; + + afterEach(() => { + chromium.launch = originalChromiumLaunch; + }); + + it('passes through launch and context options without timeout translation', async () => { + const launchCalls: Array> = []; + const contextCalls: Array> = []; + const defaultTimeoutCalls: unknown[][] = []; + const defaultNavigationTimeoutCalls: unknown[][] = []; + + const fakeContext: FakeContext = { + async newPage() { + throw new Error('unused in this test'); + }, + on() {}, + async close() {}, + setDefaultTimeout(...args: unknown[]) { + defaultTimeoutCalls.push(args); + }, + setDefaultNavigationTimeout(...args: unknown[]) { + defaultNavigationTimeoutCalls.push(args); + }, + }; + + chromium.launch = (async (options?: Record) => { + launchCalls.push(options ?? {}); + + return { + async close() {}, + async newContext(contextOptions?: Record) { + contextCalls.push(contextOptions ?? {}); + return fakeContext as never; + }, + } as never; + }) as typeof chromium.launch; + + const runtime = await launchPlaywrightBrowser({ + browserName: 'chromium', + headless: true, + providerOptions: { + launch: { + channel: 'chrome', + timeout: 1234, + }, + }, + }); + + await runtime.browser.newContext({ + providerOptions: { + context: { + colorScheme: 'dark', + locale: 'en-US', + }, + actionTimeout: 5000, + navigationTimeout: 6000, + }, + viewport: { width: 1280, height: 720 }, + }); + + expect(launchCalls).toEqual([ + { + channel: 'chrome', + timeout: 1234, + headless: true, + args: [ + '--disable-popup-blocking', + '--no-first-run', + '--no-default-browser-check', + ], + }, + ]); + + expect(contextCalls).toEqual([ + { + colorScheme: 'dark', + locale: 'en-US', + viewport: { width: 1280, height: 720 }, + }, + ]); + expect(defaultTimeoutCalls).toEqual([]); + expect(defaultNavigationTimeoutCalls).toEqual([]); + }); + + it('prefers user-provided launch args over Chromium defaults', async () => { + const launchCalls: Array> = []; + + chromium.launch = (async (options?: Record) => { + launchCalls.push(options ?? {}); + + return { + async close() {}, + async newContext() { + throw new Error('unused in this test'); + }, + } as never; + }) as typeof chromium.launch; + + await launchPlaywrightBrowser({ + browserName: 'chromium', + headless: true, + providerOptions: { + launch: { + args: ['--user-defined-arg'], + }, + }, + }); + + expect(launchCalls).toEqual([ + { + args: ['--user-defined-arg'], + headless: true, + }, + ]); + }); +}); diff --git a/packages/core/src/config.ts b/packages/core/src/config.ts index 191deb459..efe7da1b5 100644 --- a/packages/core/src/config.ts +++ b/packages/core/src/config.ts @@ -187,6 +187,7 @@ const createDefaultConfig = (): NormalizedConfig => ({ browser: 'chromium', headless: isCI, strictPort: false, + providerOptions: {}, }, coverage: { exclude: [ @@ -252,6 +253,7 @@ export const withDefaultConfig = (config: RstestConfig): NormalizedConfig => { port: merged.browser?.port, strictPort: merged.browser?.strictPort ?? false, viewport: merged.browser?.viewport, + providerOptions: merged.browser?.providerOptions ?? {}, }; return { diff --git a/packages/core/src/types/config.ts b/packages/core/src/types/config.ts index ccc17d29c..89d8fa79a 100644 --- a/packages/core/src/types/config.ts +++ b/packages/core/src/types/config.ts @@ -131,6 +131,13 @@ export type BrowserModeConfig = { * @default false */ strictPort?: boolean; + /** + * Provider-specific config passed through to the selected browser provider. + * + * Use provider-owned types or helpers in user config when you want richer + * IntelliSense for this field. + */ + providerOptions?: Record; }; type SnapshotFormat = Omit< @@ -470,6 +477,7 @@ export type NormalizedBrowserModeConfig = { port?: number; strictPort: boolean; viewport?: BrowserViewport; + providerOptions: Record; }; export type NormalizedConfig = Required< diff --git a/packages/core/tests/__snapshots__/config.test.ts.snap b/packages/core/tests/__snapshots__/config.test.ts.snap index e70bac436..d3d6134a1 100644 --- a/packages/core/tests/__snapshots__/config.test.ts.snap +++ b/packages/core/tests/__snapshots__/config.test.ts.snap @@ -9,6 +9,7 @@ exports[`mergeRstestConfig > should merge config correctly with default config 1 "headless": false, "port": undefined, "provider": "playwright", + "providerOptions": {}, "strictPort": false, "viewport": undefined, }, diff --git a/packages/core/tests/core/__snapshots__/rstest.test.ts.snap b/packages/core/tests/core/__snapshots__/rstest.test.ts.snap index 67c0dcd19..4b66e81b5 100644 --- a/packages/core/tests/core/__snapshots__/rstest.test.ts.snap +++ b/packages/core/tests/core/__snapshots__/rstest.test.ts.snap @@ -9,6 +9,7 @@ exports[`rstest context > should generate rstest context correctly 1`] = ` "headless": false, "port": undefined, "provider": "playwright", + "providerOptions": {}, "strictPort": false, "viewport": undefined, }, @@ -96,6 +97,7 @@ exports[`rstest context > should generate rstest context correctly with multiple "headless": false, "port": undefined, "provider": "playwright", + "providerOptions": {}, "strictPort": false, "viewport": undefined, }, @@ -186,6 +188,7 @@ exports[`rstest context > should generate rstest context correctly with multiple "headless": false, "port": undefined, "provider": "playwright", + "providerOptions": {}, "strictPort": false, "viewport": undefined, }, diff --git a/website/docs/en/config/test/browser.mdx b/website/docs/en/config/test/browser.mdx index 037b4593c..aeb848ebb 100644 --- a/website/docs/en/config/test/browser.mdx +++ b/website/docs/en/config/test/browser.mdx @@ -1,3 +1,5 @@ +import { ApiMeta } from '@theme/components/ApiMeta'; + # browser (experimental) - **Type:** @@ -10,6 +12,7 @@ type BrowserModeConfig = { headless?: boolean; port?: number; strictPort?: boolean; + providerOptions?: Record; }; ``` @@ -23,6 +26,7 @@ const defaultBrowser = { headless: true, // CI environment; false for local development port: undefined, // Random available port strictPort: false, + providerOptions: {}, }; ``` @@ -223,6 +227,33 @@ export default defineConfig({ }); ``` +### providerOptions + + + +- **Type:** `Record` +- **Default:** `{}` + +Provider-specific options passed through to the selected browser provider. Rstest does not validate or interpret the contents of this object — it is forwarded as-is to the provider at both browser launch and context creation time. + +The structure of `providerOptions` is defined by each provider. See the [Provider options](#provider-options) section below for details. + +```ts title="rstest.config.ts" +import { defineConfig } from '@rstest/core'; + +export default defineConfig({ + browser: { + enabled: true, + provider: 'playwright', + providerOptions: { + launch: { + timeout: 60_000, + }, + }, + }, +}); +``` + ## Current limitation: browser launch options must match in one run In one `rstest` process, all Browser Mode projects must share the same browser launch options: @@ -232,6 +263,7 @@ In one `rstest` process, all Browser Mode projects must share the same browser l - `headless` - `port` - `strictPort` +- `providerOptions` This means mixing multiple providers, or running Chromium/Firefox/WebKit together in the same run, is not supported yet. @@ -247,7 +279,7 @@ npx rstest --browser.name webkit When using `projects` in Browser Mode, each project still compiles and executes with its own build config (for example `plugins`, `include`, and framework-specific setup). -Browser launch options still need to stay aligned across browser projects: `provider`, `browser`, `headless`, `port`, and `strictPort`. +Browser launch options still need to stay aligned across browser projects: `provider`, `browser`, `headless`, `port`, `strictPort`, and `providerOptions`. ```ts title="rstest.config.ts" import { defineConfig } from '@rstest/core'; @@ -304,6 +336,43 @@ export default defineConfig({ }); ``` +## Provider options + +### Playwright + +When using `provider: 'playwright'`, the Playwright provider recognizes the following fields in `providerOptions`: + +1. **`launch`** — Passed to Playwright's [`browserType.launch()`](https://playwright.dev/docs/api/class-browsertype#browser-type-launch). Controls how the browser process is started. +2. **`context`** — Passed to Playwright's [`browser.newContext()`](https://playwright.dev/docs/api/class-browser#browser-new-context). Controls the browser context created for each test file. + +Since `providerOptions` is typed as `Record`, you won't get IntelliSense out of the box. To get type checking and autocompletion, import types directly from the `playwright` package and use TypeScript's `satisfies` operator: + +```ts title="rstest.config.ts" +import type { LaunchOptions, BrowserContextOptions } from 'playwright'; +import { defineConfig } from '@rstest/core'; + +type PlaywrightProviderOptions = { + launch?: LaunchOptions; + context?: BrowserContextOptions; +}; + +export default defineConfig({ + browser: { + enabled: true, + provider: 'playwright', + providerOptions: { + launch: { + timeout: 60_000, + }, + context: { + locale: 'en-US', + colorScheme: 'dark', + }, + } satisfies PlaywrightProviderOptions, + }, +}); +``` + ## Related links - [Browser Mode Guide](/guide/browser-testing/) - Introduction and usage guide for Browser Mode diff --git a/website/docs/en/config/test/coverage.mdx b/website/docs/en/config/test/coverage.mdx index d432a2b53..b81792bbe 100644 --- a/website/docs/en/config/test/coverage.mdx +++ b/website/docs/en/config/test/coverage.mdx @@ -1,5 +1,9 @@ +import { ApiMeta } from '@theme/components/ApiMeta'; + # coverage + + - **Type:** ```ts @@ -17,7 +21,6 @@ type CoverageOptions = { ``` - **Default:** `undefined` -- **Version:** `>=0.4.0` Collect code coverage and generate coverage reports. diff --git a/website/docs/zh/config/test/browser.mdx b/website/docs/zh/config/test/browser.mdx index e8b317ad6..bf5aa7409 100644 --- a/website/docs/zh/config/test/browser.mdx +++ b/website/docs/zh/config/test/browser.mdx @@ -1,3 +1,5 @@ +import { ApiMeta } from '@theme/components/ApiMeta'; + # browser(实验性) - **类型:** @@ -10,6 +12,7 @@ type BrowserModeConfig = { headless?: boolean; port?: number; strictPort?: boolean; + providerOptions?: Record; }; ``` @@ -23,6 +26,7 @@ const defaultBrowser = { headless: true, // CI 环境;本地开发为 false port: undefined, // 随机可用端口 strictPort: false, + providerOptions: {}, }; ``` @@ -223,6 +227,33 @@ export default defineConfig({ }); ``` +### providerOptions + + + +- **类型:** `Record` +- **默认值:** `{}` + +传递给选定 browser provider 的特定选项。Rstest 不会验证或解析该对象的内容——它会在浏览器启动和 context 创建时原样转发给 provider。 + +`providerOptions` 的结构由各 provider 自行定义。详见下方 [Provider 选项](#provider-选项)。 + +```ts title="rstest.config.ts" +import { defineConfig } from '@rstest/core'; + +export default defineConfig({ + browser: { + enabled: true, + provider: 'playwright', + providerOptions: { + launch: { + timeout: 60_000, + }, + }, + }, +}); +``` + ## 当前限制:同一次 run 的 browser 启动配置必须一致 在一个 `rstest` 进程内,所有启用 Browser Mode 的项目需要共享同一组 browser 启动配置: @@ -232,6 +263,7 @@ export default defineConfig({ - `headless` - `port` - `strictPort` +- `providerOptions` 这意味着目前还不支持在同一次 run 里通过 `projects` 混用多个 provider,或同时配置 Chromium/Firefox/WebKit。 @@ -247,7 +279,7 @@ npx rstest --browser.name webkit 在 Browser Mode 下使用 `projects` 时,每个项目会按自己的构建配置独立编译和执行(如 `plugins`、`include`、框架设置),不会复用其他项目的构建配置。 -但 browser 启动配置仍需保持一致:`provider`、`browser`、`headless`、`port`、`strictPort` 必须在所有 browser 项目中对齐。 +但 browser 启动配置仍需保持一致:`provider`、`browser`、`headless`、`port`、`strictPort`、`providerOptions` 必须在所有 browser 项目中对齐。 ```ts title="rstest.config.ts" import { defineConfig } from '@rstest/core'; @@ -304,6 +336,43 @@ export default defineConfig({ }); ``` +## Provider 选项 + +### Playwright + +使用 `provider: 'playwright'` 时,Playwright provider 识别 `providerOptions` 中的以下字段: + +1. **`launch`** — 传递给 Playwright [`browserType.launch()`](https://playwright.dev/docs/api/class-browsertype#browser-type-launch),控制浏览器进程的启动方式。 +2. **`context`** — 传递给 Playwright [`browser.newContext()`](https://playwright.dev/docs/api/class-browser#browser-new-context),控制每个测试文件创建的 browser context。 + +由于 `providerOptions` 的类型是 `Record`,默认不会提供 IntelliSense。如需类型检查和自动补全,可以直接从 `playwright` 包导入类型,并配合 TypeScript 的 `satisfies` 运算符使用: + +```ts title="rstest.config.ts" +import type { LaunchOptions, BrowserContextOptions } from 'playwright'; +import { defineConfig } from '@rstest/core'; + +type PlaywrightProviderOptions = { + launch?: LaunchOptions; + context?: BrowserContextOptions; +}; + +export default defineConfig({ + browser: { + enabled: true, + provider: 'playwright', + providerOptions: { + launch: { + timeout: 60_000, + }, + context: { + locale: 'en-US', + colorScheme: 'dark', + }, + } satisfies PlaywrightProviderOptions, + }, +}); +``` + ## 相关链接 - [浏览器模式指南](/guide/browser-testing/) - 浏览器模式介绍和使用指南 diff --git a/website/docs/zh/config/test/coverage.mdx b/website/docs/zh/config/test/coverage.mdx index b3acfd8e0..9e168381f 100644 --- a/website/docs/zh/config/test/coverage.mdx +++ b/website/docs/zh/config/test/coverage.mdx @@ -1,5 +1,9 @@ +import { ApiMeta } from '@theme/components/ApiMeta'; + # coverage + + - **类型:** ```ts @@ -17,7 +21,6 @@ type CoverageOptions = { ``` - **默认值:** `undefined` -- **版本:** `>=0.4.0` 收集测试覆盖率信息并生成覆盖率报告。 diff --git a/website/theme/components/ApiMeta.module.scss b/website/theme/components/ApiMeta.module.scss new file mode 100644 index 000000000..e750c7248 --- /dev/null +++ b/website/theme/components/ApiMeta.module.scss @@ -0,0 +1,47 @@ +.wrapper { + margin-top: 1.5rem; +} + +.wrapperInline { + display: inline; +} + +:global(h2) + .wrapper { + margin-top: 0px; +} + +:global(h3) + .wrapper { + margin-top: 0px; +} + +.tagInline { + display: inline-flex; + color: #1c1c1c; + margin-right: 8px; + padding: 4px 10px; + font-size: 12px; + font-weight: 600; + border-radius: 6px; + line-height: 18px; +} + +.tag { + composes: tagInline; + margin-bottom: 12px; +} + +.added { + background-color: hsl(162deg 60% 75%); +} + +.deprecated { + background-color: hsl(36deg 100% 82%); +} + +.removed { + background-color: hsl(0deg 100% 90%); +} + +.experimental { + background-color: hsl(262deg 100% 90%); +} diff --git a/website/theme/components/ApiMeta.tsx b/website/theme/components/ApiMeta.tsx new file mode 100644 index 000000000..41b6b5895 --- /dev/null +++ b/website/theme/components/ApiMeta.tsx @@ -0,0 +1,39 @@ +import { Link } from '@rspress/core/theme'; +import styles from './ApiMeta.module.scss'; + +export interface ApiMetaProps { + addedVersion?: string; + deprecatedVersion?: string; + removedVersion?: string; + inline?: boolean; +} + +export function ApiMeta(props: ApiMetaProps) { + const tagStyle = props.inline ? styles.tagInline : styles.tag; + const wrapperStyle = props.inline ? styles.wrapperInline : styles.wrapper; + + const getGitTagHref = (version: string) => + `https://github.com/web-infra-dev/rstest/releases/tag/v${version.replace('v', '')}`; + + return ( +
+ {props.addedVersion && ( + + + Added in v{props.addedVersion} + + + )} + {props.deprecatedVersion && ( + + Deprecated in v{props.deprecatedVersion} + + )} + {props.removedVersion && ( + + Removed in v{props.removedVersion} + + )} +
+ ); +} diff --git a/website/theme/components/ConfigOverview.tsx b/website/theme/components/ConfigOverview.tsx index 09bc0543f..794c267f4 100644 --- a/website/theme/components/ConfigOverview.tsx +++ b/website/theme/components/ConfigOverview.tsx @@ -62,6 +62,7 @@ const OVERVIEW_GROUPS: BasicGroup[] = [ 'browser.browser', 'browser.headless', 'browser.port', + 'browser.providerOptions', ], }, {