From dd9a22e106fa0e9eb3025e59ef0c430f016d19e3 Mon Sep 17 00:00:00 2001 From: fi3ework Date: Fri, 8 May 2026 19:38:17 +0800 Subject: [PATCH 1/3] feat(rstest): add @midscene/rstest package MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Introduce @midscene/rstest, a Rstest plugin that drives a Midscene Agent through a familiar describe / it flow. Each it block gets a fresh agent; a custom reporter prints the merged Midscene HTML report path after each test file finishes. Two browser providers ship behind subpath exports: - @midscene/rstest/playwright - @midscene/rstest/puppeteer createWebTest(url, options?) returns { agent } valid inside it(...). Options pass directly through to the underlying provider — only headless and viewport are curated as convenience fields: - headless / viewport: shortcuts for the highest-frequency knobs. - launchOptions / contextOptions / gotoOptions: resolvers that forward to Playwright / Puppeteer's own option types. Object form shallow-merges over midscene defaults; function form (defaults) => options takes full control. - agentOptions: forwarded to PlaywrightAgent / PuppeteerAgent. - setup: escape hatch for lifecycles the defaults don't cover (persistent context, CDP connect, fixture reuse, custom trace/video orchestration). --- apps/site/docs/en/integrate-with-rstest.mdx | 265 ++++ apps/site/docs/zh/integrate-with-rstest.mdx | 265 ++++ apps/site/rspress.config.ts | 8 + package.json | 2 +- packages/rstest/README.md | 5 + packages/rstest/demo/contacts.test.ts | 58 + packages/rstest/demo/rstest.config.ts | 12 + packages/rstest/package.json | 89 ++ packages/rstest/rslib.config.ts | 23 + packages/rstest/rstest.config.ts | 5 + packages/rstest/src/index.ts | 6 + packages/rstest/src/lifecycle.ts | 99 ++ packages/rstest/src/playwright.ts | 135 ++ packages/rstest/src/provider-shared.ts | 16 + packages/rstest/src/puppeteer.ts | 122 ++ packages/rstest/src/report-helper.ts | 105 ++ packages/rstest/src/reporter.ts | 30 + packages/rstest/src/resolve.ts | 17 + packages/rstest/src/utils.ts | 17 + packages/rstest/tests/tsconfig.json | 9 + .../tests/unit-test/report-helper.test.ts | 63 + .../rstest/tests/unit-test/resolve.test.ts | 54 + packages/rstest/tests/unit-test/utils.test.ts | 26 + packages/rstest/tsconfig.json | 21 + pnpm-lock.yaml | 1386 +++++++++++------ 25 files changed, 2331 insertions(+), 507 deletions(-) create mode 100644 apps/site/docs/en/integrate-with-rstest.mdx create mode 100644 apps/site/docs/zh/integrate-with-rstest.mdx create mode 100644 packages/rstest/README.md create mode 100644 packages/rstest/demo/contacts.test.ts create mode 100644 packages/rstest/demo/rstest.config.ts create mode 100644 packages/rstest/package.json create mode 100644 packages/rstest/rslib.config.ts create mode 100644 packages/rstest/rstest.config.ts create mode 100644 packages/rstest/src/index.ts create mode 100644 packages/rstest/src/lifecycle.ts create mode 100644 packages/rstest/src/playwright.ts create mode 100644 packages/rstest/src/provider-shared.ts create mode 100644 packages/rstest/src/puppeteer.ts create mode 100644 packages/rstest/src/report-helper.ts create mode 100644 packages/rstest/src/reporter.ts create mode 100644 packages/rstest/src/resolve.ts create mode 100644 packages/rstest/src/utils.ts create mode 100644 packages/rstest/tests/tsconfig.json create mode 100644 packages/rstest/tests/unit-test/report-helper.test.ts create mode 100644 packages/rstest/tests/unit-test/resolve.test.ts create mode 100644 packages/rstest/tests/unit-test/utils.test.ts create mode 100644 packages/rstest/tsconfig.json diff --git a/apps/site/docs/en/integrate-with-rstest.mdx b/apps/site/docs/en/integrate-with-rstest.mdx new file mode 100644 index 0000000000..037e51b21c --- /dev/null +++ b/apps/site/docs/en/integrate-with-rstest.mdx @@ -0,0 +1,265 @@ +import SetupEnv from './common/setup-env.mdx'; +import { PackageManagerTabs } from '@theme'; + +# Integrate with Rstest + +[Rstest](https://github.com/web-infra-dev/rstest) is a fast Rspack-based testing framework. With `@midscene/rstest` you can write AI-driven end-to-end tests in the same `describe / it` style you already use — Midscene takes care of launching the browser, creating an Agent for each test, and producing an HTML report at the end of every test file. + +```ts +import { describe, expect, it } from '@rstest/core'; +import { createWebTest } from '@midscene/rstest/playwright'; + +describe('Todo list', () => { + const ctx = createWebTest('http://localhost:5173/'); + + it('adds a todo', async () => { + const { agent } = ctx; + await agent.aiAct("type 'Study AI' in the input and press Enter"); + await agent.aiAssert('the list contains exactly one item: "Study AI"'); + }); +}); +``` + +What you get out of the box: + +- A fresh `agent` per `it` block, ready to call any [Agent API](./api#interaction-methods) (`aiAct`, `aiAssert`, `aiQuery`, `aiTap`, `aiInput`, `aiHover`, …). +- Automatic browser lifecycle — the browser launches once per test file, and a clean page is set up and torn down around every test. +- A merged Midscene HTML report per test file, with the path printed to the console after the file finishes. +- Direct pass-through to the underlying Playwright / Puppeteer option types, plus a `setup` hook for lifecycles the defaults don't cover. + +Two browser providers ship in the package, behind separate subpath exports: + +- `@midscene/rstest/playwright` — backed by `chromium.launch()`. +- `@midscene/rstest/puppeteer` — backed by `puppeteer.launch()`. + + + +:::info Example Project + +A runnable example lives at [https://github.com/web-infra-dev/midscene-example/tree/main/rstest-demo](https://github.com/web-infra-dev/midscene-example/tree/main/rstest-demo). + +::: + +## Step 1: Install dependencies + +Pick the provider you want to use and install it together with `@midscene/web`, `@rstest/core`, and `@midscene/rstest`. + +With Playwright: + + + +Or with Puppeteer: + + + +## Step 2: Configure Rstest + +Create `rstest.config.ts` in the root of your test project: + +```ts title="rstest.config.ts" +import 'dotenv/config'; // load model env vars from .env +import { defineConfig } from '@rstest/core'; +import MidsceneReporter from '@midscene/rstest/reporter'; + +export default defineConfig({ + include: ['e2e/**/*.test.ts'], + // AI calls are slow — leave plenty of headroom for both tests and hooks. + testTimeout: 1_800_000, + hookTimeout: 60_000, + reporters: ['default', new MidsceneReporter()], +}); +``` + +`MidsceneReporter` prints a line like `Midscene report: midscene_run/report/.html` after each test file. Open it in your browser to replay every AI step with screenshots. + +## Step 3: Write a test + +```ts title="e2e/todo-list.test.ts" +import { describe, expect, it } from '@rstest/core'; +import { createWebTest } from '@midscene/rstest/playwright'; + +describe('Todo list', () => { + const ctx = createWebTest('http://localhost:5173/'); + + it('adds and completes a todo', async () => { + const { agent } = ctx; + + await agent.aiAct("type 'Study AI' in the input and press Enter"); + await agent.aiAct('click the checkbox of the first item'); + + const list = await agent.aiQuery( + 'string[], the complete task list', + ); + expect(list).toHaveLength(1); + + await agent.aiAssert('the only item is marked as done'); + }); +}); +``` + +`createWebTest(url, options)` registers Rstest's `beforeAll` / `beforeEach` / `afterEach` / `afterAll` hooks under the hood. Call it once per `describe` block — every `it` inside it gets its own freshly-navigated page. + +:::warning `agent` is only valid inside `it(...)` +The agent is created in `beforeEach` and destroyed in `afterEach`. Reading `ctx.agent` from `describe` scope, a hook, or after the test finishes throws. +::: + +To use Puppeteer instead, change a single import: + +```ts +import { createWebTest } from '@midscene/rstest/puppeteer'; +``` + +The `agent` API is identical. + +## Step 4: Run the tests + +```bash +npx rstest run + +# or, while iterating: +npx rstest watch +``` + +After each file finishes you'll see something like: + +```text +Midscene report: midscene_run/report/.html +``` + +Reports are written to `midscene_run/report/`. See [Consume report files](./consume-report-file) for programmatic access. + +## Project-wide defaults (optional) + +If every `createWebTest` call should share the same options, put them in a setup file and reference it from `setupFiles`: + +```ts title="midscene.setup.ts" +import { defineMidsceneDefaults } from '@midscene/rstest/playwright'; + +defineMidsceneDefaults({ + viewport: { width: 1280, height: 720 }, + contextOptions: { locale: 'en-US' }, +}); +``` + +```ts title="rstest.config.ts" +export default defineConfig({ + // ... + setupFiles: ['./midscene.setup.ts'], +}); +``` + +Options passed to `createWebTest(url, options)` shallow-merge over these defaults at the top level — nested fields like `launchOptions` are *replaced*, not deep-merged. Use the function form of a resolver if you need to compose. The Puppeteer provider exposes the same `defineMidsceneDefaults` from `@midscene/rstest/puppeteer`. + +## Options + +### Playwright (`@midscene/rstest/playwright`) + +| Option | Type | Default | Description | +| --- | --- | --- | --- | +| `headless` | `boolean` | `true` in CI, `false` locally | Convenience for `launchOptions.headless`. | +| `viewport` | `{ width, height }` | `1920×1080` | Convenience for `contextOptions.viewport`. | +| `launchOptions` | `LaunchOptions \| (defaults) => LaunchOptions` | — | Forwarded to `chromium.launch(...)`. | +| `contextOptions` | `BrowserContextOptions \| (defaults) => BrowserContextOptions` | — | Forwarded to `browser.newContext(...)`. | +| `gotoOptions` | `Parameters[1]` | — | Forwarded to `page.goto(url, ...)`. | +| `agentOptions` | `WebPageAgentOpt` | — | Forwarded to `PlaywrightAgent` (e.g. `aiActionContext`, `modelConfig`). `groupName` and `reportFileName` are managed by the lifecycle. | +| `setup` | `(api) => Promise` | — | See [Custom page setup](#custom-page-setup) below. | + +### Puppeteer (`@midscene/rstest/puppeteer`) + +| Option | Type | Default | Description | +| --- | --- | --- | --- | +| `headless` | `boolean` | `true` in CI, `false` locally | Convenience for `launchOptions.headless`. | +| `viewport` | `{ width, height }` | `1920×1080` | Convenience for `launchOptions.defaultViewport`. | +| `launchOptions` | `LaunchOptions \| (defaults) => LaunchOptions` | — | Forwarded to `puppeteer.launch(...)`. | +| `gotoOptions` | `GoToOptions` | — | Forwarded to `page.goto(url, ...)`. | +| `agentOptions` | `WebPageAgentOpt` | — | Forwarded to `PuppeteerAgent`. | +| `setup` | `(api) => Promise` | — | See [Custom page setup](#custom-page-setup) below. | + +Resolver fields (`launchOptions`, `contextOptions`) accept either an object — shallow-merged over midscene's defaults — or a function `(defaults) => options` that takes full control. + +### Examples + +Override one launch field while keeping midscene's defaults (`--no-sandbox`, etc.): + +```ts +const ctx = createWebTest(url, { + launchOptions: { proxy: { server: 'http://corp-proxy:8080' } }, +}); +``` + +Compose with the function form: + +```ts +const ctx = createWebTest(url, { + launchOptions: (defaults) => ({ + ...defaults, + args: [...(defaults.args ?? []), '--disable-gpu'], + }), +}); +``` + +Configure the context (Playwright): + +```ts +const ctx = createWebTest(url, { + contextOptions: { + locale: 'zh-CN', + storageState: 'auth.json', + recordVideo: { dir: 'midscene_run/video' }, + }, +}); +``` + +### Custom page setup + +For lifecycles the defaults don't cover — persistent context, CDP connect, custom trace orchestration, reusing existing fixtures — pass `setup`. It receives the shared browser launched in `beforeAll` and must return the page midscene should attach the agent to. When `setup` is provided, `headless`, `viewport`, `launchOptions`, `contextOptions`, and `gotoOptions` are all ignored; only `agentOptions` still applies. + +```ts +import { createWebTest } from '@midscene/rstest/playwright'; +import { mkdirSync } from 'node:fs'; +import { join } from 'node:path'; + +const ctx = createWebTest('https://example.com', { + async setup({ browser, url }) { + const context = await browser.newContext({ + viewport: { width: 1280, height: 720 }, + }); + await context.tracing.start({ screenshots: true, snapshots: true }); + const page = await context.newPage(); + await page.goto(url); + + return { + page, + async teardown(testCtx) { + const passed = testCtx.task.result?.status === 'pass'; + if (passed) { + await context.tracing.stop(); + } else { + mkdirSync('midscene_run/trace', { recursive: true }); + await context.tracing.stop({ + path: join('midscene_run/trace', `${testCtx.task.name}.zip`), + }); + } + await context.close(); + }, + }; + }, +}); +``` + +The `teardown` callback runs **before** `agent.destroy()`, so the page is still alive — exactly the right window to stop traces, save videos, dump cookies, etc. If you don't need it, return `{ page }`. + +## Advanced: custom providers + +Need to drive a different browser stack — a remote browser, an Electron app, or your own grid? The lifecycle primitive both providers are built on is exported: + +```ts +import { registerLifecycle } from '@midscene/rstest'; +``` + +It accepts a `LifecycleProvider` describing how to launch a browser, build an Agent, and tear it down. The Playwright and Puppeteer providers are thin wrappers on top of it. + +## More + +- All Agent methods: [API Reference](./api#interaction-methods). +- Working example: [https://github.com/web-infra-dev/midscene-example/tree/main/rstest-demo](https://github.com/web-infra-dev/midscene-example/tree/main/rstest-demo). diff --git a/apps/site/docs/zh/integrate-with-rstest.mdx b/apps/site/docs/zh/integrate-with-rstest.mdx new file mode 100644 index 0000000000..b87ad6234a --- /dev/null +++ b/apps/site/docs/zh/integrate-with-rstest.mdx @@ -0,0 +1,265 @@ +import SetupEnv from './common/setup-env.mdx'; +import { PackageManagerTabs } from '@theme'; + +# 集成到 Rstest + +[Rstest](https://github.com/web-infra-dev/rstest) 是基于 Rspack 的高性能测试框架。`@midscene/rstest` 让你以熟悉的 `describe / it` 风格编写 AI 驱动的端到端测试 —— 浏览器启动、Agent 创建、HTML 报告生成都由 Midscene 自动处理。 + +```ts +import { describe, expect, it } from '@rstest/core'; +import { createWebTest } from '@midscene/rstest/playwright'; + +describe('Todo list', () => { + const ctx = createWebTest('http://localhost:5173/'); + + it('adds a todo', async () => { + const { agent } = ctx; + await agent.aiAct("type 'Study AI' in the input and press Enter"); + await agent.aiAssert('the list contains exactly one item: "Study AI"'); + }); +}); +``` + +开箱即用的能力: + +- 每个 `it` 块都拿到一份全新的 `agent`,可以直接调用 [Agent API](./api#interaction-methods) 中的任意方法(`aiAct`、`aiAssert`、`aiQuery`、`aiTap`、`aiInput`、`aiHover` 等)。 +- 自动管理浏览器生命周期 —— 每个测试文件启动一次浏览器,每个 test 之前打开干净的页面,结束后自动销毁。 +- 每个测试文件结束时输出一份合并后的 Midscene HTML 报告,路径会打印到控制台。 +- 直接 pass-through 到 Playwright / Puppeteer 自身的 options 类型;默认 lifecycle 无法满足时,可用 `setup` 钩子自定义。 + +包内自带 Playwright 与 Puppeteer 两套 provider,通过 subpath export 区分: + +- `@midscene/rstest/playwright` —— 基于 `chromium.launch()`。 +- `@midscene/rstest/puppeteer` —— 基于 `puppeteer.launch()`。 + + + +:::info 样例项目 + +可运行的样例:[https://github.com/web-infra-dev/midscene-example/tree/main/rstest-demo](https://github.com/web-infra-dev/midscene-example/tree/main/rstest-demo)。 + +::: + +## 第一步:安装依赖 + +按需选择 Playwright 或 Puppeteer,与 `@midscene/web`、`@rstest/core`、`@midscene/rstest` 一起安装。 + +使用 Playwright: + + + +或者使用 Puppeteer: + + + +## 第二步:配置 Rstest + +在测试项目根目录新建 `rstest.config.ts`: + +```ts title="rstest.config.ts" +import 'dotenv/config'; // 从 .env 加载模型相关环境变量 +import { defineConfig } from '@rstest/core'; +import MidsceneReporter from '@midscene/rstest/reporter'; + +export default defineConfig({ + include: ['e2e/**/*.test.ts'], + // AI 调用较慢,testTimeout 与 hookTimeout 都留足。 + testTimeout: 1_800_000, + hookTimeout: 60_000, + reporters: ['default', new MidsceneReporter()], +}); +``` + +`MidsceneReporter` 会在每个测试文件结束时打印形如 `Midscene report: midscene_run/report/.html` 的一行。在浏览器中打开这个 HTML,即可逐步回放每个 AI 步骤与对应截图。 + +## 第三步:编写测试 + +```ts title="e2e/todo-list.test.ts" +import { describe, expect, it } from '@rstest/core'; +import { createWebTest } from '@midscene/rstest/playwright'; + +describe('Todo list', () => { + const ctx = createWebTest('http://localhost:5173/'); + + it('adds and completes a todo', async () => { + const { agent } = ctx; + + await agent.aiAct("type 'Study AI' in the input and press Enter"); + await agent.aiAct('click the checkbox of the first item'); + + const list = await agent.aiQuery( + 'string[], the complete task list', + ); + expect(list).toHaveLength(1); + + await agent.aiAssert('the only item is marked as done'); + }); +}); +``` + +`createWebTest(url, options)` 会在内部注册 Rstest 的 `beforeAll` / `beforeEach` / `afterEach` / `afterAll` 钩子。在每个 `describe` 块里调用一次即可,里面的每个 `it` 都会拿到一份重新导航过的全新页面。 + +:::warning `agent` 仅在 `it(...)` 内可用 +agent 会在 `beforeEach` 中创建、`afterEach` 中销毁。在 `describe` 作用域、其他钩子、或 test 结束之后访问 `ctx.agent` 都会抛错。 +::: + +如果想换成 Puppeteer,只需改一行 import: + +```ts +import { createWebTest } from '@midscene/rstest/puppeteer'; +``` + +`agent` 的 API 完全一致。 + +## 第四步:运行测试 + +```bash +npx rstest run + +# 调试时也可以用 watch 模式: +npx rstest watch +``` + +每个文件运行完后会输出: + +```text +Midscene report: midscene_run/report/.html +``` + +报告默认输出到 `midscene_run/report/`。详见[消费报告文件](./consume-report-file)。 + +## 项目级默认配置(可选) + +如果希望所有 `createWebTest` 调用共享同一份配置,把它们写到一个 setup 文件里,并通过 `setupFiles` 注册: + +```ts title="midscene.setup.ts" +import { defineMidsceneDefaults } from '@midscene/rstest/playwright'; + +defineMidsceneDefaults({ + viewport: { width: 1280, height: 720 }, + contextOptions: { locale: 'zh-CN' }, +}); +``` + +```ts title="rstest.config.ts" +export default defineConfig({ + // ... + setupFiles: ['./midscene.setup.ts'], +}); +``` + +调用 `createWebTest(url, options)` 时传入的 options 会按顶层 key 浅合并到这里的默认值上 —— `launchOptions` 这种嵌套字段是**整体替换**,不是深度合并。需要组合可以用 resolver 的函数形式。Puppeteer provider 在 `@midscene/rstest/puppeteer` 提供同名 `defineMidsceneDefaults`。 + +## 选项 + +### Playwright (`@midscene/rstest/playwright`) + +| 选项 | 类型 | 默认值 | 说明 | +| --- | --- | --- | --- | +| `headless` | `boolean` | CI 中 `true`,本地 `false` | `launchOptions.headless` 的便捷写法。 | +| `viewport` | `{ width, height }` | `1920×1080` | `contextOptions.viewport` 的便捷写法。 | +| `launchOptions` | `LaunchOptions \| (defaults) => LaunchOptions` | — | 透传到 `chromium.launch(...)`。 | +| `contextOptions` | `BrowserContextOptions \| (defaults) => BrowserContextOptions` | — | 透传到 `browser.newContext(...)`。 | +| `gotoOptions` | `Parameters[1]` | — | 透传到 `page.goto(url, ...)`。 | +| `agentOptions` | `WebPageAgentOpt` | — | 透传给 `PlaywrightAgent`(如 `aiActionContext`、`modelConfig`)。`groupName` 与 `reportFileName` 由 lifecycle 管理。 | +| `setup` | `(api) => Promise` | — | 详见下方 [自定义 page 设置](#自定义-page-设置)。 | + +### Puppeteer (`@midscene/rstest/puppeteer`) + +| 选项 | 类型 | 默认值 | 说明 | +| --- | --- | --- | --- | +| `headless` | `boolean` | CI 中 `true`,本地 `false` | `launchOptions.headless` 的便捷写法。 | +| `viewport` | `{ width, height }` | `1920×1080` | `launchOptions.defaultViewport` 的便捷写法。 | +| `launchOptions` | `LaunchOptions \| (defaults) => LaunchOptions` | — | 透传到 `puppeteer.launch(...)`。 | +| `gotoOptions` | `GoToOptions` | — | 透传到 `page.goto(url, ...)`。 | +| `agentOptions` | `WebPageAgentOpt` | — | 透传给 `PuppeteerAgent`。 | +| `setup` | `(api) => Promise` | — | 详见下方 [自定义 page 设置](#自定义-page-设置)。 | + +`launchOptions`、`contextOptions` 这类 resolver 字段同时接受两种形式:传对象会和 midscene 的默认值浅合并;传函数 `(defaults) => options` 则完全接管。 + +### 示例 + +只覆盖一个 launch 字段,保留 midscene 的其它默认(`--no-sandbox` 等): + +```ts +const ctx = createWebTest(url, { + launchOptions: { proxy: { server: 'http://corp-proxy:8080' } }, +}); +``` + +用函数形式做组合: + +```ts +const ctx = createWebTest(url, { + launchOptions: (defaults) => ({ + ...defaults, + args: [...(defaults.args ?? []), '--disable-gpu'], + }), +}); +``` + +配置 context(Playwright): + +```ts +const ctx = createWebTest(url, { + contextOptions: { + locale: 'zh-CN', + storageState: 'auth.json', + recordVideo: { dir: 'midscene_run/video' }, + }, +}); +``` + +### 自定义 page 设置 + +默认 lifecycle 无法满足的场景 —— persistent context、CDP connect、自定义 trace 编排、复用现有 fixture —— 用 `setup`。它接收 `beforeAll` 启动好的共享浏览器,返回 midscene 要附着 agent 的 page。传了 `setup` 之后,`headless`、`viewport`、`launchOptions`、`contextOptions`、`gotoOptions` 全部被忽略,只有 `agentOptions` 仍然生效。 + +```ts +import { createWebTest } from '@midscene/rstest/playwright'; +import { mkdirSync } from 'node:fs'; +import { join } from 'node:path'; + +const ctx = createWebTest('https://example.com', { + async setup({ browser, url }) { + const context = await browser.newContext({ + viewport: { width: 1280, height: 720 }, + }); + await context.tracing.start({ screenshots: true, snapshots: true }); + const page = await context.newPage(); + await page.goto(url); + + return { + page, + async teardown(testCtx) { + const passed = testCtx.task.result?.status === 'pass'; + if (passed) { + await context.tracing.stop(); + } else { + mkdirSync('midscene_run/trace', { recursive: true }); + await context.tracing.stop({ + path: join('midscene_run/trace', `${testCtx.task.name}.zip`), + }); + } + await context.close(); + }, + }; + }, +}); +``` + +`teardown` 在 `agent.destroy()` **之前**触发,page 还是活的 —— 这正是停 trace、保存录屏、dump cookies 等操作的窗口期。不需要的话直接返回 `{ page }`。 + +## 进阶:自定义 provider + +如果需要驱动其他浏览器栈(远程浏览器、Electron 应用、自建 grid 等),可以使用底层原语: + +```ts +import { registerLifecycle } from '@midscene/rstest'; +``` + +它接受一个 `LifecycleProvider`,描述如何启动浏览器、构造 Agent、销毁资源。包内的 Playwright / Puppeteer provider 都是它的薄封装。 + +## 更多 + +- 所有 Agent 方法:[API Reference](./api#interaction-methods)。 +- 样例项目:[https://github.com/web-infra-dev/midscene-example/tree/main/rstest-demo](https://github.com/web-infra-dev/midscene-example/tree/main/rstest-demo)。 diff --git a/apps/site/rspress.config.ts b/apps/site/rspress.config.ts index 07fb03b593..ce7b6668cf 100644 --- a/apps/site/rspress.config.ts +++ b/apps/site/rspress.config.ts @@ -101,6 +101,10 @@ export default defineConfig({ text: 'Integrate with Puppeteer', link: '/integrate-with-puppeteer', }, + { + text: 'Integrate with Rstest', + link: '/integrate-with-rstest', + }, { text: 'Bridge to the desktop Chrome', link: '/bridge-mode', @@ -281,6 +285,10 @@ export default defineConfig({ text: '集成到 Puppeteer', link: '/zh/integrate-with-puppeteer', }, + { + text: '集成到 Rstest', + link: '/zh/integrate-with-rstest', + }, { text: '桥接到桌面 Chrome', link: '/zh/bridge-mode', diff --git a/package.json b/package.json index ab5524d442..8034921d85 100644 --- a/package.json +++ b/package.json @@ -7,7 +7,7 @@ "build": "nx run-many --target=build --exclude=doc --verbose", "build:packages": "pnpm -r --filter './packages/**' --if-present run build", "build:skip-cache": "npm run clean && nx run-many --target=build --exclude=doc --verbose --skip-nx-cache", - "test": "nx run-many --target=test --projects=@midscene/core,@midscene/shared,@midscene/visualizer,@midscene/web,@midscene/cli,@midscene/android,@midscene/ios,@midscene/computer,@midscene/android-mcp,@midscene/ios-mcp,@midscene/web-bridge-mcp,@midscene/playground,studio --verbose", + "test": "nx run-many --target=test --projects=@midscene/core,@midscene/shared,@midscene/visualizer,@midscene/web,@midscene/cli,@midscene/android,@midscene/ios,@midscene/computer,@midscene/android-mcp,@midscene/ios-mcp,@midscene/web-bridge-mcp,@midscene/playground,@midscene/rstest,studio --verbose", "test:ai": "nx run-many --target=test:ai --projects=@midscene/core,@midscene/web,@midscene/cli,@midscene/computer --verbose", "e2e": "nx run @midscene/web:e2e --verbose --exclude-task-dependencies", "e2e:cache": "nx run @midscene/web:e2e:cache --verbose --exclude-task-dependencies", diff --git a/packages/rstest/README.md b/packages/rstest/README.md new file mode 100644 index 0000000000..8eb1023dc8 --- /dev/null +++ b/packages/rstest/README.md @@ -0,0 +1,5 @@ +# @midscene/rstest + +Run Midscene AI browser agents as [Rstest](https://rstest.rsbuild.dev/) tests. Provides Playwright and Puppeteer providers plus an Rstest reporter that surfaces Midscene reports. + +- **Integration guide**: diff --git a/packages/rstest/demo/contacts.test.ts b/packages/rstest/demo/contacts.test.ts new file mode 100644 index 0000000000..1a186f202a --- /dev/null +++ b/packages/rstest/demo/contacts.test.ts @@ -0,0 +1,58 @@ +import { createWebTest } from '@midscene/rstest/playwright'; +import { describe, expect, it } from '@rstest/core'; + +const PAGE_URL = + 'https://lf3-static.bytednsdoc.com/obj/eden-cn/nupipfups/Midscene/contacts3.html'; + +describe('Contacts page', () => { + const ctx = createWebTest(PAGE_URL); + + it('renders the smart contacts header and grid', async () => { + const { agent } = ctx; + await agent.aiAssert( + 'the page header reads "Smart Contacts" with the subtitle "Midscene AI-powered Contact Management"', + ); + await agent.aiAssert( + 'a grid of contact cards is visible, each card shows an avatar, a name, a position, and detail rows for phone, email, company, address and last contact date', + ); + }); + + it('lists every contact with the expected fields', async () => { + const { agent } = ctx; + const contacts = await agent.aiQuery< + { name: string; position: string; email: string }[] + >( + 'Array<{name: string, position: string, email: string}>, the name (heading), position (line under the name) and email address shown on every contact card', + ); + + expect(contacts).toHaveLength(5); + const byName = Object.fromEntries(contacts.map((c) => [c.name, c])); + expect(byName['Alice Johnson']?.position).toBe('Senior Software Engineer'); + expect(byName['Alice Johnson']?.email).toBe('alice.johnson@techcorp.com'); + expect(byName['Bob Wilson']?.position).toBe('UI/UX Designer'); + expect(byName['Carol Davis']?.position).toBe('Sales Director'); + expect(byName['David Brown']?.position).toBe('Marketing Manager'); + expect(byName['Emma Taylor']?.position).toBe('HR Manager'); + }); + + it('opens the custom context menu on right-click', async () => { + const { agent } = ctx; + await agent.aiRightClick("Alice Johnson's contact card"); + await agent.aiWaitFor( + 'a context menu is visible with the items "Call Contact", "Send Email", "Send Message", "Edit Contact", "Copy Info" and "Delete Contact"', + { timeoutMs: 10_000 }, + ); + + const items = await agent.aiQuery( + 'string[], the visible text of every item inside the open context menu, in order', + ); + expect(items).toEqual([ + 'Call Contact', + 'Send Email', + 'Send Message', + 'Edit Contact', + 'Copy Info', + 'Delete Contact', + ]); + }); +}); diff --git a/packages/rstest/demo/rstest.config.ts b/packages/rstest/demo/rstest.config.ts new file mode 100644 index 0000000000..0067f04718 --- /dev/null +++ b/packages/rstest/demo/rstest.config.ts @@ -0,0 +1,12 @@ +import MidsceneReporter from '@midscene/rstest/reporter'; +import { defineConfig } from '@rstest/core'; +import dotenv from 'dotenv'; + +dotenv.config({ path: '../../.env' }); + +export default defineConfig({ + include: ['*.test.ts'], + testTimeout: 1_800_000, + hookTimeout: 60_000, + reporters: ['default', new MidsceneReporter()], +}); diff --git a/packages/rstest/package.json b/packages/rstest/package.json new file mode 100644 index 0000000000..17d6778835 --- /dev/null +++ b/packages/rstest/package.json @@ -0,0 +1,89 @@ +{ + "name": "@midscene/rstest", + "description": "Run Midscene AI browser agents as Rstest tests. Provides Playwright and Puppeteer providers plus a Rstest reporter that surfaces Midscene reports.", + "keywords": [ + "rstest", + "midscene", + "AI testing", + "AI UI automation", + "Browser use", + "playwright", + "puppeteer" + ], + "version": "1.7.9", + "repository": "https://github.com/web-infra-dev/midscene", + "homepage": "https://midscenejs.com/", + "type": "module", + "sideEffects": false, + "files": ["dist", "README.md"], + "exports": { + ".": { + "types": "./dist/index.d.ts", + "import": "./dist/index.js" + }, + "./reporter": { + "types": "./dist/reporter.d.ts", + "import": "./dist/reporter.js" + }, + "./playwright": { + "types": "./dist/playwright.d.ts", + "import": "./dist/playwright.js" + }, + "./puppeteer": { + "types": "./dist/puppeteer.d.ts", + "import": "./dist/puppeteer.js" + } + }, + "scripts": { + "dev": "npm run build:watch", + "build": "rslib build", + "build:watch": "rslib build --watch --no-clean", + "test": "rstest run", + "test:demo": "rstest run --config rstest.config.ts --root demo", + "test:watch": "rstest watch", + "typecheck": "tsc --noEmit" + }, + "dependencies": { + "@midscene/shared": "workspace:*", + "std-env": "^4.1.0" + }, + "peerDependencies": { + "@midscene/core": "workspace:*", + "@midscene/web": "workspace:*", + "@playwright/test": "^1.45.0", + "@rstest/core": ">=0.9.0", + "playwright": "^1.45.0", + "puppeteer": ">=20.0.0" + }, + "peerDependenciesMeta": { + "@playwright/test": { + "optional": true + }, + "playwright": { + "optional": true + }, + "puppeteer": { + "optional": true + } + }, + "devDependencies": { + "@midscene/core": "workspace:*", + "@midscene/web": "workspace:*", + "@playwright/test": "^1.45.0", + "@rslib/core": "^0.18.3", + "@rstest/core": "^0.9.9", + "@types/node": "^18.0.0", + "dotenv": "^16.4.5", + "playwright": "^1.45.0", + "puppeteer": "24.6.0", + "typescript": "^5.8.3" + }, + "engines": { + "node": ">=18.19.0" + }, + "publishConfig": { + "access": "public", + "registry": "https://registry.npmjs.org" + }, + "license": "MIT" +} diff --git a/packages/rstest/rslib.config.ts b/packages/rstest/rslib.config.ts new file mode 100644 index 0000000000..f463c73d7e --- /dev/null +++ b/packages/rstest/rslib.config.ts @@ -0,0 +1,23 @@ +import { defineConfig } from '@rslib/core'; + +export default defineConfig({ + lib: [ + { + source: { + entry: { + index: './src/index.ts', + reporter: './src/reporter.ts', + playwright: './src/playwright.ts', + puppeteer: './src/puppeteer.ts', + }, + }, + format: 'esm', + syntax: 'es2022', + dts: { bundle: false }, + }, + ], + output: { + target: 'node', + sourceMap: true, + }, +}); diff --git a/packages/rstest/rstest.config.ts b/packages/rstest/rstest.config.ts new file mode 100644 index 0000000000..d381135fbd --- /dev/null +++ b/packages/rstest/rstest.config.ts @@ -0,0 +1,5 @@ +import { defineConfig } from '@rstest/core'; + +export default defineConfig({ + include: ['tests/unit-test/**/*.test.ts'], +}); diff --git a/packages/rstest/src/index.ts b/packages/rstest/src/index.ts new file mode 100644 index 0000000000..f0fb414731 --- /dev/null +++ b/packages/rstest/src/index.ts @@ -0,0 +1,6 @@ +export { + registerLifecycle, + type AgentBundle, + type LifecycleProvider, +} from './lifecycle'; +export type { RstestTestContext } from './report-helper'; diff --git a/packages/rstest/src/lifecycle.ts b/packages/rstest/src/lifecycle.ts new file mode 100644 index 0000000000..3332f34a05 --- /dev/null +++ b/packages/rstest/src/lifecycle.ts @@ -0,0 +1,99 @@ +import { getDebug } from '@midscene/shared/logger'; +import { afterAll, afterEach, beforeAll, beforeEach } from '@rstest/core'; +import { + type AgentLike, + ReportHelper, + type RstestTestContext, + buildReportMeta, +} from './report-helper'; + +const debug = getDebug('rstest:lifecycle', { console: true }); + +interface SuiteContext { + filepath: string; +} + +export interface AgentBundle { + agent: TAgent; + /** Runs before `agent.destroy()` — use for tasks that need the page alive (e.g. stop trace). */ + teardown?: (testCtx: RstestTestContext) => Promise; +} + +export interface LifecycleProvider< + TAgent extends AgentLike, + TBrowser, + TOptions, +> { + launchBrowser(options: TOptions): Promise; + closeBrowser(browser: TBrowser): Promise; + createAgent( + browser: TBrowser, + url: string, + options: TOptions, + meta: { groupName: string; reportFileName: string }, + ): Promise>; +} + +/** Returned `.agent` is only valid inside `it(...)` — it's created in `beforeEach`. */ +export function registerLifecycle( + url: string, + options: TOptions, + provider: LifecycleProvider, +): { readonly agent: TAgent } { + const reportHelper = new ReportHelper(); + let browser: TBrowser | null = null; + let filepath = ''; + let currentBundle: AgentBundle | null = null; + let startTime = 0; + + beforeAll(async (suite: SuiteContext) => { + filepath = suite.filepath; + reportHelper.reset(); + browser = await provider.launchBrowser(options); + }); + + beforeEach(async (testCtx) => { + if (!browser) throw new Error('[@midscene/rstest] browser not initialized'); + const meta = buildReportMeta(testCtx as RstestTestContext, filepath); + currentBundle = await provider.createAgent(browser, url, options, meta); + startTime = performance.now(); + }); + + afterEach(async (testCtx) => { + const bundle = currentBundle; + currentBundle = null; + + if (bundle?.teardown) { + try { + await bundle.teardown(testCtx as RstestTestContext); + } catch (err) { + debug('provider teardown failed:', err); + } + } + + await reportHelper.collectReport( + bundle?.agent, + bundle ? startTime : undefined, + testCtx as RstestTestContext, + ); + }); + + afterAll(async (suite: SuiteContext) => { + reportHelper.mergeReports(suite.filepath); + if (browser) { + await provider.closeBrowser(browser); + browser = null; + } + }); + + return { + get agent(): TAgent { + if (!currentBundle) { + throw new Error( + '[@midscene/rstest] agent is only available inside `it(...)` blocks', + ); + } + return currentBundle.agent; + }, + }; +} diff --git a/packages/rstest/src/playwright.ts b/packages/rstest/src/playwright.ts new file mode 100644 index 0000000000..03f570d39b --- /dev/null +++ b/packages/rstest/src/playwright.ts @@ -0,0 +1,135 @@ +import { + PlaywrightAgent, + type WebPageAgentOpt, +} from '@midscene/web/playwright'; +import { + type Browser, + type BrowserContextOptions, + type LaunchOptions, + type Page, + chromium, +} from 'playwright'; +import * as playwrightNs from 'playwright'; +import { isCI } from 'std-env'; +import { registerLifecycle } from './lifecycle'; +import { + DEFAULT_BROWSER_ARGS, + DEFAULT_VIEWPORT, + createDefaultsStore, +} from './provider-shared'; +import type { RstestTestContext } from './report-helper'; +import { type Resolver, applyResolver } from './resolve'; + +export type { Resolver }; + +type GoToOptions = NonNullable[1]>; + +export interface SetupApi { + url: string; + browser: Browser; + playwright: typeof playwrightNs; +} + +export interface PageBundle { + page: Page; + /** Runs before `agent.destroy()`, while the page is still alive. */ + teardown?: (testCtx: RstestTestContext) => Promise; +} + +export interface CreateWebTestOptions { + /** Default: `true` in CI, `false` locally. */ + headless?: boolean; + /** Default: 1920×1080. Routed into the default `contextOptions.viewport`. */ + viewport?: { width: number; height: number }; + + launchOptions?: Resolver; + contextOptions?: Resolver; + gotoOptions?: GoToOptions; + + agentOptions?: Omit; + + /** + * Take over the per-test page lifecycle. When provided, midscene skips its + * default page setup; `headless`, `viewport`, `launchOptions`, + * `contextOptions`, and `gotoOptions` are all ignored. Only `agentOptions` + * still applies. + */ + setup?: (api: SetupApi) => Promise; +} + +export interface WebTestContext { + readonly agent: PlaywrightAgent; +} + +const defaultsStore = createDefaultsStore(); + +/** + * Project-wide defaults for `createWebTest`. Call from a `setupFiles` entry. + * Per-call options shallow-merge over these at the top-level key; nested + * fields like `launchOptions` are replaced, not deep-merged. Use the function + * form of a resolver to compose with defaults. + */ +export const defineMidsceneDefaults = defaultsStore.define; + +async function defaultSetup( + api: SetupApi, + opts: CreateWebTestOptions, +): Promise { + const contextOptions = await applyResolver(opts.contextOptions, { + viewport: opts.viewport ?? DEFAULT_VIEWPORT, + }); + const context = await api.browser.newContext(contextOptions); + const page = await context.newPage(); + await page.goto(api.url, opts.gotoOptions); + + return { + page, + async teardown() { + try { + await context.close(); + } catch { + // The user's setup teardown may have already closed the context. + } + }, + }; +} + +export function createWebTest( + url: string, + options: CreateWebTestOptions = {}, +): WebTestContext { + const merged: CreateWebTestOptions = { ...defaultsStore.get(), ...options }; + + return registerLifecycle( + url, + merged, + { + async launchBrowser(opts) { + const launchOptions = await applyResolver(opts.launchOptions, { + headless: opts.headless ?? isCI, + args: DEFAULT_BROWSER_ARGS, + }); + return chromium.launch(launchOptions); + }, + async closeBrowser(browser) { + await browser.close(); + }, + async createAgent(browser, targetUrl, opts, meta) { + const setup = opts.setup ?? ((api) => defaultSetup(api, opts)); + const bundle = await setup({ + url: targetUrl, + browser, + playwright: playwrightNs, + }); + + const agent = new PlaywrightAgent(bundle.page, { + ...opts.agentOptions, + groupName: meta.groupName, + reportFileName: meta.reportFileName, + }); + + return { agent, teardown: bundle.teardown }; + }, + }, + ); +} diff --git a/packages/rstest/src/provider-shared.ts b/packages/rstest/src/provider-shared.ts new file mode 100644 index 0000000000..5f45e21a7d --- /dev/null +++ b/packages/rstest/src/provider-shared.ts @@ -0,0 +1,16 @@ +export const DEFAULT_BROWSER_ARGS = [ + '--no-sandbox', + '--ignore-certificate-errors', +]; + +export const DEFAULT_VIEWPORT = { width: 1920, height: 1080 } as const; + +export function createDefaultsStore() { + let defaults: T = {} as T; + return { + get: (): T => defaults, + define: (next: T): void => { + defaults = { ...defaults, ...next }; + }, + }; +} diff --git a/packages/rstest/src/puppeteer.ts b/packages/rstest/src/puppeteer.ts new file mode 100644 index 0000000000..344a5095b2 --- /dev/null +++ b/packages/rstest/src/puppeteer.ts @@ -0,0 +1,122 @@ +import { PuppeteerAgent, type WebPageAgentOpt } from '@midscene/web/puppeteer'; +import puppeteer, { + type Browser, + type GoToOptions, + type LaunchOptions, + type Page, +} from 'puppeteer'; +import { isCI } from 'std-env'; +import { registerLifecycle } from './lifecycle'; +import { + DEFAULT_BROWSER_ARGS, + DEFAULT_VIEWPORT, + createDefaultsStore, +} from './provider-shared'; +import type { RstestTestContext } from './report-helper'; +import { type Resolver, applyResolver } from './resolve'; + +export type { Resolver }; + +export interface SetupApi { + url: string; + browser: Browser; + puppeteer: typeof puppeteer; +} + +export interface PageBundle { + page: Page; + /** Runs before `agent.destroy()`, while the page is still alive. */ + teardown?: (testCtx: RstestTestContext) => Promise; +} + +export interface CreateWebTestOptions { + /** Default: `true` in CI, `false` locally. */ + headless?: boolean; + /** Default: 1920×1080. Routed into the default `launchOptions.defaultViewport`. */ + viewport?: { width: number; height: number }; + + launchOptions?: Resolver; + gotoOptions?: GoToOptions; + + agentOptions?: Omit; + + /** + * Take over the per-test page lifecycle. When provided, midscene skips its + * default page setup; `headless`, `viewport`, `launchOptions`, and + * `gotoOptions` are all ignored. Only `agentOptions` still applies. + */ + setup?: (api: SetupApi) => Promise; +} + +export interface WebTestContext { + readonly agent: PuppeteerAgent; +} + +const defaultsStore = createDefaultsStore(); + +/** + * Project-wide defaults for `createWebTest`. Call from a `setupFiles` entry. + * Per-call options shallow-merge over these at the top-level key; nested + * fields like `launchOptions` are replaced, not deep-merged. Use the function + * form of a resolver to compose with defaults. + */ +export const defineMidsceneDefaults = defaultsStore.define; + +async function defaultSetup( + api: SetupApi, + opts: CreateWebTestOptions, +): Promise { + const page = await api.browser.newPage(); + await page.goto(api.url, opts.gotoOptions); + return { + page, + async teardown() { + try { + await page.close(); + } catch { + // The user's setup teardown may have already closed the page. + } + }, + }; +} + +export function createWebTest( + url: string, + options: CreateWebTestOptions = {}, +): WebTestContext { + const merged: CreateWebTestOptions = { ...defaultsStore.get(), ...options }; + + return registerLifecycle( + url, + merged, + { + async launchBrowser(opts) { + const launchOptions = await applyResolver(opts.launchOptions, { + headless: opts.headless ?? isCI, + args: DEFAULT_BROWSER_ARGS, + defaultViewport: opts.viewport ?? DEFAULT_VIEWPORT, + }); + return puppeteer.launch(launchOptions); + }, + async closeBrowser(browser) { + await browser.close(); + }, + async createAgent(browser, targetUrl, opts, meta) { + const setup = opts.setup ?? ((api) => defaultSetup(api, opts)); + const bundle = await setup({ + url: targetUrl, + browser, + puppeteer, + }); + + const agent = new PuppeteerAgent(bundle.page, { + ...opts.agentOptions, + groupName: meta.groupName, + reportFileName: meta.reportFileName, + }); + + return { agent, teardown: bundle.teardown }; + }, + }, + ); +} diff --git a/packages/rstest/src/report-helper.ts b/packages/rstest/src/report-helper.ts new file mode 100644 index 0000000000..313737afca --- /dev/null +++ b/packages/rstest/src/report-helper.ts @@ -0,0 +1,105 @@ +import { mkdirSync, writeFileSync } from 'node:fs'; +import { basename, extname, join } from 'node:path'; +import type { TestStatus } from '@midscene/core'; +import { ReportMergingTool } from '@midscene/core/report'; +import { MANIFEST_DIR, generateTimestamp, manifestKey } from './utils'; + +export interface RstestTestContext { + task: { + id: string; + name: string; + result?: { + status: 'pass' | 'fail' | 'skip' | 'todo'; + errors?: Array<{ message?: string }>; + }; + }; +} + +export interface AgentLike { + reportFile?: string | null; + destroy(): Promise; +} + +const STATUS_MAP: Record = { + pass: 'passed', + fail: 'failed', +}; + +function deriveStatus(result: RstestTestContext['task']['result']): TestStatus { + // TODO: rstest may eventually surface a structured timeout flag — until then + // we substring-match the error message the way Vitest does. + if (result?.errors?.[0]?.message?.includes('timed out')) return 'timedOut'; + return STATUS_MAP[result?.status ?? ''] ?? 'passed'; +} + +export class ReportHelper { + private reportTool = new ReportMergingTool(); + private firstReport: string | null = null; + + reset(): void { + this.reportTool = new ReportMergingTool(); + this.firstReport = null; + } + + async collectReport( + agent: AgentLike | undefined, + startTime: number | undefined, + testCtx: RstestTestContext, + ): Promise { + const status = deriveStatus(testCtx.task.result); + + await agent?.destroy(); + + const reportFile = agent?.reportFile; + if (!reportFile) return; + + this.reportTool.append({ + reportFilePath: reportFile, + reportAttributes: { + testId: testCtx.task.id, + testTitle: testCtx.task.name, + testDescription: '', + testDuration: + startTime !== undefined + ? Math.round(performance.now() - startTime) + : 0, + testStatus: status, + }, + }); + this.firstReport ??= reportFile; + } + + mergeReports(filepath: string): string | null { + const base = basename(filepath, extname(filepath)) || 'MergedReport'; + const finalReportName = `E2E-${base}-${generateTimestamp()}`; + + const merged = this.reportTool.mergeReports(finalReportName); + const report = merged ?? this.firstReport; + + if (report) { + mkdirSync(MANIFEST_DIR, { recursive: true }); + writeFileSync(join(MANIFEST_DIR, `${manifestKey(filepath)}.txt`), report); + } + + this.firstReport = null; + return merged; + } +} + +/** + * Rstest doesn't expose the surrounding `describe` name in the test context, + * so we derive `groupName` from the file basename. + */ +export function buildReportMeta( + testCtx: RstestTestContext, + filepath: string, +): { groupName: string; reportFileName: string } { + const base = basename(filepath, extname(filepath)) || 'UnnamedGroup'; + const taskName = testCtx.task.name; + return { + groupName: `E2E: ${base}`, + reportFileName: `E2E-${base}-${taskName}-${generateTimestamp()}`, + }; +} + +export { deriveStatus }; diff --git a/packages/rstest/src/reporter.ts b/packages/rstest/src/reporter.ts new file mode 100644 index 0000000000..cfb161b2ab --- /dev/null +++ b/packages/rstest/src/reporter.ts @@ -0,0 +1,30 @@ +import { readFileSync, rmSync } from 'node:fs'; +import { join } from 'node:path'; +import type { Reporter, TestFileResult } from '@rstest/core'; +import { MANIFEST_DIR, manifestKey } from './utils'; + +// node:util.styleText was added in Node 20; use a raw ANSI escape so the +// Node 18.19+ floor declared in `engines` keeps working. +const cyan = (text: string): string => `\x1b[36m${text}\x1b[0m`; + +export default class MidsceneReporter implements Reporter { + onTestRunStart(): void { + // Pre-clean in case a previous run crashed mid-flight. + rmSync(MANIFEST_DIR, { recursive: true, force: true }); + } + + onTestFileResult(file: TestFileResult): void { + const manifestFile = join( + MANIFEST_DIR, + `${manifestKey(file.testPath)}.txt`, + ); + let report: string; + try { + report = readFileSync(manifestFile, 'utf8').trim(); + } catch { + return; + } + if (!report) return; + console.log(` ${cyan(`Midscene report: ${report}`)}`); + } +} diff --git a/packages/rstest/src/resolve.ts b/packages/rstest/src/resolve.ts new file mode 100644 index 0000000000..cae9da5f96 --- /dev/null +++ b/packages/rstest/src/resolve.ts @@ -0,0 +1,17 @@ +/** + * An options "resolver". Pass an object to shallow-merge over the resolved + * defaults, or a function to receive the defaults and return a fully-controlled + * value (sync or async). + */ +export type Resolver = T | ((defaults: T) => T | Promise); + +export async function applyResolver( + input: Resolver | undefined, + base: T, +): Promise { + if (input === undefined) return base; + if (typeof input === 'function') { + return await (input as (defaults: T) => T | Promise)(base); + } + return { ...base, ...input }; +} diff --git a/packages/rstest/src/utils.ts b/packages/rstest/src/utils.ts new file mode 100644 index 0000000000..fde17494ce --- /dev/null +++ b/packages/rstest/src/utils.ts @@ -0,0 +1,17 @@ +import { createHash } from 'node:crypto'; + +export const MANIFEST_DIR = 'midscene_run/.rstest-manifest'; + +export function generateTimestamp(): string { + const now = new Date(); + const pad = (n: number, w = 2) => String(n).padStart(w, '0'); + return ( + `${now.getFullYear()}${pad(now.getMonth() + 1)}${pad(now.getDate())}` + + `-${pad(now.getHours())}${pad(now.getMinutes())}${pad(now.getSeconds())}` + + `${pad(now.getMilliseconds(), 3)}` + ); +} + +export function manifestKey(testPath: string): string { + return createHash('sha1').update(testPath).digest('hex').slice(0, 16); +} diff --git a/packages/rstest/tests/tsconfig.json b/packages/rstest/tests/tsconfig.json new file mode 100644 index 0000000000..c5c98286ad --- /dev/null +++ b/packages/rstest/tests/tsconfig.json @@ -0,0 +1,9 @@ +{ + "extends": "../tsconfig.json", + "compilerOptions": { + "baseUrl": "../", + "rootDir": "../" + }, + "include": ["**/*", "../src", "../rslib.config.ts", "../rstest.config.ts"], + "exclude": ["**/node_modules"] +} diff --git a/packages/rstest/tests/unit-test/report-helper.test.ts b/packages/rstest/tests/unit-test/report-helper.test.ts new file mode 100644 index 0000000000..2131c0c8c8 --- /dev/null +++ b/packages/rstest/tests/unit-test/report-helper.test.ts @@ -0,0 +1,63 @@ +import { describe, expect, it } from '@rstest/core'; +import { + type RstestTestContext, + buildReportMeta, + deriveStatus, +} from '../../src/report-helper'; + +function ctx( + name: string, + result?: RstestTestContext['task']['result'], +): RstestTestContext { + return { task: { id: 't1', name, result } }; +} + +describe('deriveStatus', () => { + it('maps pass → passed', () => { + expect(deriveStatus({ status: 'pass' })).toBe('passed'); + }); + + it('maps fail → failed', () => { + expect(deriveStatus({ status: 'fail' })).toBe('failed'); + }); + + it('detects timeout from error message substring', () => { + expect( + deriveStatus({ + status: 'fail', + errors: [{ message: 'hook timed out in 60000ms' }], + }), + ).toBe('timedOut'); + }); + + it('falls back to passed when result is missing', () => { + expect(deriveStatus(undefined)).toBe('passed'); + }); + + it('treats skip/todo as passed (no dedicated mapping)', () => { + expect(deriveStatus({ status: 'skip' })).toBe('passed'); + expect(deriveStatus({ status: 'todo' })).toBe('passed'); + }); +}); + +describe('buildReportMeta', () => { + it('derives groupName from file basename without extension', () => { + const meta = buildReportMeta( + ctx('adds a todo'), + '/repo/e2e/todo-list.test.ts', + ); + expect(meta.groupName).toBe('E2E: todo-list.test'); + }); + + it('falls back when filepath has no basename', () => { + const meta = buildReportMeta(ctx('case'), ''); + expect(meta.groupName).toBe('E2E: UnnamedGroup'); + }); + + it('reportFileName embeds basename and task name', () => { + const meta = buildReportMeta(ctx('case A'), '/x/foo.test.ts'); + expect(meta.reportFileName.startsWith('E2E-foo.test-case A-')).toBe(true); + // trailing timestamp + expect(meta.reportFileName).toMatch(/-\d{8}-\d{9}$/); + }); +}); diff --git a/packages/rstest/tests/unit-test/resolve.test.ts b/packages/rstest/tests/unit-test/resolve.test.ts new file mode 100644 index 0000000000..cd53496c0a --- /dev/null +++ b/packages/rstest/tests/unit-test/resolve.test.ts @@ -0,0 +1,54 @@ +import { describe, expect, it } from '@rstest/core'; +import { applyResolver } from '../../src/resolve'; + +describe('applyResolver', () => { + it('returns base when input is undefined', async () => { + const base = { a: 1, b: 2 }; + expect(await applyResolver(undefined, base)).toEqual(base); + }); + + it('shallow-merges object input over base', async () => { + const base = { a: 1, b: 2, c: 3 }; + expect(await applyResolver({ b: 20 }, base)).toEqual({ a: 1, b: 20, c: 3 }); + }); + + it('replaces nested values entirely (no deep merge)', async () => { + const base = { args: ['--no-sandbox'], proxy: { server: 'a' } }; + expect( + await applyResolver( + { proxy: { server: 'b' } } as { proxy: { server: string } }, + base, + ), + ).toEqual({ args: ['--no-sandbox'], proxy: { server: 'b' } }); + }); + + it('calls function with resolved defaults and returns its result', async () => { + const base = { headless: false, args: ['--no-sandbox'] as string[] }; + const result = await applyResolver( + (defaults) => ({ + ...defaults, + args: [...defaults.args, '--disable-gpu'], + }), + base, + ); + expect(result).toEqual({ + headless: false, + args: ['--no-sandbox', '--disable-gpu'], + }); + }); + + it('awaits async resolver functions', async () => { + const base = { a: 1 }; + const result = await applyResolver( + async (defaults) => ({ ...defaults, a: defaults.a + 10 }), + base, + ); + expect(result).toEqual({ a: 11 }); + }); + + it('lets the function fully replace defaults', async () => { + const base = { a: 1, b: 2 }; + const result = await applyResolver(() => ({ a: 99, b: 99 }), base); + expect(result).toEqual({ a: 99, b: 99 }); + }); +}); diff --git a/packages/rstest/tests/unit-test/utils.test.ts b/packages/rstest/tests/unit-test/utils.test.ts new file mode 100644 index 0000000000..89e223f974 --- /dev/null +++ b/packages/rstest/tests/unit-test/utils.test.ts @@ -0,0 +1,26 @@ +import { describe, expect, it } from '@rstest/core'; +import { MANIFEST_DIR, generateTimestamp, manifestKey } from '../../src/utils'; + +describe('utils', () => { + it('MANIFEST_DIR points under midscene_run', () => { + expect(MANIFEST_DIR).toBe('midscene_run/.rstest-manifest'); + }); + + it('manifestKey is deterministic and 16-char hex', () => { + const a = manifestKey('/abs/path/to/foo.test.ts'); + const b = manifestKey('/abs/path/to/foo.test.ts'); + expect(a).toBe(b); + expect(a).toMatch(/^[0-9a-f]{16}$/); + }); + + it('manifestKey changes with input', () => { + const a = manifestKey('/a.test.ts'); + const b = manifestKey('/b.test.ts'); + expect(a).not.toBe(b); + }); + + it('generateTimestamp formats as YYYYMMDD-HHmmssSSS', () => { + const ts = generateTimestamp(); + expect(ts).toMatch(/^\d{8}-\d{9}$/); + }); +}); diff --git a/packages/rstest/tsconfig.json b/packages/rstest/tsconfig.json new file mode 100644 index 0000000000..c670106a2f --- /dev/null +++ b/packages/rstest/tsconfig.json @@ -0,0 +1,21 @@ +{ + "extends": "../shared/tsconfig.base.json", + "compilerOptions": { + "baseUrl": ".", + "rootDir": "./src", + "target": "es2022", + "types": ["node"] + }, + "include": ["src"], + "references": [ + { + "path": "../core" + }, + { + "path": "../shared" + }, + { + "path": "../web-integration" + } + ] +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 0ef4037615..9745895781 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -16,7 +16,7 @@ importers: version: 2.24.1 '@commitlint/cli': specifier: 19.8.0 - version: 19.8.0(@types/node@25.5.2)(typescript@5.8.3) + version: 19.8.0(@types/node@25.6.0)(typescript@5.8.3) '@commitlint/config-conventional': specifier: 19.8.0 version: 19.8.0 @@ -34,7 +34,7 @@ importers: version: 4.1.1 commitizen: specifier: 4.2.5 - version: 4.2.5(@types/node@25.5.2)(typescript@5.8.3) + version: 4.2.5(@types/node@25.6.0)(typescript@5.8.3) cspell-ban-words: specifier: ^0.0.3 version: 0.0.3 @@ -222,7 +222,7 @@ importers: version: 5.8.3 vitest: specifier: 3.0.5 - version: 3.0.5(@types/debug@4.1.12)(@types/node@25.5.2)(jsdom@29.0.2)(less@4.2.2)(lightningcss@1.30.1)(sass-embedded@1.86.3)(terser@5.46.1) + version: 3.0.5(@types/debug@4.1.12)(@types/node@25.6.0)(jsdom@29.0.2)(less@4.2.2)(lightningcss@1.30.1)(sass-embedded@1.86.3)(terser@5.46.2) web-ext: specifier: 9.0.0 version: 9.0.0(body-parser@1.20.3)(express@4.21.2) @@ -417,7 +417,7 @@ importers: version: 1.4.1(@rsbuild/core@1.6.15) '@rsbuild/plugin-type-check': specifier: ^1.3.2 - version: 1.3.2(@rsbuild/core@1.6.15)(@rspack/core@2.0.0-beta.9(@swc/helpers@0.5.21))(typescript@5.8.3) + version: 1.3.2(@rsbuild/core@1.6.15)(@rspack/core@2.0.1(@swc/helpers@0.5.21))(typescript@5.8.3) '@types/chrome': specifier: 0.0.279 version: 0.0.279 @@ -448,7 +448,7 @@ importers: version: 1.2.2(@rsbuild/core@1.6.15)(typescript@5.8.3) '@rsdoctor/rspack-plugin': specifier: 1.0.2 - version: 1.0.2(@rsbuild/core@1.6.15)(@rspack/core@2.0.0-beta.9(@swc/helpers@0.5.21))(bufferutil@4.0.9)(webpack@5.99.5) + version: 1.0.2(@rsbuild/core@1.6.15)(@rspack/core@2.0.1(@swc/helpers@0.5.21))(bufferutil@4.0.9)(webpack@5.99.5) '@types/react': specifier: ^18.3.1 version: 18.3.23 @@ -485,16 +485,16 @@ importers: devDependencies: '@rspress/core': specifier: 2.0.8 - version: 2.0.8(@types/mdast@4.0.4)(@types/react@19.1.5)(core-js@3.47.0)(micromark-util-types@2.0.2)(micromark@4.0.2) + version: 2.0.8(@types/mdast@4.0.4)(@types/react@19.2.14)(core-js@3.47.0)(micromark-util-types@2.0.2)(micromark@4.0.2) '@rspress/plugin-client-redirects': specifier: 2.0.8 - version: 2.0.8(@rspress/core@2.0.8(@types/mdast@4.0.4)(@types/react@19.1.5)(core-js@3.47.0)(micromark-util-types@2.0.2)(micromark@4.0.2)) + version: 2.0.8(@rspress/core@2.0.8(@types/mdast@4.0.4)(@types/react@19.2.14)(core-js@3.47.0)(micromark-util-types@2.0.2)(micromark@4.0.2)) '@rspress/plugin-llms': specifier: 2.0.8 - version: 2.0.8(@rspress/core@2.0.8(@types/mdast@4.0.4)(@types/react@19.1.5)(core-js@3.47.0)(micromark-util-types@2.0.2)(micromark@4.0.2)) + version: 2.0.8(@rspress/core@2.0.8(@types/mdast@4.0.4)(@types/react@19.2.14)(core-js@3.47.0)(micromark-util-types@2.0.2)(micromark@4.0.2)) '@rspress/plugin-sitemap': specifier: 2.0.8 - version: 2.0.8(@rspress/core@2.0.8(@types/mdast@4.0.4)(@types/react@19.1.5)(core-js@3.47.0)(micromark-util-types@2.0.2)(micromark@4.0.2)) + version: 2.0.8(@rspress/core@2.0.8(@types/mdast@4.0.4)(@types/react@19.2.14)(core-js@3.47.0)(micromark-util-types@2.0.2)(micromark@4.0.2)) '@tailwindcss/postcss': specifier: 4.1.11 version: 4.1.11 @@ -625,7 +625,7 @@ importers: version: 5.8.3 vitest: specifier: 3.0.5 - version: 3.0.5(@types/debug@4.1.12)(@types/node@18.19.130)(jsdom@29.0.2)(less@4.3.0)(lightningcss@1.30.1)(sass-embedded@1.86.3)(terser@5.46.1) + version: 3.0.5(@types/debug@4.1.12)(@types/node@18.19.130)(jsdom@29.0.2)(less@4.3.0)(lightningcss@1.30.1)(sass-embedded@1.86.3)(terser@5.46.2) packages/android: dependencies: @@ -684,7 +684,7 @@ importers: version: 6.22.0 vitest: specifier: 3.0.5 - version: 3.0.5(@types/debug@4.1.12)(@types/node@18.19.62)(jsdom@29.0.2)(less@4.3.0)(lightningcss@1.30.1)(sass-embedded@1.86.3)(terser@5.46.1) + version: 3.0.5(@types/debug@4.1.12)(@types/node@18.19.62)(jsdom@29.0.2)(less@4.3.0)(lightningcss@1.30.1)(sass-embedded@1.86.3)(terser@5.46.2) zod: specifier: ^3.25.1 version: 3.25.76 @@ -721,7 +721,7 @@ importers: version: link:../shared '@modelcontextprotocol/inspector': specifier: ^0.16.3 - version: 0.16.3(@types/node@18.19.118)(@types/react-dom@19.1.5(@types/react@19.1.5))(@types/react@19.1.5)(bufferutil@4.0.9)(typescript@5.8.3)(utf-8-validate@6.0.5) + version: 0.16.3(@types/node@18.19.118)(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(bufferutil@4.0.9)(typescript@5.8.3)(utf-8-validate@6.0.5) '@modelcontextprotocol/sdk': specifier: 1.10.2 version: 1.10.2 @@ -742,7 +742,7 @@ importers: version: 5.8.3 vitest: specifier: 3.0.5 - version: 3.0.5(@types/debug@4.1.12)(@types/node@18.19.118)(jsdom@29.0.2)(less@4.3.0)(lightningcss@1.30.1)(sass-embedded@1.86.3)(terser@5.46.1) + version: 3.0.5(@types/debug@4.1.12)(@types/node@18.19.118)(jsdom@29.0.2)(less@4.3.0)(lightningcss@1.30.1)(sass-embedded@1.86.3)(terser@5.46.2) packages/android-playground: dependencies: @@ -882,7 +882,7 @@ importers: version: 5.8.3 vitest: specifier: 3.0.5 - version: 3.0.5(@types/debug@4.1.12)(@types/node@18.19.62)(jsdom@29.0.2)(less@4.3.0)(lightningcss@1.30.1)(sass-embedded@1.86.3)(terser@5.46.1) + version: 3.0.5(@types/debug@4.1.12)(@types/node@18.19.62)(jsdom@29.0.2)(less@4.3.0)(lightningcss@1.30.1)(sass-embedded@1.86.3)(terser@5.46.2) yargs: specifier: 17.7.2 version: 17.7.2 @@ -929,7 +929,7 @@ importers: version: 5.8.3 vitest: specifier: 3.0.5 - version: 3.0.5(@types/debug@4.1.12)(@types/node@18.19.118)(jsdom@29.0.2)(less@4.3.0)(lightningcss@1.30.1)(sass-embedded@1.86.3)(terser@5.46.1) + version: 3.0.5(@types/debug@4.1.12)(@types/node@18.19.118)(jsdom@29.0.2)(less@4.3.0)(lightningcss@1.30.1)(sass-embedded@1.86.3)(terser@5.46.2) packages/computer-linux: dependencies: @@ -998,7 +998,7 @@ importers: version: link:../shared '@modelcontextprotocol/inspector': specifier: ^0.16.3 - version: 0.16.3(@types/node@18.19.118)(@types/react-dom@19.1.5(@types/react@19.1.5))(@types/react@19.1.5)(bufferutil@4.0.9)(typescript@5.8.3)(utf-8-validate@6.0.5) + version: 0.16.3(@types/node@18.19.118)(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(bufferutil@4.0.9)(typescript@5.8.3)(utf-8-validate@6.0.5) '@modelcontextprotocol/sdk': specifier: 1.10.2 version: 1.10.2 @@ -1019,7 +1019,7 @@ importers: version: 5.8.3 vitest: specifier: 3.0.5 - version: 3.0.5(@types/debug@4.1.12)(@types/node@18.19.118)(jsdom@29.0.2)(less@4.3.0)(lightningcss@1.30.1)(sass-embedded@1.86.3)(terser@5.46.1) + version: 3.0.5(@types/debug@4.1.12)(@types/node@18.19.118)(jsdom@29.0.2)(less@4.3.0)(lightningcss@1.30.1)(sass-embedded@1.86.3)(terser@5.46.2) packages/computer-playground: dependencies: @@ -1136,7 +1136,7 @@ importers: version: 5.8.3 vitest: specifier: 3.0.5 - version: 3.0.5(@types/debug@4.1.12)(@types/node@18.19.62)(jsdom@29.0.2)(less@4.3.0)(lightningcss@1.30.1)(sass-embedded@1.86.3)(terser@5.46.1) + version: 3.0.5(@types/debug@4.1.12)(@types/node@18.19.62)(jsdom@29.0.2)(less@4.3.0)(lightningcss@1.30.1)(sass-embedded@1.86.3)(terser@5.46.2) packages/evaluation: dependencies: @@ -1152,7 +1152,7 @@ importers: devDependencies: '@playwright/test': specifier: ^1.45.0 - version: 1.58.1 + version: 1.59.1 cli-progress: specifier: 3.12.0 version: 3.12.0 @@ -1161,7 +1161,7 @@ importers: version: 16.4.5 playwright: specifier: ^1.45.0 - version: 1.58.1 + version: 1.59.1 sharp: specifier: ^0.34.3 version: 0.34.3 @@ -1170,7 +1170,7 @@ importers: version: 5.8.3 vitest: specifier: 3.0.5 - version: 3.0.5(@types/debug@4.1.12)(@types/node@25.5.2)(jsdom@29.0.2)(less@4.3.0)(lightningcss@1.30.1)(sass-embedded@1.86.3)(terser@5.46.1) + version: 3.0.5(@types/debug@4.1.12)(@types/node@25.6.0)(jsdom@29.0.2)(less@4.3.0)(lightningcss@1.30.1)(sass-embedded@1.86.3)(terser@5.46.2) packages/harmony: dependencies: @@ -1204,7 +1204,7 @@ importers: version: 5.8.3 vitest: specifier: 3.0.5 - version: 3.0.5(@types/debug@4.1.12)(@types/node@18.19.118)(jsdom@29.0.2)(less@4.3.0)(lightningcss@1.30.1)(sass-embedded@1.86.3)(terser@5.46.1) + version: 3.0.5(@types/debug@4.1.12)(@types/node@18.19.118)(jsdom@29.0.2)(less@4.3.0)(lightningcss@1.30.1)(sass-embedded@1.86.3)(terser@5.46.2) zod: specifier: ^3.25.1 version: 3.25.76 @@ -1225,7 +1225,7 @@ importers: version: link:../shared '@modelcontextprotocol/inspector': specifier: ^0.16.3 - version: 0.16.3(@types/node@18.19.118)(@types/react-dom@19.1.5(@types/react@19.1.5))(@types/react@19.1.5)(bufferutil@4.0.9)(typescript@5.8.3)(utf-8-validate@6.0.5) + version: 0.16.3(@types/node@18.19.118)(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(bufferutil@4.0.9)(typescript@5.8.3)(utf-8-validate@6.0.5) '@modelcontextprotocol/sdk': specifier: 1.10.2 version: 1.10.2 @@ -1299,7 +1299,7 @@ importers: version: 5.8.3 vitest: specifier: 3.0.5 - version: 3.0.5(@types/debug@4.1.12)(@types/node@18.19.118)(jsdom@29.0.2)(less@4.3.0)(lightningcss@1.30.1)(sass-embedded@1.86.3)(terser@5.46.1) + version: 3.0.5(@types/debug@4.1.12)(@types/node@18.19.118)(jsdom@29.0.2)(less@4.3.0)(lightningcss@1.30.1)(sass-embedded@1.86.3)(terser@5.46.2) zod: specifier: ^3.25.1 version: 3.25.76 @@ -1336,7 +1336,7 @@ importers: version: link:../shared '@modelcontextprotocol/inspector': specifier: ^0.16.3 - version: 0.16.3(@types/node@18.19.118)(@types/react-dom@19.1.5(@types/react@19.1.5))(@types/react@19.1.5)(bufferutil@4.0.9)(typescript@5.8.3)(utf-8-validate@6.0.5) + version: 0.16.3(@types/node@18.19.118)(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(bufferutil@4.0.9)(typescript@5.8.3)(utf-8-validate@6.0.5) '@modelcontextprotocol/sdk': specifier: 1.10.2 version: 1.10.2 @@ -1357,7 +1357,7 @@ importers: version: 5.8.3 vitest: specifier: 3.0.5 - version: 3.0.5(@types/debug@4.1.12)(@types/node@18.19.118)(jsdom@29.0.2)(less@4.3.0)(lightningcss@1.30.1)(sass-embedded@1.86.3)(terser@5.46.1) + version: 3.0.5(@types/debug@4.1.12)(@types/node@18.19.118)(jsdom@29.0.2)(less@4.3.0)(lightningcss@1.30.1)(sass-embedded@1.86.3)(terser@5.46.2) packages/ios-playground: dependencies: @@ -1394,7 +1394,7 @@ importers: version: link:../web-integration '@modelcontextprotocol/inspector': specifier: ^0.16.3 - version: 0.16.3(@types/node@18.19.62)(@types/react-dom@19.1.5(@types/react@19.1.5))(@types/react@19.1.5)(typescript@5.8.3) + version: 0.16.3(@types/node@18.19.62)(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(typescript@5.8.3) '@modelcontextprotocol/sdk': specifier: 1.10.2 version: 1.10.2 @@ -1452,7 +1452,7 @@ importers: version: 5.8.3 vitest: specifier: 3.0.5 - version: 3.0.5(@types/debug@4.1.12)(@types/node@18.19.118)(jsdom@29.0.2)(less@4.3.0)(lightningcss@1.30.1)(sass-embedded@1.86.3)(terser@5.46.1) + version: 3.0.5(@types/debug@4.1.12)(@types/node@18.19.118)(jsdom@29.0.2)(less@4.3.0)(lightningcss@1.30.1)(sass-embedded@1.86.3)(terser@5.46.2) packages/playground-app: dependencies: @@ -1483,16 +1483,16 @@ importers: devDependencies: '@rsbuild/plugin-less': specifier: ^1.5.0 - version: 1.5.0(@rsbuild/core@2.0.0-beta.11(core-js@3.47.0)) + version: 1.5.0(@rsbuild/core@2.0.3(core-js@3.47.0)) '@rsbuild/plugin-node-polyfill': specifier: 1.4.2 - version: 1.4.2(@rsbuild/core@2.0.0-beta.11(core-js@3.47.0)) + version: 1.4.2(@rsbuild/core@2.0.3(core-js@3.47.0)) '@rsbuild/plugin-react': specifier: ^1.4.1 - version: 1.4.5(@rsbuild/core@2.0.0-beta.11(core-js@3.47.0)) + version: 1.4.5(@rsbuild/core@2.0.3(core-js@3.47.0)) '@rsbuild/plugin-svgr': specifier: ^1.2.2 - version: 1.2.2(@rsbuild/core@2.0.0-beta.11(core-js@3.47.0))(typescript@5.8.3) + version: 1.2.2(@rsbuild/core@2.0.3(core-js@3.47.0))(typescript@5.8.3) '@rslib/core': specifier: ^0.18.3 version: 0.18.3(@microsoft/api-extractor@7.52.10(@types/node@18.19.118))(typescript@5.8.3) @@ -1519,7 +1519,7 @@ importers: version: 5.8.3 vitest: specifier: 3.0.5 - version: 3.0.5(@types/debug@4.1.12)(@types/node@18.19.118)(jsdom@29.0.2)(less@4.3.0)(lightningcss@1.30.1)(sass-embedded@1.86.3)(terser@5.46.1) + version: 3.0.5(@types/debug@4.1.12)(@types/node@18.19.118)(jsdom@29.0.2)(less@4.3.0)(lightningcss@1.30.1)(sass-embedded@1.86.3)(terser@5.46.2) packages/recorder: dependencies: @@ -1541,10 +1541,10 @@ importers: devDependencies: '@rsbuild/plugin-react': specifier: ^1.4.1 - version: 1.4.1(@rsbuild/core@2.0.0-beta.11(core-js@3.47.0)) + version: 1.4.1(@rsbuild/core@2.0.3(core-js@3.47.0)) '@rslib/core': specifier: ^0.18.3 - version: 0.18.3(@microsoft/api-extractor@7.52.10(@types/node@25.5.2))(typescript@5.8.3) + version: 0.18.3(@microsoft/api-extractor@7.52.10(@types/node@25.6.0))(typescript@5.8.3) '@types/react': specifier: ^18.3.1 version: 18.3.23 @@ -1555,6 +1555,46 @@ importers: specifier: ^5.8.3 version: 5.8.3 + packages/rstest: + dependencies: + '@midscene/shared': + specifier: workspace:* + version: link:../shared + std-env: + specifier: ^4.1.0 + version: 4.1.0 + devDependencies: + '@midscene/core': + specifier: workspace:* + version: link:../core + '@midscene/web': + specifier: workspace:* + version: link:../web-integration + '@playwright/test': + specifier: ^1.45.0 + version: 1.59.1 + '@rslib/core': + specifier: ^0.18.3 + version: 0.18.3(@microsoft/api-extractor@7.52.10(@types/node@18.19.130))(typescript@5.8.3) + '@rstest/core': + specifier: ^0.9.9 + version: 0.9.9(core-js@3.47.0)(jsdom@29.0.2) + '@types/node': + specifier: ^18.0.0 + version: 18.19.130 + dotenv: + specifier: ^16.4.5 + version: 16.4.7 + playwright: + specifier: ^1.45.0 + version: 1.59.1 + puppeteer: + specifier: 24.6.0 + version: 24.6.0(bufferutil@4.0.9)(typescript@5.8.3)(utf-8-validate@6.0.5) + typescript: + specifier: ^5.8.3 + version: 5.8.3 + packages/shared: dependencies: '@modelcontextprotocol/sdk': @@ -1614,7 +1654,7 @@ importers: version: 5.8.3 vitest: specifier: 3.0.5 - version: 3.0.5(@types/debug@4.1.12)(@types/node@18.19.62)(jsdom@29.0.2)(less@4.3.0)(lightningcss@1.30.1)(sass-embedded@1.86.3)(terser@5.46.1) + version: 3.0.5(@types/debug@4.1.12)(@types/node@18.19.62)(jsdom@29.0.2)(less@4.3.0)(lightningcss@1.30.1)(sass-embedded@1.86.3)(terser@5.46.2) packages/visualizer: dependencies: @@ -1645,16 +1685,16 @@ importers: devDependencies: '@rsbuild/plugin-less': specifier: ^1.5.0 - version: 1.5.0(@rsbuild/core@2.0.0-beta.11(core-js@3.47.0)) + version: 1.5.0(@rsbuild/core@2.0.3(core-js@3.47.0)) '@rsbuild/plugin-node-polyfill': specifier: 1.4.2 - version: 1.4.2(@rsbuild/core@2.0.0-beta.11(core-js@3.47.0)) + version: 1.4.2(@rsbuild/core@2.0.3(core-js@3.47.0)) '@rsbuild/plugin-react': specifier: ^1.4.1 - version: 1.4.1(@rsbuild/core@2.0.0-beta.11(core-js@3.47.0)) + version: 1.4.1(@rsbuild/core@2.0.3(core-js@3.47.0)) '@rsbuild/plugin-svgr': specifier: ^1.2.2 - version: 1.2.2(@rsbuild/core@2.0.0-beta.11(core-js@3.47.0))(typescript@5.8.3) + version: 1.2.2(@rsbuild/core@2.0.3(core-js@3.47.0))(typescript@5.8.3) '@rslib/core': specifier: ^0.18.3 version: 0.18.3(@microsoft/api-extractor@7.52.10(@types/node@18.19.62))(typescript@5.8.3) @@ -1699,7 +1739,7 @@ importers: version: 5.8.3 vitest: specifier: 3.0.5 - version: 3.0.5(@types/debug@4.1.12)(@types/node@18.19.62)(jsdom@29.0.2)(less@4.3.0)(lightningcss@1.30.1)(sass-embedded@1.86.3)(terser@5.46.1) + version: 3.0.5(@types/debug@4.1.12)(@types/node@18.19.62)(jsdom@29.0.2)(less@4.3.0)(lightningcss@1.30.1)(sass-embedded@1.86.3)(terser@5.46.2) zustand: specifier: 4.5.2 version: 4.5.2(@types/react@18.3.23)(immer@10.1.1)(react@18.3.1) @@ -1736,7 +1776,7 @@ importers: version: link:../web-integration '@modelcontextprotocol/inspector': specifier: ^0.16.3 - version: 0.16.3(@types/node@18.19.118)(@types/react-dom@19.1.5(@types/react@19.1.5))(@types/react@19.1.5)(bufferutil@4.0.9)(typescript@5.8.3)(utf-8-validate@6.0.5) + version: 0.16.3(@types/node@18.19.118)(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(bufferutil@4.0.9)(typescript@5.8.3)(utf-8-validate@6.0.5) '@modelcontextprotocol/sdk': specifier: 1.10.2 version: 1.10.2 @@ -1760,7 +1800,7 @@ importers: version: 5.8.3 vitest: specifier: 3.0.5 - version: 3.0.5(@types/debug@4.1.12)(@types/node@18.19.118)(jsdom@29.0.2)(less@4.3.0)(lightningcss@1.30.1)(sass-embedded@1.86.3)(terser@5.46.1) + version: 3.0.5(@types/debug@4.1.12)(@types/node@18.19.118)(jsdom@29.0.2)(less@4.3.0)(lightningcss@1.30.1)(sass-embedded@1.86.3)(terser@5.46.2) packages/web-integration: dependencies: @@ -1803,7 +1843,7 @@ importers: devDependencies: '@playwright/test': specifier: ^1.45.0 - version: 1.58.1 + version: 1.59.1 '@rslib/core': specifier: ^0.18.3 version: 0.18.3(@microsoft/api-extractor@7.52.10(@types/node@18.19.62))(typescript@5.8.3) @@ -1836,7 +1876,7 @@ importers: version: 4.1.0 playwright: specifier: ^1.45.0 - version: 1.58.1 + version: 1.59.1 puppeteer: specifier: 24.6.0 version: 24.6.0(bufferutil@4.0.9)(typescript@5.8.3)(utf-8-validate@6.0.5) @@ -1848,7 +1888,7 @@ importers: version: 5.8.3 vitest: specifier: 3.0.5 - version: 3.0.5(@types/debug@4.1.12)(@types/node@18.19.62)(jsdom@29.0.2)(less@4.3.0)(lightningcss@1.30.1)(sass-embedded@1.86.3)(terser@5.46.1) + version: 3.0.5(@types/debug@4.1.12)(@types/node@18.19.62)(jsdom@29.0.2)(less@4.3.0)(lightningcss@1.30.1)(sass-embedded@1.86.3)(terser@5.46.2) packages/webdriver: dependencies: @@ -1870,7 +1910,7 @@ importers: version: 5.8.3 vitest: specifier: 3.0.5 - version: 3.0.5(@types/debug@4.1.12)(@types/node@18.19.118)(jsdom@29.0.2)(less@4.3.0)(lightningcss@1.30.1)(sass-embedded@1.86.3)(terser@5.46.1) + version: 3.0.5(@types/debug@4.1.12)(@types/node@18.19.118)(jsdom@29.0.2)(less@4.3.0)(lightningcss@1.30.1)(sass-embedded@1.86.3)(terser@5.46.2) packages: @@ -2141,8 +2181,8 @@ packages: resolution: {integrity: sha512-ctxtJ/eA+t+6q2++vj5j7FYX3nRu311q1wfYH3xjlLOsczhlhxAg2FWNUXhpGvAw3BWo1xBcvOV6/YLc2r5FJw==} hasBin: true - '@bufbuild/protobuf@2.11.0': - resolution: {integrity: sha512-sBXGT13cpmPR5BMgHE6UEEfEaShh5Ror6rfN3yEK5si7QVrtZg8LEPQb0VVhiLRUslD2yLnXtnRzG035J/mZXQ==} + '@bufbuild/protobuf@2.12.0': + resolution: {integrity: sha512-B/XlCaFIP8LOwzo+bz5uFzATYokcwCKQcghqnlfwSmM5eX/qTkvDBnDPs+gXtX/RyjxJ4DRikECcPJbyALA8FA==} '@changesets/apply-release-plan@6.1.4': resolution: {integrity: sha512-FMpKF1fRlJyCZVYHr3CbinpZZ+6MwvOtWUuO8uo+svcATEoc1zRDcj23pAurJ2TZ/uVz1wFHH6K3NlACy0PLew==} @@ -2438,15 +2478,24 @@ packages: engines: {node: '>=22.12.0'} hasBin: true + '@emnapi/core@1.10.0': + resolution: {integrity: sha512-yq6OkJ4p82CAfPl0u9mQebQHKPJkY7WrIuk205cTYnYe+k2Z8YBh11FrbRG/H6ihirqcacOgl2BIO8oyMQLeXw==} + '@emnapi/core@1.7.1': resolution: {integrity: sha512-o1uhUASyo921r2XtHYOHy7gdkGLge8ghBEQHMWmyJFoXlpU58kIrhhN3w26lpQb6dspetweapMn2CSNwQ8I4wg==} + '@emnapi/runtime@1.10.0': + resolution: {integrity: sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA==} + '@emnapi/runtime@1.7.1': resolution: {integrity: sha512-PVtJr5CmLwYAU9PZDMITZoR5iAOShYREoR45EyyLrbntV50mdePTgUn4AmOw90Ifcj+x2kRjdzr1HP3RrNiHGA==} '@emnapi/wasi-threads@1.1.0': resolution: {integrity: sha512-WI0DdZ8xFSbgMjR1sFsKABJ/C5OnRrjT06JXbZKexJGrDuPTzZdDYfFlsgcCXCyf+suG5QU2e/y1Wo2V/OapLQ==} + '@emnapi/wasi-threads@1.2.1': + resolution: {integrity: sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w==} + '@emotion/hash@0.8.0': resolution: {integrity: sha512-kBJtf7PH6aWwZ6fka3zQ0p6SBYzx4fl1LoZXE2RrnYST9Xljm7WfKJrU4g/Xr3Beg72MLrp1AWNUmuYJTL7Cow==} @@ -3593,6 +3642,12 @@ packages: '@napi-rs/wasm-runtime@1.1.1': resolution: {integrity: sha512-p64ah1M1ld8xjWv3qbvFwHiFVWrq1yFvV4f7w+mzaqiR4IlSgkqhcRdHwsGgomwzBH51sRY4NEowLxnaBjcW/A==} + '@napi-rs/wasm-runtime@1.1.4': + resolution: {integrity: sha512-3NQNNgA1YSlJb/kMH1ildASP9HW7/7kYnRI2szWJaofaS1hWmbGI4H+d3+22aGzXXN9IJ+n+GiFVcGipJP18ow==} + peerDependencies: + '@emnapi/core': ^1.7.1 + '@emnapi/runtime': ^1.7.1 + '@nodelib/fs.scandir@2.1.5': resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==} engines: {node: '>= 8'} @@ -3719,8 +3774,8 @@ packages: resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==} engines: {node: '>=14'} - '@playwright/test@1.58.1': - resolution: {integrity: sha512-6LdVIUERWxQMmUSSQi0I53GgCBYgM2RpGngCPY7hSeju+VrKjq3lvs7HpJoPbDiY5QM5EYRtRX5fvrinnMAz3w==} + '@playwright/test@1.59.1': + resolution: {integrity: sha512-PG6q63nQg5c9rIi4/Z5lR5IVF7yU5MqmKaPOe0HSc0O2cX1fPi96sUQu5j7eo4gKCkB2AnNGoWt7y4/Xx3Kcqg==} engines: {node: '>=18'} hasBin: true @@ -3745,8 +3800,8 @@ packages: '@protobufjs/base64@1.1.2': resolution: {integrity: sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg==} - '@protobufjs/codegen@2.0.4': - resolution: {integrity: sha512-YyFaikqM5sH0ziFZCN3xDC7zeGaB/d0IUb9CATugHWbd1FRFwWwt4ld4OYMPWu5a3Xe01mGAULCdqhMlPl29Jg==} + '@protobufjs/codegen@2.0.5': + resolution: {integrity: sha512-zgXFLzW3Ap33e6d0Wlj4MGIm6Ce8O89n/apUaGNB/jx+hw+ruWEp7EwGUshdLKVRCxZW12fp9r40E1mQrf/34g==} '@protobufjs/eventemitter@1.1.0': resolution: {integrity: sha512-j9ednRT81vYJ9OfVuXG6ERSTdEL1xVsNgqpkxMsbIabzSo3goCjDIveeGv5d03om39ML71RdmrGNjG5SReBP/Q==} @@ -3757,8 +3812,8 @@ packages: '@protobufjs/float@1.0.2': resolution: {integrity: sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ==} - '@protobufjs/inquire@1.1.0': - resolution: {integrity: sha512-kdSefcPdruJiFMVSbn801t4vFK7KB/5gd2fYvrxhuJYg8ILrmn9SKSX2tZdV6V+ksulWqS7aXjBcRXl3wHoD9Q==} + '@protobufjs/inquire@1.1.1': + resolution: {integrity: sha512-mnzgDV26ueAvk7rsbt9L7bE0SuAoqyuys/sMMrmVcN5x9VsxpcG3rqAUSgDyLp0UZlmNfIbQ4fHfCtreVBk8Ew==} '@protobufjs/path@1.1.2': resolution: {integrity: sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA==} @@ -3766,8 +3821,8 @@ packages: '@protobufjs/pool@1.1.0': resolution: {integrity: sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw==} - '@protobufjs/utf8@1.1.0': - resolution: {integrity: sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw==} + '@protobufjs/utf8@1.1.1': + resolution: {integrity: sha512-oOAWABowe8EAbMyWKM0tYDKi8Yaox52D+HWZhAIJqQXbqe0xI/GV7FhLWqlEKreMkfDjshR5FKgi3mnle0h6Eg==} '@puppeteer/browsers@2.9.0': resolution: {integrity: sha512-8+xM+cFydYET4X/5/3yZMHs7sjS6c9I6H5I3xJdb6cinzxWUT/I2QVw4avxCQ8QDndwdHkG/FiSZIrCjAbaKvQ==} @@ -4308,6 +4363,16 @@ packages: core-js: optional: true + '@rsbuild/core@2.0.3': + resolution: {integrity: sha512-2myp7jUgGen50saxW8OJD/eMVKp7HnuBN5MUzwRb6mDbRZZVpoorfI4LQqiGSBNjGLB6jltvx/R2yHmcmnchwg==} + engines: {node: ^20.19.0 || >=22.12.0} + hasBin: true + peerDependencies: + core-js: '>= 3.0.0' + peerDependenciesMeta: + core-js: + optional: true + '@rsbuild/plugin-check-syntax@1.3.0': resolution: {integrity: sha512-lHrd6hToPFVOGWr0U/Ox7pudHWdhPSFsr2riWpjNRlUuwiXdU2SYMROaVUCrLJvYFzJyEMsFOi1w59rBQCG2HQ==} peerDependencies: @@ -4422,6 +4487,11 @@ packages: cpu: [arm64] os: [darwin] + '@rspack/binding-darwin-arm64@2.0.1': + resolution: {integrity: sha512-CGFO5zmajD1Itch1lxAI7+gvKiagzyqXopHv/jHG9Su2WWQ2/Nhn2/rkSpdp6ptE9ri6+6tCOOahf099/v/Xog==} + cpu: [arm64] + os: [darwin] + '@rspack/binding-darwin-x64@1.6.6': resolution: {integrity: sha512-IcdEG2kOmbPPO70Zl7gDnowDjK7d7C1hWew2vU7dPltr2t1JalRIMnS051lhiur0ULkSxV3cW1zXqv0Oi8AnOg==} cpu: [x64] @@ -4437,6 +4507,11 @@ packages: cpu: [x64] os: [darwin] + '@rspack/binding-darwin-x64@2.0.1': + resolution: {integrity: sha512-2vvBNBoS09/PurupBwSrlTZd8283o00B8v20ncsNUdEff41uCR/hzIrYoTIVWnVST+Gt5O1+cfcfORp397lajg==} + cpu: [x64] + os: [darwin] + '@rspack/binding-linux-arm64-gnu@1.6.6': resolution: {integrity: sha512-rIguCCtlTcwoFlwheDiUgdImk27spuCRn43zGJogARpM/ZYRFKIuSwFDGUtJT2g0TSLUAHUhWAUqC36NwvrbMQ==} cpu: [arm64] @@ -4452,6 +4527,11 @@ packages: cpu: [arm64] os: [linux] + '@rspack/binding-linux-arm64-gnu@2.0.1': + resolution: {integrity: sha512-uvNXk6ahE3AH3h2avnd1Mgno68YQpS4cfX1OkOGWIC/roL+NrOP2XVXV4yfVAoydPALDO7AfbIfN0QdmBK3rsA==} + cpu: [arm64] + os: [linux] + '@rspack/binding-linux-arm64-musl@1.6.6': resolution: {integrity: sha512-x6X6Gr0fUw6qrJGxZt3Rb6oIX+jd9pdcyp0VbtofcLaqGVQbzustYsYnuLATPOys0q4J/4kWnmEhkjLJHwkhpQ==} cpu: [arm64] @@ -4467,6 +4547,11 @@ packages: cpu: [arm64] os: [linux] + '@rspack/binding-linux-arm64-musl@2.0.1': + resolution: {integrity: sha512-S/a6uN9PiZ5O/PjSqyIXhuRC1lVzeJkJV69NeLk5sIEUiDQ/aQGZG97uN+tluwpbo1tPbLJkdHYETfjspOX4Pg==} + cpu: [arm64] + os: [linux] + '@rspack/binding-linux-x64-gnu@1.6.6': resolution: {integrity: sha512-gSlVdASszWHosQKn+nzYOInBijdQboUnmNMGgW9/PijVg3433IvQjzviUuJFno8CMGgrACV9yw+ZFDuK0J57VA==} cpu: [x64] @@ -4482,6 +4567,11 @@ packages: cpu: [x64] os: [linux] + '@rspack/binding-linux-x64-gnu@2.0.1': + resolution: {integrity: sha512-C13Kk0OkZiocZVj187Sf753UH6pDXnuEu6vzUvi3qv9ltibG1ki0H2Y8isXBYL2cHQOV+hk0g1S6/4z3TTB97A==} + cpu: [x64] + os: [linux] + '@rspack/binding-linux-x64-musl@1.6.6': resolution: {integrity: sha512-TZaqVkh7memsTK/hxkOBrbpdzbmBUMea1YnYt++7QjMgco1kWFvAQ+YhAWtIaOaEg8s6C07Lt0Zp8izM2Dja0g==} cpu: [x64] @@ -4497,6 +4587,11 @@ packages: cpu: [x64] os: [linux] + '@rspack/binding-linux-x64-musl@2.0.1': + resolution: {integrity: sha512-TQsiBFpEDGkuvK9tNdGj/Uc+AIytzqhxXH/1jKU6M24cWB1DTw/Cx7DdrkCBDyq3129K3POLdujvbWCGqBzQUw==} + cpu: [x64] + os: [linux] + '@rspack/binding-wasm32-wasi@1.6.6': resolution: {integrity: sha512-W4mWdlLnYrbUaktyHOGNfATblxMTbgF7CBfDw8PhbDtjd2l8e/TnaHgIDkwITHXAOMEF/QEKfo9FtusbcQJNKw==} cpu: [wasm32] @@ -4509,6 +4604,10 @@ packages: resolution: {integrity: sha512-Vl7aDAt7DCqtZ/RJd8hLFjQqufX+efL/XZG3qADsagl/SspH1ItJ7N6X1S8o50eKoshy27Jr7mQYZEdufX9qhQ==} cpu: [wasm32] + '@rspack/binding-wasm32-wasi@2.0.1': + resolution: {integrity: sha512-wk3gyUgBW/ayP49bI54bkY8+EQnfBHxdoe9dz3oobSTZQc8AOWwmUUDEPltW8rUvPOM6dfHECTOUMnfaf2f5yA==} + cpu: [wasm32] + '@rspack/binding-win32-arm64-msvc@1.6.6': resolution: {integrity: sha512-cw5OgxqoDwjoZlk0L3vGEwcjPZsOVFYLwr2ssiC05rsTbhBwxj8coLpAJdvUvbf6C2TTmCB7iPe2sPq1KWD37g==} cpu: [arm64] @@ -4524,6 +4623,11 @@ packages: cpu: [arm64] os: [win32] + '@rspack/binding-win32-arm64-msvc@2.0.1': + resolution: {integrity: sha512-rHjLcy3VcAC3+x+PxH+gwhwv6tPe0JdXTNT5eAOs9wgZIM6T9p4wre49+K4Qy98+Fb7TTbLX0ObUitlOkGwTSA==} + cpu: [arm64] + os: [win32] + '@rspack/binding-win32-ia32-msvc@1.6.6': resolution: {integrity: sha512-M4ruR+VZ59iy+mPjy6FQPT27cOgeytf3wFBrt7e0suKeNLYGxrNyI9YhgpCTY++SMJsAMgRLGDHoI3ZgWulw1Q==} cpu: [ia32] @@ -4539,6 +4643,11 @@ packages: cpu: [ia32] os: [win32] + '@rspack/binding-win32-ia32-msvc@2.0.1': + resolution: {integrity: sha512-Ad1vVqMBBnd4T8rsORngu9sl2kyRTlS4kMlvFudjzl1X2UFArEDBe0YVGNN7ZvahM12CErUx2WiN8Sd8pb+qXQ==} + cpu: [ia32] + os: [win32] + '@rspack/binding-win32-x64-msvc@1.6.6': resolution: {integrity: sha512-q5QTvdhPUh+CA93cQG5zWKRIHMIWPzw+ftFDEwBw52zYdvNAoLniqD8o5Mi8CT0pndhulXgR5aw0Sjd3eMah+A==} cpu: [x64] @@ -4554,6 +4663,11 @@ packages: cpu: [x64] os: [win32] + '@rspack/binding-win32-x64-msvc@2.0.1': + resolution: {integrity: sha512-oPM2Jtm7HOlmxl/aBfleAVlL6t9VeHx6WvEets7BBJMInemFXAQd4CErRqybf7rXutACzLeUWBOue4Jpd1/ykw==} + cpu: [x64] + os: [win32] + '@rspack/binding@1.6.6': resolution: {integrity: sha512-noiV+qhyBTVpvG2M4bnOwKk2Ynl6G47Wf7wpCjPCFr87qr3txNwTTnhkEJEU59yj+VvIhbRD2rf5+9TLoT0Wxg==} @@ -4563,6 +4677,9 @@ packages: '@rspack/binding@2.0.0-beta.9': resolution: {integrity: sha512-QgkOvzl6BJc4Vg5eaY9r7MkHNfXvVZPgTIeYkdBEOYPowdyCLhlG9vH7QltqLKP9KDNel70YIeMyUrpTqez01w==} + '@rspack/binding@2.0.1': + resolution: {integrity: sha512-ynV1gw4KqFtQ0P+ZZh76SUj49wBb2FuHW3zSmHverHWuxBhzvrZS6/dZ+fCFQG8bTTPtrPz0RQUTN3uEDbPVBQ==} + '@rspack/core@1.6.6': resolution: {integrity: sha512-2mR+2YBydlgZ7Q0Rpd6bCC3MBnV9TS0x857K0zIhbDj4BQOqaWVy1n7fx/B3MrS8TR0QCuzKfyDAjNz+XTyJVQ==} engines: {node: '>=18.12.0'} @@ -4593,6 +4710,18 @@ packages: '@swc/helpers': optional: true + '@rspack/core@2.0.1': + resolution: {integrity: sha512-lgfZiExh8kDR/3obgi3RQKwKG5av1Xf5qDN1aVde777W9pbmx0Pqvrww1qtNvJ+gobEjbrrn5HEZWYGe0VLmcA==} + engines: {node: ^20.19.0 || >=22.12.0} + peerDependencies: + '@module-federation/runtime-tools': ^0.24.1 || ^2.0.0 + '@swc/helpers': '>=0.5.1' + peerDependenciesMeta: + '@module-federation/runtime-tools': + optional: true + '@swc/helpers': + optional: true + '@rspack/lite-tapable@1.1.0': resolution: {integrity: sha512-E2B0JhYFmVAwdDiG14+DW0Di4Ze4Jg10Pc4/lILUrd5DRCaklduz2OvJ5HYQ6G+hd+WTzqQb3QnDNfK4yvAFYw==} @@ -4658,6 +4787,19 @@ packages: '@rspress/shared@2.0.8': resolution: {integrity: sha512-kvfBUvMvWcn/7PJHqZxPeu1yblzvAuB1/gk/1orp5KsYu3wbZ7X3Hsm9smDJVs5Plw1iPt67t9fOYNSM0+VjUA==} + '@rstest/core@0.9.9': + resolution: {integrity: sha512-AdlRgvyitoenjBTxlnYHr4rf71as96abmqVjmPXnfc0MyYk563AKMagHoWW5YMQKGCjRcrZx92dDRIUI0Z8Lyg==} + engines: {node: ^20.19.0 || >=22.12.0} + hasBin: true + peerDependencies: + happy-dom: ^20.8.3 + jsdom: '*' + peerDependenciesMeta: + happy-dom: + optional: true + jsdom: + optional: true + '@rushstack/node-core-library@5.14.0': resolution: {integrity: sha512-eRong84/rwQUlATGFW3TMTYVyqL1vfW9Lf10PH+mVGfIb9HzU3h5AASNIw+axnBLjnD0n3rT5uQBwu9fvzATrg==} peerDependencies: @@ -4951,6 +5093,9 @@ packages: '@types/cacheable-request@6.0.3': resolution: {integrity: sha512-IQ3EbTzGxIigb1I3qPZc1rWJnH0BmSKv5QYTalEwweFvyBDLSAe24zP0le/hyi7ecGfZVlIVAg4BZqb8WBwKqw==} + '@types/chai@5.2.3': + resolution: {integrity: sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==} + '@types/chrome@0.0.279': resolution: {integrity: sha512-wl0IxQ2OQiMazPZM5LimHQ7Jwd72/O8UvvzyptplXT2S4eUqXH5C0n8S+v8PtKhyX89p0igCPpNy3Bwksyk57g==} @@ -4969,6 +5114,9 @@ packages: '@types/debug@4.1.12': resolution: {integrity: sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ==} + '@types/deep-eql@4.0.2': + resolution: {integrity: sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==} + '@types/eslint-scope@3.7.7': resolution: {integrity: sha512-MzMFlSLBqNF2gcHWO0G1vP/YQyfvrxZ0bF+u7mzUdZ1/xK4A4sru+nraZz5i3iEIk1l1uyicaDVTB4QbbEkAYg==} @@ -5080,8 +5228,8 @@ packages: '@types/node@24.12.2': resolution: {integrity: sha512-A1sre26ke7HDIuY/M23nd9gfB+nrmhtYyMINbjI1zHJxYteKR6qSMX56FsmjMcDb3SMcjJg5BiRRgOCC/yBD0g==} - '@types/node@25.5.2': - resolution: {integrity: sha512-tO4ZIRKNC+MDWV4qKVZe3Ql/woTnmHDr5JD8UI5hn2pwBrHEwOEMZK7WlNb5RKB6EoJ02gwmQS9OrjuFnZYdpg==} + '@types/node@25.6.0': + resolution: {integrity: sha512-+qIYRKdNYJwY3vRCZMdJbPLJAtGjQBudzZzdzwQYkEPQd+PJGixUL5QfvCLDaULoLv+RhT3LDkwEfKaAkgSmNQ==} '@types/normalize-package-data@2.4.4': resolution: {integrity: sha512-37i+OaWTh9qeK4LSHPsyRC7NahnGotNuZvjLSgcPzblpHB3rrCJxAOgI5gCdKm7coonsaX1Of0ILiTcnZjbfxA==} @@ -5103,16 +5251,16 @@ packages: peerDependencies: '@types/react': ^18.0.0 - '@types/react-dom@19.1.5': - resolution: {integrity: sha512-CMCjrWucUBZvohgZxkjd6S9h0nZxXjzus6yDfUb+xLxYM7VvjKNH1tQrE9GWLql1XoOP4/Ds3bwFqShHUYraGg==} + '@types/react-dom@19.2.3': + resolution: {integrity: sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==} peerDependencies: - '@types/react': ^19.0.0 + '@types/react': ^19.2.0 '@types/react@18.3.23': resolution: {integrity: sha512-/LDXMQh55EzZQ0uVAZmKKhfENivEvWz6E+EYzh+/MCjMhNsotd+ZHhBGIjFDTi6+fz0OhQQQLbTgdQIxxCsC0w==} - '@types/react@19.1.5': - resolution: {integrity: sha512-piErsCVVbpMMT2r7wbawdZsq4xMvIAhQuac2gedQHysu1TZYEigE6pnFfgZT+/jQnrRuF5r+SHzuehFjfRjr4g==} + '@types/react@19.2.14': + resolution: {integrity: sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w==} '@types/responselike@1.0.3': resolution: {integrity: sha512-H/+L+UkTV33uf49PH5pCAUBVPNj2nDBXTN+qS1dOwyyg24l3CcicicCA7ca+HMvJBZcFgl5r8e+RR6elsb4Lyw==} @@ -5422,8 +5570,8 @@ packages: ajv@8.17.1: resolution: {integrity: sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==} - ajv@8.18.0: - resolution: {integrity: sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A==} + ajv@8.20.0: + resolution: {integrity: sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA==} ansi-align@3.0.1: resolution: {integrity: sha512-IOfwwBF5iczOjp/WeY4YxyjqAFMQoZufdQWDd19SEExbVLNXqvpzSJ/M7Za4/sCPmQ0+GRquoA7bGcINcxew6w==} @@ -6440,9 +6588,6 @@ packages: resolution: {integrity: sha512-0LrrStPOdJj+SPCCrGhzryycLjwcgUSHBtxNA8aIDxf0GLsRh1cKYhB00Gd1lDOS4yGH69+SNn13+TWbVHETFQ==} engines: {node: ^10 || ^12.20.0 || ^14.13.0 || >=15.0.0, npm: '>=7.0.0'} - csstype@3.1.3: - resolution: {integrity: sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==} - csstype@3.2.3: resolution: {integrity: sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==} @@ -6816,6 +6961,10 @@ packages: resolution: {integrity: sha512-Qohcme7V1inbAfvjItgw0EaxVX5q2rdVEZHRBrEQdRZTssLDGsL8Lwrznl8oQ/6kuTJONLaDcGjkNP247XEhcA==} engines: {node: '>=10.13.0'} + enhanced-resolve@5.21.0: + resolution: {integrity: sha512-otxSQPw4lkOZWkHpB3zaEQs6gWYEsmX4xQF68ElXC/TWvGxGMSGOvoNbaLXm6/cS/fSfHtsEdw90y20PCd+sCA==} + engines: {node: '>=10.13.0'} + enquirer@2.3.6: resolution: {integrity: sha512-yjNnPr315/FjS4zIsUxYguYUPP2e1NK4d7E7ZOLiyYCcbFBiTMyID+2wvm2w6+pZ/odMA7cRkjhsPbltwBOrLg==} engines: {node: '>=8.6'} @@ -7298,6 +7447,15 @@ packages: debug: optional: true + follow-redirects@1.16.0: + resolution: {integrity: sha512-y5rN/uOsadFT/JfYwhxRS5R7Qce+g3zG97+JrtFZlC9klX/W5hD7iiLzScI4nZqUS7DNUdhPgw4xI8W2LuXlUw==} + engines: {node: '>=4.0'} + peerDependencies: + debug: '*' + peerDependenciesMeta: + debug: + optional: true + for-each@0.3.5: resolution: {integrity: sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==} engines: {node: '>= 0.4'} @@ -8538,8 +8696,8 @@ packages: resolution: {integrity: sha512-OfCBkGEw4nN6JLtgRidPX6QxjBQGQf72q3si2uvqyFEMbycSFFHwAZeXx6cJgFM9wmLrf9zBwCP3Ivqa+LLZPw==} engines: {node: '>=6'} - loader-runner@4.3.1: - resolution: {integrity: sha512-IWqP2SCPhyVFTBtRcgMHdzlf9ul25NwaFx4wCEH/KjAXuuHY4yNjvPXsBokp8jCB936PyWRaPKUNh8NvylLp2Q==} + loader-runner@4.3.2: + resolution: {integrity: sha512-DFEqQ3ihfS9blba08cLfYf1NRAIEm+dDjic073DRDc3/JspI/8wYmtDsHwd3+4hwvdxSK7PGaElfTmm0awWJ4w==} engines: {node: '>=6.11.5'} loader-utils@3.3.1: @@ -9715,13 +9873,13 @@ packages: resolution: {integrity: sha512-NPE8TDbzl/3YQYY7CSS228s3g2ollTFnc+Qi3tqmqJp9Vg2ovUpixcJEo2HJScN2Ez+kEaal6y70c0ehqJBJeA==} engines: {node: '>=10'} - playwright-core@1.58.1: - resolution: {integrity: sha512-bcWzOaTxcW+VOOGBCQgnaKToLJ65d6AqfLVKEWvexyS3AS6rbXl+xdpYRMGSRBClPvyj44njOWoxjNdL/H9UNg==} + playwright-core@1.59.1: + resolution: {integrity: sha512-HBV/RJg81z5BiiZ9yPzIiClYV/QMsDCKUyogwH9p3MCP6IYjUFu/MActgYAvK0oWyV9NlwM3GLBjADyWgydVyg==} engines: {node: '>=18'} hasBin: true - playwright@1.58.1: - resolution: {integrity: sha512-+2uTZHxSCcxjvGc5C891LrS1/NlxglGxzrC4seZiVjcYVQfUa87wBL6rTDqzGjuoWNjnBzRqKmF6zRYGMvQUaQ==} + playwright@1.59.1: + resolution: {integrity: sha512-C8oWjPR3F81yljW9o5OxcWzfh6avkVwDD2VYdwIGqTkl+OGFISgypqzfu7dOe4QNLL2aqcWBmI3PMtLIK233lw==} engines: {node: '>=18'} hasBin: true @@ -10485,6 +10643,11 @@ packages: engines: {node: '>= 0.4'} hasBin: true + resolve@1.22.12: + resolution: {integrity: sha512-TyeJ1zif53BPfHootBGwPRYT1RUt6oGWsaQr8UyZW/eAm9bKoijtvruSDEmZHm92CwS9nj7/fWttqPCgzep8CA==} + engines: {node: '>= 0.4'} + hasBin: true + responselike@2.0.1: resolution: {integrity: sha512-4gl03wn3hj1HP3yzgdI7d3lCkF95F21Pz4BPGvKHinyQzALR5CapwC8yIi0Rh58DEMQ/SguC03wFj2k0M/mHhw==} @@ -11079,6 +11242,9 @@ packages: std-env@3.9.0: resolution: {integrity: sha512-UGvjygr6F6tpH7o2qyqR6QYpwraIjKSdtzyBdyytFOHmPZY917kwdwLG0RbOjWOnKmnm3PeHjaoLLMie7kPLQw==} + std-env@4.1.0: + resolution: {integrity: sha512-Rq7ybcX2RuC55r9oaPVEW7/xu3tj8u4GeBYHBWCychFtzMIr86A7e3PPEBPT37sHStKX3+TiX/Fr/ACmJLVlLQ==} + stop-iteration-iterator@1.1.0: resolution: {integrity: sha512-eLoXW/DHyl62zxY4SCaIgnRhuMr6ri4juEYARS8E6sCEqzKpOiE521Ucofdx+KnDZl5xmvGYaaKCk5FEOxJCoQ==} engines: {node: '>= 0.4'} @@ -11297,6 +11463,10 @@ packages: resolution: {integrity: sha512-1MOpMXuhGzGL5TTCZFItxCc0AARf1EZFQkGqMm7ERKj8+Hgr5oLvJOVFcC+lRmR8hCe2S3jC4T5D7Vg/d7/fhA==} engines: {node: '>=6'} + tapable@2.3.3: + resolution: {integrity: sha512-uxc/zpqFg6x7C8vOE7lh6Lbda8eEL9zmVm/PLeTPBRhh1xCgdWaQ+J1CUieGpIfm2HdtsUpRv+HshiasBMcc6A==} + engines: {node: '>=6'} + tar-fs@3.1.1: resolution: {integrity: sha512-LZA0oaPOc2fVo82Txf3gw+AkEd38szODlptMYejQUhndHMLQ9M059uXR+AfS7DNo0NpINvSqDsvyaCrBVkptWg==} @@ -11324,8 +11494,8 @@ packages: resolution: {integrity: sha512-wK0Ri4fOGjv/XPy8SBHZChl8CM7uMc5VML7SqiQ0zG7+J5Vr+RMQDoHa2CNT6KHUnTGIXH34UDMkPzAUyapBZg==} engines: {node: '>=8'} - terser-webpack-plugin@5.4.0: - resolution: {integrity: sha512-Bn5vxm48flOIfkdl5CaD2+1CiUVbonWQ3KQPyP7/EuIl9Gbzq/gQFOzaMFUEgVjB1396tcK0SG8XcNJ/2kDH8g==} + terser-webpack-plugin@5.5.0: + resolution: {integrity: sha512-UYhptBwhWvfIjKd/UuFo6D8uq9xpGLDK+z8EDsj/zWhrTaH34cKEbrkMKfV5YWqGBvAYA3tlzZbs2R+qYrbQJA==} engines: {node: '>= 10.13.0'} peerDependencies: '@swc/core': '*' @@ -11340,8 +11510,8 @@ packages: uglify-js: optional: true - terser@5.46.1: - resolution: {integrity: sha512-vzCjQO/rgUuK9sf8VJZvjqiqiHFaZLnOiimmUuOKODxWL8mm/xua7viT7aqX7dgPY60otQjUotzFMmCB4VdmqQ==} + terser@5.46.2: + resolution: {integrity: sha512-uxfo9fPcSgLDYob/w1FuL0c99MWiJDnv+5qXSQc5+Ki5NjVNsYi66INnMFBjf6uFz6OnX12piJQPF4IpjJTNTw==} engines: {node: '>=10'} hasBin: true @@ -11408,6 +11578,10 @@ packages: resolution: {integrity: sha512-Zba82s87IFq9A9XmjiX5uZA/ARWDrB03OHlq+Vw1fSdt0I+4/Kutwy8BP4Y/y/aORMo61FQ0vIb5j44vSo5Pkg==} engines: {node: ^18.0.0 || >=20.0.0} + tinypool@2.1.0: + resolution: {integrity: sha512-Pugqs6M0m7Lv1I7FtxN4aoyToKg1C4tu+/381vH35y8oENM/Ai7f7C4StcoK4/+BSw9ebcS8jRiVrORFKCALLw==} + engines: {node: ^20.0.0 || >=22.0.0} + tinyrainbow@2.0.0: resolution: {integrity: sha512-op4nsTR47R6p0vMUUoYl/a+ljLFVtlfaXkLQmqfLR1qHma1h/ysYk4hEXZ880bf2CYgTskvTa/e196Vd5dDQXw==} engines: {node: '>=14.0.0'} @@ -11631,8 +11805,8 @@ packages: undici-types@7.16.0: resolution: {integrity: sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==} - undici-types@7.18.2: - resolution: {integrity: sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w==} + undici-types@7.19.2: + resolution: {integrity: sha512-qYVnV5OEm2AW8cJMCpdV20CDyaN3g0AjDlOGf1OW4iaDEx8MwdtChUp4zu4H0VP3nDRF/8RKWH+IPp9uW0YGZg==} undici@6.22.0: resolution: {integrity: sha512-hU/10obOIu62MGYjdskASR3CUAiYaFTtC9Pa6vHyf//mAipSvSQg6od2CnJswq7fvzNS3zJhxoRkgNVaHurWKw==} @@ -11921,8 +12095,8 @@ packages: engines: {node: '>= 10.13.0'} hasBin: true - webpack-sources@3.3.4: - resolution: {integrity: sha512-7tP1PdV4vF+lYPnkMR0jMY5/la2ub5Fc/8VQrrU+lXkiM6C4TjVfGw7iKfyhnTQOsD+6Q/iKw0eFciziRgD58Q==} + webpack-sources@3.4.0: + resolution: {integrity: sha512-gHwIe1cgBvvfLeu1Yz/dcFpmHfKDVxxyqI+kzqmuxZED81z2ChxpyqPaWcNqigPywhaEke7AjSGga+kxY55gjQ==} engines: {node: '>=10.13.0'} webpack@5.99.5: @@ -12284,7 +12458,7 @@ snapshots: '@emotion/hash': 0.8.0 '@emotion/unitless': 0.7.5 classnames: 2.5.1 - csstype: 3.1.3 + csstype: 3.2.3 rc-util: 5.43.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1) react: 18.3.1 react-dom: 18.3.1(react@18.3.1) @@ -12587,7 +12761,7 @@ snapshots: dependencies: css-tree: 3.2.1 - '@bufbuild/protobuf@2.11.0': + '@bufbuild/protobuf@2.12.0': optional: true '@changesets/apply-release-plan@6.1.4': @@ -12750,11 +12924,11 @@ snapshots: '@colors/colors@1.6.0': {} - '@commitlint/cli@19.8.0(@types/node@25.5.2)(typescript@5.8.3)': + '@commitlint/cli@19.8.0(@types/node@25.6.0)(typescript@5.8.3)': dependencies: '@commitlint/format': 19.8.1 '@commitlint/lint': 19.8.1 - '@commitlint/load': 19.8.1(@types/node@25.5.2)(typescript@5.8.3) + '@commitlint/load': 19.8.1(@types/node@25.6.0)(typescript@5.8.3) '@commitlint/read': 19.8.1 '@commitlint/types': 19.8.1 tinyexec: 0.3.2 @@ -12810,7 +12984,7 @@ snapshots: '@commitlint/rules': 19.8.1 '@commitlint/types': 19.8.1 - '@commitlint/load@19.8.1(@types/node@25.5.2)(typescript@5.8.3)': + '@commitlint/load@19.8.1(@types/node@25.6.0)(typescript@5.8.3)': dependencies: '@commitlint/config-validator': 19.8.1 '@commitlint/execute-rule': 19.8.1 @@ -12818,7 +12992,7 @@ snapshots: '@commitlint/types': 19.8.1 chalk: 5.6.2 cosmiconfig: 9.0.0(typescript@5.8.3) - cosmiconfig-typescript-loader: 6.2.0(@types/node@25.5.2)(cosmiconfig@9.0.0(typescript@5.8.3))(typescript@5.8.3) + cosmiconfig-typescript-loader: 6.2.0(@types/node@25.6.0)(cosmiconfig@9.0.0(typescript@5.8.3))(typescript@5.8.3) lodash.isplainobject: 4.0.6 lodash.merge: 4.6.2 lodash.uniq: 4.5.0 @@ -12826,7 +13000,7 @@ snapshots: - '@types/node' - typescript - '@commitlint/load@20.1.0(@types/node@25.5.2)(typescript@5.8.3)': + '@commitlint/load@20.1.0(@types/node@25.6.0)(typescript@5.8.3)': dependencies: '@commitlint/config-validator': 20.0.0 '@commitlint/execute-rule': 20.0.0 @@ -12834,7 +13008,7 @@ snapshots: '@commitlint/types': 20.0.0 chalk: 5.6.2 cosmiconfig: 9.0.0(typescript@5.8.3) - cosmiconfig-typescript-loader: 6.2.0(@types/node@25.5.2)(cosmiconfig@9.0.0(typescript@5.8.3))(typescript@5.8.3) + cosmiconfig-typescript-loader: 6.2.0(@types/node@25.6.0)(cosmiconfig@9.0.0(typescript@5.8.3))(typescript@5.8.3) lodash.isplainobject: 4.0.6 lodash.merge: 4.6.2 lodash.uniq: 4.5.0 @@ -13113,11 +13287,22 @@ snapshots: transitivePeerDependencies: - supports-color + '@emnapi/core@1.10.0': + dependencies: + '@emnapi/wasi-threads': 1.2.1 + tslib: 2.8.1 + optional: true + '@emnapi/core@1.7.1': dependencies: '@emnapi/wasi-threads': 1.1.0 tslib: 2.8.1 + '@emnapi/runtime@1.10.0': + dependencies: + tslib: 2.8.1 + optional: true + '@emnapi/runtime@1.7.1': dependencies: tslib: 2.8.1 @@ -13126,6 +13311,11 @@ snapshots: dependencies: tslib: 2.8.1 + '@emnapi/wasi-threads@1.2.1': + dependencies: + tslib: 2.8.1 + optional: true + '@emotion/hash@0.8.0': {} '@emotion/unitless@0.7.5': {} @@ -14192,10 +14382,10 @@ snapshots: transitivePeerDependencies: - supports-color - '@mdx-js/react@3.1.1(@types/react@19.1.5)(react@19.2.4)': + '@mdx-js/react@3.1.1(@types/react@19.2.14)(react@19.2.4)': dependencies: '@types/mdx': 2.0.13 - '@types/react': 19.1.5 + '@types/react': 19.2.14 react: 19.2.4 '@microsoft/api-extractor-model@7.30.7(@types/node@18.19.118)': @@ -14207,6 +14397,15 @@ snapshots: - '@types/node' optional: true + '@microsoft/api-extractor-model@7.30.7(@types/node@18.19.130)': + dependencies: + '@microsoft/tsdoc': 0.15.1 + '@microsoft/tsdoc-config': 0.17.1 + '@rushstack/node-core-library': 5.14.0(@types/node@18.19.130) + transitivePeerDependencies: + - '@types/node' + optional: true + '@microsoft/api-extractor-model@7.30.7(@types/node@18.19.62)': dependencies: '@microsoft/tsdoc': 0.15.1 @@ -14216,11 +14415,11 @@ snapshots: - '@types/node' optional: true - '@microsoft/api-extractor-model@7.30.7(@types/node@25.5.2)': + '@microsoft/api-extractor-model@7.30.7(@types/node@25.6.0)': dependencies: '@microsoft/tsdoc': 0.15.1 '@microsoft/tsdoc-config': 0.17.1 - '@rushstack/node-core-library': 5.14.0(@types/node@25.5.2) + '@rushstack/node-core-library': 5.14.0(@types/node@25.6.0) transitivePeerDependencies: - '@types/node' optional: true @@ -14236,7 +14435,26 @@ snapshots: '@rushstack/ts-command-line': 5.0.2(@types/node@18.19.118) lodash: 4.17.23 minimatch: 10.0.3 - resolve: 1.22.11 + resolve: 1.22.12 + semver: 7.5.4 + source-map: 0.6.1 + typescript: 5.8.2 + transitivePeerDependencies: + - '@types/node' + optional: true + + '@microsoft/api-extractor@7.52.10(@types/node@18.19.130)': + dependencies: + '@microsoft/api-extractor-model': 7.30.7(@types/node@18.19.130) + '@microsoft/tsdoc': 0.15.1 + '@microsoft/tsdoc-config': 0.17.1 + '@rushstack/node-core-library': 5.14.0(@types/node@18.19.130) + '@rushstack/rig-package': 0.5.3 + '@rushstack/terminal': 0.15.4(@types/node@18.19.130) + '@rushstack/ts-command-line': 5.0.2(@types/node@18.19.130) + lodash: 4.17.23 + minimatch: 10.0.3 + resolve: 1.22.12 semver: 7.5.4 source-map: 0.6.1 typescript: 5.8.2 @@ -14255,7 +14473,7 @@ snapshots: '@rushstack/ts-command-line': 5.0.2(@types/node@18.19.62) lodash: 4.17.23 minimatch: 10.0.3 - resolve: 1.22.11 + resolve: 1.22.12 semver: 7.5.4 source-map: 0.6.1 typescript: 5.8.2 @@ -14263,18 +14481,18 @@ snapshots: - '@types/node' optional: true - '@microsoft/api-extractor@7.52.10(@types/node@25.5.2)': + '@microsoft/api-extractor@7.52.10(@types/node@25.6.0)': dependencies: - '@microsoft/api-extractor-model': 7.30.7(@types/node@25.5.2) + '@microsoft/api-extractor-model': 7.30.7(@types/node@25.6.0) '@microsoft/tsdoc': 0.15.1 '@microsoft/tsdoc-config': 0.17.1 - '@rushstack/node-core-library': 5.14.0(@types/node@25.5.2) + '@rushstack/node-core-library': 5.14.0(@types/node@25.6.0) '@rushstack/rig-package': 0.5.3 - '@rushstack/terminal': 0.15.4(@types/node@25.5.2) - '@rushstack/ts-command-line': 5.0.2(@types/node@25.5.2) + '@rushstack/terminal': 0.15.4(@types/node@25.6.0) + '@rushstack/ts-command-line': 5.0.2(@types/node@25.6.0) lodash: 4.17.23 minimatch: 10.0.3 - resolve: 1.22.11 + resolve: 1.22.12 semver: 7.5.4 source-map: 0.6.1 typescript: 5.8.2 @@ -14287,7 +14505,7 @@ snapshots: '@microsoft/tsdoc': 0.15.1 ajv: 8.12.0 jju: 1.4.0 - resolve: 1.22.11 + resolve: 1.22.12 optional: true '@microsoft/tsdoc@0.15.1': @@ -14301,23 +14519,23 @@ snapshots: transitivePeerDependencies: - supports-color - '@modelcontextprotocol/inspector-client@0.16.3(@types/react-dom@19.1.5(@types/react@19.1.5))(@types/react@19.1.5)': + '@modelcontextprotocol/inspector-client@0.16.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)': dependencies: '@modelcontextprotocol/sdk': 1.17.2 - '@radix-ui/react-checkbox': 1.3.2(@types/react-dom@19.1.5(@types/react@19.1.5))(@types/react@19.1.5)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@radix-ui/react-dialog': 1.1.14(@types/react-dom@19.1.5(@types/react@19.1.5))(@types/react@19.1.5)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-checkbox': 1.3.2(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-dialog': 1.1.14(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@radix-ui/react-icons': 1.3.2(react@18.3.1) - '@radix-ui/react-label': 2.1.7(@types/react-dom@19.1.5(@types/react@19.1.5))(@types/react@19.1.5)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@radix-ui/react-popover': 1.1.14(@types/react-dom@19.1.5(@types/react@19.1.5))(@types/react@19.1.5)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@radix-ui/react-select': 2.2.5(@types/react-dom@19.1.5(@types/react@19.1.5))(@types/react@19.1.5)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@radix-ui/react-slot': 1.2.3(@types/react@19.1.5)(react@18.3.1) - '@radix-ui/react-tabs': 1.1.12(@types/react-dom@19.1.5(@types/react@19.1.5))(@types/react@19.1.5)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@radix-ui/react-toast': 1.2.14(@types/react-dom@19.1.5(@types/react@19.1.5))(@types/react@19.1.5)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@radix-ui/react-tooltip': 1.2.7(@types/react-dom@19.1.5(@types/react@19.1.5))(@types/react@19.1.5)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-label': 2.1.7(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-popover': 1.1.14(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-select': 2.2.5(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-slot': 1.2.3(@types/react@19.2.14)(react@18.3.1) + '@radix-ui/react-tabs': 1.1.12(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-toast': 1.2.14(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-tooltip': 1.2.7(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) ajv: 6.12.6 class-variance-authority: 0.7.1 clsx: 2.1.1 - cmdk: 1.1.1(@types/react-dom@19.1.5(@types/react@19.1.5))(@types/react@19.1.5)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + cmdk: 1.1.1(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) lucide-react: 0.523.0(react@18.3.1) pkce-challenge: 4.1.0 prismjs: 1.30.0 @@ -14346,10 +14564,10 @@ snapshots: - supports-color - utf-8-validate - '@modelcontextprotocol/inspector@0.16.3(@types/node@18.19.118)(@types/react-dom@19.1.5(@types/react@19.1.5))(@types/react@19.1.5)(bufferutil@4.0.9)(typescript@5.8.3)(utf-8-validate@6.0.5)': + '@modelcontextprotocol/inspector@0.16.3(@types/node@18.19.118)(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(bufferutil@4.0.9)(typescript@5.8.3)(utf-8-validate@6.0.5)': dependencies: '@modelcontextprotocol/inspector-cli': 0.16.3 - '@modelcontextprotocol/inspector-client': 0.16.3(@types/react-dom@19.1.5(@types/react@19.1.5))(@types/react@19.1.5) + '@modelcontextprotocol/inspector-client': 0.16.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14) '@modelcontextprotocol/inspector-server': 0.16.3(bufferutil@4.0.9)(utf-8-validate@6.0.5) '@modelcontextprotocol/sdk': 1.17.2 concurrently: 9.2.0 @@ -14370,10 +14588,10 @@ snapshots: - typescript - utf-8-validate - '@modelcontextprotocol/inspector@0.16.3(@types/node@18.19.62)(@types/react-dom@19.1.5(@types/react@19.1.5))(@types/react@19.1.5)(typescript@5.8.3)': + '@modelcontextprotocol/inspector@0.16.3(@types/node@18.19.62)(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(typescript@5.8.3)': dependencies: '@modelcontextprotocol/inspector-cli': 0.16.3 - '@modelcontextprotocol/inspector-client': 0.16.3(@types/react-dom@19.1.5(@types/react@19.1.5))(@types/react@19.1.5) + '@modelcontextprotocol/inspector-client': 0.16.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14) '@modelcontextprotocol/inspector-server': 0.16.3(bufferutil@4.0.9)(utf-8-validate@6.0.5) '@modelcontextprotocol/sdk': 1.17.2 concurrently: 9.2.0 @@ -14471,6 +14689,13 @@ snapshots: '@tybys/wasm-util': 0.10.1 optional: true + '@napi-rs/wasm-runtime@1.1.4(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)': + dependencies: + '@emnapi/core': 1.10.0 + '@emnapi/runtime': 1.10.0 + '@tybys/wasm-util': 0.10.1 + optional: true + '@nodelib/fs.scandir@2.1.5': dependencies: '@nodelib/fs.stat': 2.0.5 @@ -14592,9 +14817,9 @@ snapshots: '@pkgjs/parseargs@0.11.0': optional: true - '@playwright/test@1.58.1': + '@playwright/test@1.59.1': dependencies: - playwright: 1.58.1 + playwright: 1.59.1 '@pnpm/config.env-replace@1.1.0': {} @@ -14616,7 +14841,7 @@ snapshots: '@protobufjs/base64@1.1.2': optional: true - '@protobufjs/codegen@2.0.4': + '@protobufjs/codegen@2.0.5': optional: true '@protobufjs/eventemitter@1.1.0': @@ -14625,13 +14850,13 @@ snapshots: '@protobufjs/fetch@1.1.0': dependencies: '@protobufjs/aspromise': 1.1.2 - '@protobufjs/inquire': 1.1.0 + '@protobufjs/inquire': 1.1.1 optional: true '@protobufjs/float@1.0.2': optional: true - '@protobufjs/inquire@1.1.0': + '@protobufjs/inquire@1.1.1': optional: true '@protobufjs/path@1.1.2': @@ -14640,7 +14865,7 @@ snapshots: '@protobufjs/pool@1.1.0': optional: true - '@protobufjs/utf8@1.1.0': + '@protobufjs/utf8@1.1.1': optional: true '@puppeteer/browsers@2.9.0': @@ -14662,374 +14887,374 @@ snapshots: '@radix-ui/primitive@1.1.2': {} - '@radix-ui/react-arrow@1.1.7(@types/react-dom@19.1.5(@types/react@19.1.5))(@types/react@19.1.5)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + '@radix-ui/react-arrow@1.1.7(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: - '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.1.5(@types/react@19.1.5))(@types/react@19.1.5)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) react: 18.3.1 react-dom: 18.3.1(react@18.3.1) optionalDependencies: - '@types/react': 19.1.5 - '@types/react-dom': 19.1.5(@types/react@19.1.5) + '@types/react': 19.2.14 + '@types/react-dom': 19.2.3(@types/react@19.2.14) - '@radix-ui/react-checkbox@1.3.2(@types/react-dom@19.1.5(@types/react@19.1.5))(@types/react@19.1.5)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + '@radix-ui/react-checkbox@1.3.2(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: '@radix-ui/primitive': 1.1.2 - '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.1.5)(react@18.3.1) - '@radix-ui/react-context': 1.1.2(@types/react@19.1.5)(react@18.3.1) - '@radix-ui/react-presence': 1.1.4(@types/react-dom@19.1.5(@types/react@19.1.5))(@types/react@19.1.5)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.1.5(@types/react@19.1.5))(@types/react@19.1.5)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.1.5)(react@18.3.1) - '@radix-ui/react-use-previous': 1.1.1(@types/react@19.1.5)(react@18.3.1) - '@radix-ui/react-use-size': 1.1.1(@types/react@19.1.5)(react@18.3.1) + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.14)(react@18.3.1) + '@radix-ui/react-context': 1.1.2(@types/react@19.2.14)(react@18.3.1) + '@radix-ui/react-presence': 1.1.4(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.14)(react@18.3.1) + '@radix-ui/react-use-previous': 1.1.1(@types/react@19.2.14)(react@18.3.1) + '@radix-ui/react-use-size': 1.1.1(@types/react@19.2.14)(react@18.3.1) react: 18.3.1 react-dom: 18.3.1(react@18.3.1) optionalDependencies: - '@types/react': 19.1.5 - '@types/react-dom': 19.1.5(@types/react@19.1.5) + '@types/react': 19.2.14 + '@types/react-dom': 19.2.3(@types/react@19.2.14) - '@radix-ui/react-collection@1.1.7(@types/react-dom@19.1.5(@types/react@19.1.5))(@types/react@19.1.5)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + '@radix-ui/react-collection@1.1.7(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: - '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.1.5)(react@18.3.1) - '@radix-ui/react-context': 1.1.2(@types/react@19.1.5)(react@18.3.1) - '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.1.5(@types/react@19.1.5))(@types/react@19.1.5)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@radix-ui/react-slot': 1.2.3(@types/react@19.1.5)(react@18.3.1) + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.14)(react@18.3.1) + '@radix-ui/react-context': 1.1.2(@types/react@19.2.14)(react@18.3.1) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-slot': 1.2.3(@types/react@19.2.14)(react@18.3.1) react: 18.3.1 react-dom: 18.3.1(react@18.3.1) optionalDependencies: - '@types/react': 19.1.5 - '@types/react-dom': 19.1.5(@types/react@19.1.5) + '@types/react': 19.2.14 + '@types/react-dom': 19.2.3(@types/react@19.2.14) - '@radix-ui/react-compose-refs@1.1.2(@types/react@19.1.5)(react@18.3.1)': + '@radix-ui/react-compose-refs@1.1.2(@types/react@19.2.14)(react@18.3.1)': dependencies: react: 18.3.1 optionalDependencies: - '@types/react': 19.1.5 + '@types/react': 19.2.14 - '@radix-ui/react-context@1.1.2(@types/react@19.1.5)(react@18.3.1)': + '@radix-ui/react-context@1.1.2(@types/react@19.2.14)(react@18.3.1)': dependencies: react: 18.3.1 optionalDependencies: - '@types/react': 19.1.5 + '@types/react': 19.2.14 - '@radix-ui/react-dialog@1.1.14(@types/react-dom@19.1.5(@types/react@19.1.5))(@types/react@19.1.5)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + '@radix-ui/react-dialog@1.1.14(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: '@radix-ui/primitive': 1.1.2 - '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.1.5)(react@18.3.1) - '@radix-ui/react-context': 1.1.2(@types/react@19.1.5)(react@18.3.1) - '@radix-ui/react-dismissable-layer': 1.1.10(@types/react-dom@19.1.5(@types/react@19.1.5))(@types/react@19.1.5)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@radix-ui/react-focus-guards': 1.1.2(@types/react@19.1.5)(react@18.3.1) - '@radix-ui/react-focus-scope': 1.1.7(@types/react-dom@19.1.5(@types/react@19.1.5))(@types/react@19.1.5)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@radix-ui/react-id': 1.1.1(@types/react@19.1.5)(react@18.3.1) - '@radix-ui/react-portal': 1.1.9(@types/react-dom@19.1.5(@types/react@19.1.5))(@types/react@19.1.5)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@radix-ui/react-presence': 1.1.4(@types/react-dom@19.1.5(@types/react@19.1.5))(@types/react@19.1.5)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.1.5(@types/react@19.1.5))(@types/react@19.1.5)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@radix-ui/react-slot': 1.2.3(@types/react@19.1.5)(react@18.3.1) - '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.1.5)(react@18.3.1) + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.14)(react@18.3.1) + '@radix-ui/react-context': 1.1.2(@types/react@19.2.14)(react@18.3.1) + '@radix-ui/react-dismissable-layer': 1.1.10(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-focus-guards': 1.1.2(@types/react@19.2.14)(react@18.3.1) + '@radix-ui/react-focus-scope': 1.1.7(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-id': 1.1.1(@types/react@19.2.14)(react@18.3.1) + '@radix-ui/react-portal': 1.1.9(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-presence': 1.1.4(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-slot': 1.2.3(@types/react@19.2.14)(react@18.3.1) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.14)(react@18.3.1) aria-hidden: 1.2.6 react: 18.3.1 react-dom: 18.3.1(react@18.3.1) - react-remove-scroll: 2.7.1(@types/react@19.1.5)(react@18.3.1) + react-remove-scroll: 2.7.1(@types/react@19.2.14)(react@18.3.1) optionalDependencies: - '@types/react': 19.1.5 - '@types/react-dom': 19.1.5(@types/react@19.1.5) + '@types/react': 19.2.14 + '@types/react-dom': 19.2.3(@types/react@19.2.14) - '@radix-ui/react-direction@1.1.1(@types/react@19.1.5)(react@18.3.1)': + '@radix-ui/react-direction@1.1.1(@types/react@19.2.14)(react@18.3.1)': dependencies: react: 18.3.1 optionalDependencies: - '@types/react': 19.1.5 + '@types/react': 19.2.14 - '@radix-ui/react-dismissable-layer@1.1.10(@types/react-dom@19.1.5(@types/react@19.1.5))(@types/react@19.1.5)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + '@radix-ui/react-dismissable-layer@1.1.10(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: '@radix-ui/primitive': 1.1.2 - '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.1.5)(react@18.3.1) - '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.1.5(@types/react@19.1.5))(@types/react@19.1.5)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.1.5)(react@18.3.1) - '@radix-ui/react-use-escape-keydown': 1.1.1(@types/react@19.1.5)(react@18.3.1) + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.14)(react@18.3.1) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.2.14)(react@18.3.1) + '@radix-ui/react-use-escape-keydown': 1.1.1(@types/react@19.2.14)(react@18.3.1) react: 18.3.1 react-dom: 18.3.1(react@18.3.1) optionalDependencies: - '@types/react': 19.1.5 - '@types/react-dom': 19.1.5(@types/react@19.1.5) + '@types/react': 19.2.14 + '@types/react-dom': 19.2.3(@types/react@19.2.14) - '@radix-ui/react-focus-guards@1.1.2(@types/react@19.1.5)(react@18.3.1)': + '@radix-ui/react-focus-guards@1.1.2(@types/react@19.2.14)(react@18.3.1)': dependencies: react: 18.3.1 optionalDependencies: - '@types/react': 19.1.5 + '@types/react': 19.2.14 - '@radix-ui/react-focus-scope@1.1.7(@types/react-dom@19.1.5(@types/react@19.1.5))(@types/react@19.1.5)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + '@radix-ui/react-focus-scope@1.1.7(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: - '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.1.5)(react@18.3.1) - '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.1.5(@types/react@19.1.5))(@types/react@19.1.5)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.1.5)(react@18.3.1) + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.14)(react@18.3.1) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.2.14)(react@18.3.1) react: 18.3.1 react-dom: 18.3.1(react@18.3.1) optionalDependencies: - '@types/react': 19.1.5 - '@types/react-dom': 19.1.5(@types/react@19.1.5) + '@types/react': 19.2.14 + '@types/react-dom': 19.2.3(@types/react@19.2.14) '@radix-ui/react-icons@1.3.2(react@18.3.1)': dependencies: react: 18.3.1 - '@radix-ui/react-id@1.1.1(@types/react@19.1.5)(react@18.3.1)': + '@radix-ui/react-id@1.1.1(@types/react@19.2.14)(react@18.3.1)': dependencies: - '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.1.5)(react@18.3.1) + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.14)(react@18.3.1) react: 18.3.1 optionalDependencies: - '@types/react': 19.1.5 + '@types/react': 19.2.14 - '@radix-ui/react-label@2.1.7(@types/react-dom@19.1.5(@types/react@19.1.5))(@types/react@19.1.5)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + '@radix-ui/react-label@2.1.7(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: - '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.1.5(@types/react@19.1.5))(@types/react@19.1.5)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) react: 18.3.1 react-dom: 18.3.1(react@18.3.1) optionalDependencies: - '@types/react': 19.1.5 - '@types/react-dom': 19.1.5(@types/react@19.1.5) + '@types/react': 19.2.14 + '@types/react-dom': 19.2.3(@types/react@19.2.14) - '@radix-ui/react-popover@1.1.14(@types/react-dom@19.1.5(@types/react@19.1.5))(@types/react@19.1.5)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + '@radix-ui/react-popover@1.1.14(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: '@radix-ui/primitive': 1.1.2 - '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.1.5)(react@18.3.1) - '@radix-ui/react-context': 1.1.2(@types/react@19.1.5)(react@18.3.1) - '@radix-ui/react-dismissable-layer': 1.1.10(@types/react-dom@19.1.5(@types/react@19.1.5))(@types/react@19.1.5)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@radix-ui/react-focus-guards': 1.1.2(@types/react@19.1.5)(react@18.3.1) - '@radix-ui/react-focus-scope': 1.1.7(@types/react-dom@19.1.5(@types/react@19.1.5))(@types/react@19.1.5)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@radix-ui/react-id': 1.1.1(@types/react@19.1.5)(react@18.3.1) - '@radix-ui/react-popper': 1.2.7(@types/react-dom@19.1.5(@types/react@19.1.5))(@types/react@19.1.5)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@radix-ui/react-portal': 1.1.9(@types/react-dom@19.1.5(@types/react@19.1.5))(@types/react@19.1.5)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@radix-ui/react-presence': 1.1.4(@types/react-dom@19.1.5(@types/react@19.1.5))(@types/react@19.1.5)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.1.5(@types/react@19.1.5))(@types/react@19.1.5)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@radix-ui/react-slot': 1.2.3(@types/react@19.1.5)(react@18.3.1) - '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.1.5)(react@18.3.1) + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.14)(react@18.3.1) + '@radix-ui/react-context': 1.1.2(@types/react@19.2.14)(react@18.3.1) + '@radix-ui/react-dismissable-layer': 1.1.10(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-focus-guards': 1.1.2(@types/react@19.2.14)(react@18.3.1) + '@radix-ui/react-focus-scope': 1.1.7(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-id': 1.1.1(@types/react@19.2.14)(react@18.3.1) + '@radix-ui/react-popper': 1.2.7(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-portal': 1.1.9(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-presence': 1.1.4(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-slot': 1.2.3(@types/react@19.2.14)(react@18.3.1) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.14)(react@18.3.1) aria-hidden: 1.2.6 react: 18.3.1 react-dom: 18.3.1(react@18.3.1) - react-remove-scroll: 2.7.1(@types/react@19.1.5)(react@18.3.1) + react-remove-scroll: 2.7.1(@types/react@19.2.14)(react@18.3.1) optionalDependencies: - '@types/react': 19.1.5 - '@types/react-dom': 19.1.5(@types/react@19.1.5) + '@types/react': 19.2.14 + '@types/react-dom': 19.2.3(@types/react@19.2.14) - '@radix-ui/react-popper@1.2.7(@types/react-dom@19.1.5(@types/react@19.1.5))(@types/react@19.1.5)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + '@radix-ui/react-popper@1.2.7(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: '@floating-ui/react-dom': 2.1.4(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@radix-ui/react-arrow': 1.1.7(@types/react-dom@19.1.5(@types/react@19.1.5))(@types/react@19.1.5)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.1.5)(react@18.3.1) - '@radix-ui/react-context': 1.1.2(@types/react@19.1.5)(react@18.3.1) - '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.1.5(@types/react@19.1.5))(@types/react@19.1.5)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.1.5)(react@18.3.1) - '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.1.5)(react@18.3.1) - '@radix-ui/react-use-rect': 1.1.1(@types/react@19.1.5)(react@18.3.1) - '@radix-ui/react-use-size': 1.1.1(@types/react@19.1.5)(react@18.3.1) + '@radix-ui/react-arrow': 1.1.7(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.14)(react@18.3.1) + '@radix-ui/react-context': 1.1.2(@types/react@19.2.14)(react@18.3.1) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.2.14)(react@18.3.1) + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.14)(react@18.3.1) + '@radix-ui/react-use-rect': 1.1.1(@types/react@19.2.14)(react@18.3.1) + '@radix-ui/react-use-size': 1.1.1(@types/react@19.2.14)(react@18.3.1) '@radix-ui/rect': 1.1.1 react: 18.3.1 react-dom: 18.3.1(react@18.3.1) optionalDependencies: - '@types/react': 19.1.5 - '@types/react-dom': 19.1.5(@types/react@19.1.5) + '@types/react': 19.2.14 + '@types/react-dom': 19.2.3(@types/react@19.2.14) - '@radix-ui/react-portal@1.1.9(@types/react-dom@19.1.5(@types/react@19.1.5))(@types/react@19.1.5)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + '@radix-ui/react-portal@1.1.9(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: - '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.1.5(@types/react@19.1.5))(@types/react@19.1.5)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.1.5)(react@18.3.1) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.14)(react@18.3.1) react: 18.3.1 react-dom: 18.3.1(react@18.3.1) optionalDependencies: - '@types/react': 19.1.5 - '@types/react-dom': 19.1.5(@types/react@19.1.5) + '@types/react': 19.2.14 + '@types/react-dom': 19.2.3(@types/react@19.2.14) - '@radix-ui/react-presence@1.1.4(@types/react-dom@19.1.5(@types/react@19.1.5))(@types/react@19.1.5)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + '@radix-ui/react-presence@1.1.4(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: - '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.1.5)(react@18.3.1) - '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.1.5)(react@18.3.1) + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.14)(react@18.3.1) + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.14)(react@18.3.1) react: 18.3.1 react-dom: 18.3.1(react@18.3.1) optionalDependencies: - '@types/react': 19.1.5 - '@types/react-dom': 19.1.5(@types/react@19.1.5) + '@types/react': 19.2.14 + '@types/react-dom': 19.2.3(@types/react@19.2.14) - '@radix-ui/react-primitive@2.1.3(@types/react-dom@19.1.5(@types/react@19.1.5))(@types/react@19.1.5)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + '@radix-ui/react-primitive@2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: - '@radix-ui/react-slot': 1.2.3(@types/react@19.1.5)(react@18.3.1) + '@radix-ui/react-slot': 1.2.3(@types/react@19.2.14)(react@18.3.1) react: 18.3.1 react-dom: 18.3.1(react@18.3.1) optionalDependencies: - '@types/react': 19.1.5 - '@types/react-dom': 19.1.5(@types/react@19.1.5) + '@types/react': 19.2.14 + '@types/react-dom': 19.2.3(@types/react@19.2.14) - '@radix-ui/react-roving-focus@1.1.10(@types/react-dom@19.1.5(@types/react@19.1.5))(@types/react@19.1.5)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + '@radix-ui/react-roving-focus@1.1.10(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: '@radix-ui/primitive': 1.1.2 - '@radix-ui/react-collection': 1.1.7(@types/react-dom@19.1.5(@types/react@19.1.5))(@types/react@19.1.5)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.1.5)(react@18.3.1) - '@radix-ui/react-context': 1.1.2(@types/react@19.1.5)(react@18.3.1) - '@radix-ui/react-direction': 1.1.1(@types/react@19.1.5)(react@18.3.1) - '@radix-ui/react-id': 1.1.1(@types/react@19.1.5)(react@18.3.1) - '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.1.5(@types/react@19.1.5))(@types/react@19.1.5)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.1.5)(react@18.3.1) - '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.1.5)(react@18.3.1) + '@radix-ui/react-collection': 1.1.7(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.14)(react@18.3.1) + '@radix-ui/react-context': 1.1.2(@types/react@19.2.14)(react@18.3.1) + '@radix-ui/react-direction': 1.1.1(@types/react@19.2.14)(react@18.3.1) + '@radix-ui/react-id': 1.1.1(@types/react@19.2.14)(react@18.3.1) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.2.14)(react@18.3.1) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.14)(react@18.3.1) react: 18.3.1 react-dom: 18.3.1(react@18.3.1) optionalDependencies: - '@types/react': 19.1.5 - '@types/react-dom': 19.1.5(@types/react@19.1.5) + '@types/react': 19.2.14 + '@types/react-dom': 19.2.3(@types/react@19.2.14) - '@radix-ui/react-select@2.2.5(@types/react-dom@19.1.5(@types/react@19.1.5))(@types/react@19.1.5)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + '@radix-ui/react-select@2.2.5(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: '@radix-ui/number': 1.1.1 '@radix-ui/primitive': 1.1.2 - '@radix-ui/react-collection': 1.1.7(@types/react-dom@19.1.5(@types/react@19.1.5))(@types/react@19.1.5)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.1.5)(react@18.3.1) - '@radix-ui/react-context': 1.1.2(@types/react@19.1.5)(react@18.3.1) - '@radix-ui/react-direction': 1.1.1(@types/react@19.1.5)(react@18.3.1) - '@radix-ui/react-dismissable-layer': 1.1.10(@types/react-dom@19.1.5(@types/react@19.1.5))(@types/react@19.1.5)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@radix-ui/react-focus-guards': 1.1.2(@types/react@19.1.5)(react@18.3.1) - '@radix-ui/react-focus-scope': 1.1.7(@types/react-dom@19.1.5(@types/react@19.1.5))(@types/react@19.1.5)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@radix-ui/react-id': 1.1.1(@types/react@19.1.5)(react@18.3.1) - '@radix-ui/react-popper': 1.2.7(@types/react-dom@19.1.5(@types/react@19.1.5))(@types/react@19.1.5)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@radix-ui/react-portal': 1.1.9(@types/react-dom@19.1.5(@types/react@19.1.5))(@types/react@19.1.5)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.1.5(@types/react@19.1.5))(@types/react@19.1.5)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@radix-ui/react-slot': 1.2.3(@types/react@19.1.5)(react@18.3.1) - '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.1.5)(react@18.3.1) - '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.1.5)(react@18.3.1) - '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.1.5)(react@18.3.1) - '@radix-ui/react-use-previous': 1.1.1(@types/react@19.1.5)(react@18.3.1) - '@radix-ui/react-visually-hidden': 1.2.3(@types/react-dom@19.1.5(@types/react@19.1.5))(@types/react@19.1.5)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-collection': 1.1.7(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.14)(react@18.3.1) + '@radix-ui/react-context': 1.1.2(@types/react@19.2.14)(react@18.3.1) + '@radix-ui/react-direction': 1.1.1(@types/react@19.2.14)(react@18.3.1) + '@radix-ui/react-dismissable-layer': 1.1.10(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-focus-guards': 1.1.2(@types/react@19.2.14)(react@18.3.1) + '@radix-ui/react-focus-scope': 1.1.7(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-id': 1.1.1(@types/react@19.2.14)(react@18.3.1) + '@radix-ui/react-popper': 1.2.7(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-portal': 1.1.9(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-slot': 1.2.3(@types/react@19.2.14)(react@18.3.1) + '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.2.14)(react@18.3.1) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.14)(react@18.3.1) + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.14)(react@18.3.1) + '@radix-ui/react-use-previous': 1.1.1(@types/react@19.2.14)(react@18.3.1) + '@radix-ui/react-visually-hidden': 1.2.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) aria-hidden: 1.2.6 react: 18.3.1 react-dom: 18.3.1(react@18.3.1) - react-remove-scroll: 2.7.1(@types/react@19.1.5)(react@18.3.1) + react-remove-scroll: 2.7.1(@types/react@19.2.14)(react@18.3.1) optionalDependencies: - '@types/react': 19.1.5 - '@types/react-dom': 19.1.5(@types/react@19.1.5) + '@types/react': 19.2.14 + '@types/react-dom': 19.2.3(@types/react@19.2.14) - '@radix-ui/react-slot@1.2.3(@types/react@19.1.5)(react@18.3.1)': + '@radix-ui/react-slot@1.2.3(@types/react@19.2.14)(react@18.3.1)': dependencies: - '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.1.5)(react@18.3.1) + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.14)(react@18.3.1) react: 18.3.1 optionalDependencies: - '@types/react': 19.1.5 + '@types/react': 19.2.14 - '@radix-ui/react-tabs@1.1.12(@types/react-dom@19.1.5(@types/react@19.1.5))(@types/react@19.1.5)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + '@radix-ui/react-tabs@1.1.12(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: '@radix-ui/primitive': 1.1.2 - '@radix-ui/react-context': 1.1.2(@types/react@19.1.5)(react@18.3.1) - '@radix-ui/react-direction': 1.1.1(@types/react@19.1.5)(react@18.3.1) - '@radix-ui/react-id': 1.1.1(@types/react@19.1.5)(react@18.3.1) - '@radix-ui/react-presence': 1.1.4(@types/react-dom@19.1.5(@types/react@19.1.5))(@types/react@19.1.5)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.1.5(@types/react@19.1.5))(@types/react@19.1.5)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@radix-ui/react-roving-focus': 1.1.10(@types/react-dom@19.1.5(@types/react@19.1.5))(@types/react@19.1.5)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.1.5)(react@18.3.1) + '@radix-ui/react-context': 1.1.2(@types/react@19.2.14)(react@18.3.1) + '@radix-ui/react-direction': 1.1.1(@types/react@19.2.14)(react@18.3.1) + '@radix-ui/react-id': 1.1.1(@types/react@19.2.14)(react@18.3.1) + '@radix-ui/react-presence': 1.1.4(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-roving-focus': 1.1.10(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.14)(react@18.3.1) react: 18.3.1 react-dom: 18.3.1(react@18.3.1) optionalDependencies: - '@types/react': 19.1.5 - '@types/react-dom': 19.1.5(@types/react@19.1.5) + '@types/react': 19.2.14 + '@types/react-dom': 19.2.3(@types/react@19.2.14) - '@radix-ui/react-toast@1.2.14(@types/react-dom@19.1.5(@types/react@19.1.5))(@types/react@19.1.5)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + '@radix-ui/react-toast@1.2.14(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: '@radix-ui/primitive': 1.1.2 - '@radix-ui/react-collection': 1.1.7(@types/react-dom@19.1.5(@types/react@19.1.5))(@types/react@19.1.5)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.1.5)(react@18.3.1) - '@radix-ui/react-context': 1.1.2(@types/react@19.1.5)(react@18.3.1) - '@radix-ui/react-dismissable-layer': 1.1.10(@types/react-dom@19.1.5(@types/react@19.1.5))(@types/react@19.1.5)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@radix-ui/react-portal': 1.1.9(@types/react-dom@19.1.5(@types/react@19.1.5))(@types/react@19.1.5)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@radix-ui/react-presence': 1.1.4(@types/react-dom@19.1.5(@types/react@19.1.5))(@types/react@19.1.5)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.1.5(@types/react@19.1.5))(@types/react@19.1.5)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.1.5)(react@18.3.1) - '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.1.5)(react@18.3.1) - '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.1.5)(react@18.3.1) - '@radix-ui/react-visually-hidden': 1.2.3(@types/react-dom@19.1.5(@types/react@19.1.5))(@types/react@19.1.5)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-collection': 1.1.7(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.14)(react@18.3.1) + '@radix-ui/react-context': 1.1.2(@types/react@19.2.14)(react@18.3.1) + '@radix-ui/react-dismissable-layer': 1.1.10(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-portal': 1.1.9(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-presence': 1.1.4(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.2.14)(react@18.3.1) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.14)(react@18.3.1) + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.14)(react@18.3.1) + '@radix-ui/react-visually-hidden': 1.2.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) react: 18.3.1 react-dom: 18.3.1(react@18.3.1) optionalDependencies: - '@types/react': 19.1.5 - '@types/react-dom': 19.1.5(@types/react@19.1.5) + '@types/react': 19.2.14 + '@types/react-dom': 19.2.3(@types/react@19.2.14) - '@radix-ui/react-tooltip@1.2.7(@types/react-dom@19.1.5(@types/react@19.1.5))(@types/react@19.1.5)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + '@radix-ui/react-tooltip@1.2.7(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: '@radix-ui/primitive': 1.1.2 - '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.1.5)(react@18.3.1) - '@radix-ui/react-context': 1.1.2(@types/react@19.1.5)(react@18.3.1) - '@radix-ui/react-dismissable-layer': 1.1.10(@types/react-dom@19.1.5(@types/react@19.1.5))(@types/react@19.1.5)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@radix-ui/react-id': 1.1.1(@types/react@19.1.5)(react@18.3.1) - '@radix-ui/react-popper': 1.2.7(@types/react-dom@19.1.5(@types/react@19.1.5))(@types/react@19.1.5)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@radix-ui/react-portal': 1.1.9(@types/react-dom@19.1.5(@types/react@19.1.5))(@types/react@19.1.5)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@radix-ui/react-presence': 1.1.4(@types/react-dom@19.1.5(@types/react@19.1.5))(@types/react@19.1.5)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.1.5(@types/react@19.1.5))(@types/react@19.1.5)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@radix-ui/react-slot': 1.2.3(@types/react@19.1.5)(react@18.3.1) - '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.1.5)(react@18.3.1) - '@radix-ui/react-visually-hidden': 1.2.3(@types/react-dom@19.1.5(@types/react@19.1.5))(@types/react@19.1.5)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.14)(react@18.3.1) + '@radix-ui/react-context': 1.1.2(@types/react@19.2.14)(react@18.3.1) + '@radix-ui/react-dismissable-layer': 1.1.10(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-id': 1.1.1(@types/react@19.2.14)(react@18.3.1) + '@radix-ui/react-popper': 1.2.7(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-portal': 1.1.9(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-presence': 1.1.4(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-slot': 1.2.3(@types/react@19.2.14)(react@18.3.1) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.14)(react@18.3.1) + '@radix-ui/react-visually-hidden': 1.2.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) react: 18.3.1 react-dom: 18.3.1(react@18.3.1) optionalDependencies: - '@types/react': 19.1.5 - '@types/react-dom': 19.1.5(@types/react@19.1.5) + '@types/react': 19.2.14 + '@types/react-dom': 19.2.3(@types/react@19.2.14) - '@radix-ui/react-use-callback-ref@1.1.1(@types/react@19.1.5)(react@18.3.1)': + '@radix-ui/react-use-callback-ref@1.1.1(@types/react@19.2.14)(react@18.3.1)': dependencies: react: 18.3.1 optionalDependencies: - '@types/react': 19.1.5 + '@types/react': 19.2.14 - '@radix-ui/react-use-controllable-state@1.2.2(@types/react@19.1.5)(react@18.3.1)': + '@radix-ui/react-use-controllable-state@1.2.2(@types/react@19.2.14)(react@18.3.1)': dependencies: - '@radix-ui/react-use-effect-event': 0.0.2(@types/react@19.1.5)(react@18.3.1) - '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.1.5)(react@18.3.1) + '@radix-ui/react-use-effect-event': 0.0.2(@types/react@19.2.14)(react@18.3.1) + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.14)(react@18.3.1) react: 18.3.1 optionalDependencies: - '@types/react': 19.1.5 + '@types/react': 19.2.14 - '@radix-ui/react-use-effect-event@0.0.2(@types/react@19.1.5)(react@18.3.1)': + '@radix-ui/react-use-effect-event@0.0.2(@types/react@19.2.14)(react@18.3.1)': dependencies: - '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.1.5)(react@18.3.1) + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.14)(react@18.3.1) react: 18.3.1 optionalDependencies: - '@types/react': 19.1.5 + '@types/react': 19.2.14 - '@radix-ui/react-use-escape-keydown@1.1.1(@types/react@19.1.5)(react@18.3.1)': + '@radix-ui/react-use-escape-keydown@1.1.1(@types/react@19.2.14)(react@18.3.1)': dependencies: - '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.1.5)(react@18.3.1) + '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.2.14)(react@18.3.1) react: 18.3.1 optionalDependencies: - '@types/react': 19.1.5 + '@types/react': 19.2.14 - '@radix-ui/react-use-layout-effect@1.1.1(@types/react@19.1.5)(react@18.3.1)': + '@radix-ui/react-use-layout-effect@1.1.1(@types/react@19.2.14)(react@18.3.1)': dependencies: react: 18.3.1 optionalDependencies: - '@types/react': 19.1.5 + '@types/react': 19.2.14 - '@radix-ui/react-use-previous@1.1.1(@types/react@19.1.5)(react@18.3.1)': + '@radix-ui/react-use-previous@1.1.1(@types/react@19.2.14)(react@18.3.1)': dependencies: react: 18.3.1 optionalDependencies: - '@types/react': 19.1.5 + '@types/react': 19.2.14 - '@radix-ui/react-use-rect@1.1.1(@types/react@19.1.5)(react@18.3.1)': + '@radix-ui/react-use-rect@1.1.1(@types/react@19.2.14)(react@18.3.1)': dependencies: '@radix-ui/rect': 1.1.1 react: 18.3.1 optionalDependencies: - '@types/react': 19.1.5 + '@types/react': 19.2.14 - '@radix-ui/react-use-size@1.1.1(@types/react@19.1.5)(react@18.3.1)': + '@radix-ui/react-use-size@1.1.1(@types/react@19.2.14)(react@18.3.1)': dependencies: - '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.1.5)(react@18.3.1) + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.14)(react@18.3.1) react: 18.3.1 optionalDependencies: - '@types/react': 19.1.5 + '@types/react': 19.2.14 - '@radix-ui/react-visually-hidden@1.2.3(@types/react-dom@19.1.5(@types/react@19.1.5))(@types/react@19.1.5)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + '@radix-ui/react-visually-hidden@1.2.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: - '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.1.5(@types/react@19.1.5))(@types/react@19.1.5)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) react: 18.3.1 react-dom: 18.3.1(react@18.3.1) optionalDependencies: - '@types/react': 19.1.5 - '@types/react-dom': 19.1.5(@types/react@19.1.5) + '@types/react': 19.2.14 + '@types/react-dom': 19.2.3(@types/react@19.2.14) '@radix-ui/rect@1.1.1': {} @@ -15173,6 +15398,15 @@ snapshots: transitivePeerDependencies: - '@module-federation/runtime-tools' + '@rsbuild/core@2.0.3(core-js@3.47.0)': + dependencies: + '@rspack/core': 2.0.1(@swc/helpers@0.5.21) + '@swc/helpers': 0.5.21 + optionalDependencies: + core-js: 3.47.0 + transitivePeerDependencies: + - '@module-federation/runtime-tools' + '@rsbuild/plugin-check-syntax@1.3.0(@rsbuild/core@1.6.15)': dependencies: acorn: 8.15.0 @@ -15189,9 +15423,9 @@ snapshots: deepmerge: 4.3.1 reduce-configs: 1.1.1 - '@rsbuild/plugin-less@1.5.0(@rsbuild/core@2.0.0-beta.11(core-js@3.47.0))': + '@rsbuild/plugin-less@1.5.0(@rsbuild/core@2.0.3(core-js@3.47.0))': dependencies: - '@rsbuild/core': 2.0.0-beta.11(core-js@3.47.0) + '@rsbuild/core': 2.0.3(core-js@3.47.0) deepmerge: 4.3.1 reduce-configs: 1.1.1 @@ -15223,7 +15457,7 @@ snapshots: optionalDependencies: '@rsbuild/core': 1.6.15 - '@rsbuild/plugin-node-polyfill@1.4.2(@rsbuild/core@2.0.0-beta.11(core-js@3.47.0))': + '@rsbuild/plugin-node-polyfill@1.4.2(@rsbuild/core@2.0.3(core-js@3.47.0))': dependencies: assert: 2.1.0 browserify-zlib: 0.2.0 @@ -15249,7 +15483,7 @@ snapshots: util: 0.12.5 vm-browserify: 1.1.2 optionalDependencies: - '@rsbuild/core': 2.0.0-beta.11(core-js@3.47.0) + '@rsbuild/core': 2.0.3(core-js@3.47.0) '@rsbuild/plugin-react@1.4.1(@rsbuild/core@1.6.15)': dependencies: @@ -15259,9 +15493,9 @@ snapshots: transitivePeerDependencies: - webpack-hot-middleware - '@rsbuild/plugin-react@1.4.1(@rsbuild/core@2.0.0-beta.11(core-js@3.47.0))': + '@rsbuild/plugin-react@1.4.1(@rsbuild/core@2.0.3(core-js@3.47.0))': dependencies: - '@rsbuild/core': 2.0.0-beta.11(core-js@3.47.0) + '@rsbuild/core': 2.0.3(core-js@3.47.0) '@rspack/plugin-react-refresh': 1.5.1(react-refresh@0.17.0) react-refresh: 0.17.0 transitivePeerDependencies: @@ -15275,9 +15509,9 @@ snapshots: transitivePeerDependencies: - webpack-hot-middleware - '@rsbuild/plugin-react@1.4.5(@rsbuild/core@2.0.0-beta.11(core-js@3.47.0))': + '@rsbuild/plugin-react@1.4.5(@rsbuild/core@2.0.3(core-js@3.47.0))': dependencies: - '@rsbuild/core': 2.0.0-beta.11(core-js@3.47.0) + '@rsbuild/core': 2.0.3(core-js@3.47.0) '@rspack/plugin-react-refresh': 1.6.0(react-refresh@0.18.0) react-refresh: 0.18.0 transitivePeerDependencies: @@ -15315,10 +15549,10 @@ snapshots: - typescript - webpack-hot-middleware - '@rsbuild/plugin-svgr@1.2.2(@rsbuild/core@2.0.0-beta.11(core-js@3.47.0))(typescript@5.8.3)': + '@rsbuild/plugin-svgr@1.2.2(@rsbuild/core@2.0.3(core-js@3.47.0))(typescript@5.8.3)': dependencies: - '@rsbuild/core': 2.0.0-beta.11(core-js@3.47.0) - '@rsbuild/plugin-react': 1.4.1(@rsbuild/core@2.0.0-beta.11(core-js@3.47.0)) + '@rsbuild/core': 2.0.3(core-js@3.47.0) + '@rsbuild/plugin-react': 1.4.1(@rsbuild/core@2.0.3(core-js@3.47.0)) '@svgr/core': 8.1.0(typescript@5.8.3) '@svgr/plugin-jsx': 8.1.0(@svgr/core@8.1.0(typescript@5.8.3)) '@svgr/plugin-svgo': 8.1.0(@svgr/core@8.1.0(typescript@5.8.3))(typescript@5.8.3) @@ -15341,12 +15575,12 @@ snapshots: - '@rspack/core' - typescript - '@rsbuild/plugin-type-check@1.3.2(@rsbuild/core@1.6.15)(@rspack/core@2.0.0-beta.9(@swc/helpers@0.5.21))(typescript@5.8.3)': + '@rsbuild/plugin-type-check@1.3.2(@rsbuild/core@1.6.15)(@rspack/core@2.0.1(@swc/helpers@0.5.21))(typescript@5.8.3)': dependencies: deepmerge: 4.3.1 json5: 2.2.3 reduce-configs: 1.1.1 - ts-checker-rspack-plugin: 1.2.2(@rspack/core@2.0.0-beta.9(@swc/helpers@0.5.21))(typescript@5.8.3) + ts-checker-rspack-plugin: 1.2.2(@rspack/core@2.0.1(@swc/helpers@0.5.21))(typescript@5.8.3) optionalDependencies: '@rsbuild/core': 1.6.15 transitivePeerDependencies: @@ -15355,13 +15589,13 @@ snapshots: '@rsdoctor/client@1.0.2': {} - '@rsdoctor/core@1.0.2(@rsbuild/core@1.6.15)(@rspack/core@2.0.0-beta.9(@swc/helpers@0.5.21))(bufferutil@4.0.9)(webpack@5.99.5)': + '@rsdoctor/core@1.0.2(@rsbuild/core@1.6.15)(@rspack/core@2.0.1(@swc/helpers@0.5.21))(bufferutil@4.0.9)(webpack@5.99.5)': dependencies: '@rsbuild/plugin-check-syntax': 1.3.0(@rsbuild/core@1.6.15) - '@rsdoctor/graph': 1.0.2(@rspack/core@2.0.0-beta.9(@swc/helpers@0.5.21))(bufferutil@4.0.9)(webpack@5.99.5) - '@rsdoctor/sdk': 1.0.2(@rspack/core@2.0.0-beta.9(@swc/helpers@0.5.21))(bufferutil@4.0.9)(webpack@5.99.5) - '@rsdoctor/types': 1.0.2(@rspack/core@2.0.0-beta.9(@swc/helpers@0.5.21))(webpack@5.99.5) - '@rsdoctor/utils': 1.0.2(@rspack/core@2.0.0-beta.9(@swc/helpers@0.5.21))(webpack@5.99.5) + '@rsdoctor/graph': 1.0.2(@rspack/core@2.0.1(@swc/helpers@0.5.21))(bufferutil@4.0.9)(webpack@5.99.5) + '@rsdoctor/sdk': 1.0.2(@rspack/core@2.0.1(@swc/helpers@0.5.21))(bufferutil@4.0.9)(webpack@5.99.5) + '@rsdoctor/types': 1.0.2(@rspack/core@2.0.1(@swc/helpers@0.5.21))(webpack@5.99.5) + '@rsdoctor/utils': 1.0.2(@rspack/core@2.0.1(@swc/helpers@0.5.21))(webpack@5.99.5) axios: 1.9.0 browserslist-load-config: 1.0.0 enhanced-resolve: 5.12.0 @@ -15381,10 +15615,10 @@ snapshots: - utf-8-validate - webpack - '@rsdoctor/graph@1.0.2(@rspack/core@2.0.0-beta.9(@swc/helpers@0.5.21))(bufferutil@4.0.9)(webpack@5.99.5)': + '@rsdoctor/graph@1.0.2(@rspack/core@2.0.1(@swc/helpers@0.5.21))(bufferutil@4.0.9)(webpack@5.99.5)': dependencies: - '@rsdoctor/types': 1.0.2(@rspack/core@2.0.0-beta.9(@swc/helpers@0.5.21))(webpack@5.99.5) - '@rsdoctor/utils': 1.0.2(@rspack/core@2.0.0-beta.9(@swc/helpers@0.5.21))(webpack@5.99.5) + '@rsdoctor/types': 1.0.2(@rspack/core@2.0.1(@swc/helpers@0.5.21))(webpack@5.99.5) + '@rsdoctor/utils': 1.0.2(@rspack/core@2.0.1(@swc/helpers@0.5.21))(webpack@5.99.5) lodash.unionby: 4.8.0 socket.io: 4.8.1(bufferutil@4.0.9)(utf-8-validate@6.0.5) source-map: 0.7.6 @@ -15395,14 +15629,14 @@ snapshots: - utf-8-validate - webpack - '@rsdoctor/rspack-plugin@1.0.2(@rsbuild/core@1.6.15)(@rspack/core@2.0.0-beta.9(@swc/helpers@0.5.21))(bufferutil@4.0.9)(webpack@5.99.5)': + '@rsdoctor/rspack-plugin@1.0.2(@rsbuild/core@1.6.15)(@rspack/core@2.0.1(@swc/helpers@0.5.21))(bufferutil@4.0.9)(webpack@5.99.5)': dependencies: - '@rsdoctor/core': 1.0.2(@rsbuild/core@1.6.15)(@rspack/core@2.0.0-beta.9(@swc/helpers@0.5.21))(bufferutil@4.0.9)(webpack@5.99.5) - '@rsdoctor/graph': 1.0.2(@rspack/core@2.0.0-beta.9(@swc/helpers@0.5.21))(bufferutil@4.0.9)(webpack@5.99.5) - '@rsdoctor/sdk': 1.0.2(@rspack/core@2.0.0-beta.9(@swc/helpers@0.5.21))(bufferutil@4.0.9)(webpack@5.99.5) - '@rsdoctor/types': 1.0.2(@rspack/core@2.0.0-beta.9(@swc/helpers@0.5.21))(webpack@5.99.5) - '@rsdoctor/utils': 1.0.2(@rspack/core@2.0.0-beta.9(@swc/helpers@0.5.21))(webpack@5.99.5) - '@rspack/core': 2.0.0-beta.9(@swc/helpers@0.5.21) + '@rsdoctor/core': 1.0.2(@rsbuild/core@1.6.15)(@rspack/core@2.0.1(@swc/helpers@0.5.21))(bufferutil@4.0.9)(webpack@5.99.5) + '@rsdoctor/graph': 1.0.2(@rspack/core@2.0.1(@swc/helpers@0.5.21))(bufferutil@4.0.9)(webpack@5.99.5) + '@rsdoctor/sdk': 1.0.2(@rspack/core@2.0.1(@swc/helpers@0.5.21))(bufferutil@4.0.9)(webpack@5.99.5) + '@rsdoctor/types': 1.0.2(@rspack/core@2.0.1(@swc/helpers@0.5.21))(webpack@5.99.5) + '@rsdoctor/utils': 1.0.2(@rspack/core@2.0.1(@swc/helpers@0.5.21))(webpack@5.99.5) + '@rspack/core': 2.0.1(@swc/helpers@0.5.21) lodash: 4.17.21 transitivePeerDependencies: - '@rsbuild/core' @@ -15412,12 +15646,12 @@ snapshots: - utf-8-validate - webpack - '@rsdoctor/sdk@1.0.2(@rspack/core@2.0.0-beta.9(@swc/helpers@0.5.21))(bufferutil@4.0.9)(webpack@5.99.5)': + '@rsdoctor/sdk@1.0.2(@rspack/core@2.0.1(@swc/helpers@0.5.21))(bufferutil@4.0.9)(webpack@5.99.5)': dependencies: '@rsdoctor/client': 1.0.2 - '@rsdoctor/graph': 1.0.2(@rspack/core@2.0.0-beta.9(@swc/helpers@0.5.21))(bufferutil@4.0.9)(webpack@5.99.5) - '@rsdoctor/types': 1.0.2(@rspack/core@2.0.0-beta.9(@swc/helpers@0.5.21))(webpack@5.99.5) - '@rsdoctor/utils': 1.0.2(@rspack/core@2.0.0-beta.9(@swc/helpers@0.5.21))(webpack@5.99.5) + '@rsdoctor/graph': 1.0.2(@rspack/core@2.0.1(@swc/helpers@0.5.21))(bufferutil@4.0.9)(webpack@5.99.5) + '@rsdoctor/types': 1.0.2(@rspack/core@2.0.1(@swc/helpers@0.5.21))(webpack@5.99.5) + '@rsdoctor/utils': 1.0.2(@rspack/core@2.0.1(@swc/helpers@0.5.21))(webpack@5.99.5) '@types/fs-extra': 11.0.4 body-parser: 1.20.3 cors: 2.8.5 @@ -15437,7 +15671,7 @@ snapshots: - utf-8-validate - webpack - '@rsdoctor/types@1.0.2(@rspack/core@2.0.0-beta.9(@swc/helpers@0.5.21))(webpack@5.99.5)': + '@rsdoctor/types@1.0.2(@rspack/core@2.0.1(@swc/helpers@0.5.21))(webpack@5.99.5)': dependencies: '@types/connect': 3.4.38 '@types/estree': 1.0.5 @@ -15445,12 +15679,12 @@ snapshots: source-map: 0.7.6 webpack: 5.99.5 optionalDependencies: - '@rspack/core': 2.0.0-beta.9(@swc/helpers@0.5.21) + '@rspack/core': 2.0.1(@swc/helpers@0.5.21) - '@rsdoctor/utils@1.0.2(@rspack/core@2.0.0-beta.9(@swc/helpers@0.5.21))(webpack@5.99.5)': + '@rsdoctor/utils@1.0.2(@rspack/core@2.0.1(@swc/helpers@0.5.21))(webpack@5.99.5)': dependencies: '@babel/code-frame': 7.26.2 - '@rsdoctor/types': 1.0.2(@rspack/core@2.0.0-beta.9(@swc/helpers@0.5.21))(webpack@5.99.5) + '@rsdoctor/types': 1.0.2(@rspack/core@2.0.1(@swc/helpers@0.5.21))(webpack@5.99.5) '@types/estree': 1.0.5 acorn: 8.15.0 acorn-import-attributes: 1.9.5(acorn@8.15.0) @@ -15480,6 +15714,16 @@ snapshots: transitivePeerDependencies: - '@typescript/native-preview' + '@rslib/core@0.18.3(@microsoft/api-extractor@7.52.10(@types/node@18.19.130))(typescript@5.8.3)': + dependencies: + '@rsbuild/core': 1.6.15 + rsbuild-plugin-dts: 0.18.3(@microsoft/api-extractor@7.52.10(@types/node@18.19.130))(@rsbuild/core@1.6.15)(typescript@5.8.3) + optionalDependencies: + '@microsoft/api-extractor': 7.52.10(@types/node@18.19.130) + typescript: 5.8.3 + transitivePeerDependencies: + - '@typescript/native-preview' + '@rslib/core@0.18.3(@microsoft/api-extractor@7.52.10(@types/node@18.19.62))(typescript@5.8.3)': dependencies: '@rsbuild/core': 1.6.15 @@ -15490,12 +15734,12 @@ snapshots: transitivePeerDependencies: - '@typescript/native-preview' - '@rslib/core@0.18.3(@microsoft/api-extractor@7.52.10(@types/node@25.5.2))(typescript@5.8.3)': + '@rslib/core@0.18.3(@microsoft/api-extractor@7.52.10(@types/node@25.6.0))(typescript@5.8.3)': dependencies: '@rsbuild/core': 1.6.15 - rsbuild-plugin-dts: 0.18.3(@microsoft/api-extractor@7.52.10(@types/node@25.5.2))(@rsbuild/core@1.6.15)(typescript@5.8.3) + rsbuild-plugin-dts: 0.18.3(@microsoft/api-extractor@7.52.10(@types/node@25.6.0))(@rsbuild/core@1.6.15)(typescript@5.8.3) optionalDependencies: - '@microsoft/api-extractor': 7.52.10(@types/node@25.5.2) + '@microsoft/api-extractor': 7.52.10(@types/node@25.6.0) typescript: 5.8.3 transitivePeerDependencies: - '@typescript/native-preview' @@ -15509,6 +15753,9 @@ snapshots: '@rspack/binding-darwin-arm64@2.0.0-beta.9': optional: true + '@rspack/binding-darwin-arm64@2.0.1': + optional: true + '@rspack/binding-darwin-x64@1.6.6': optional: true @@ -15518,6 +15765,9 @@ snapshots: '@rspack/binding-darwin-x64@2.0.0-beta.9': optional: true + '@rspack/binding-darwin-x64@2.0.1': + optional: true + '@rspack/binding-linux-arm64-gnu@1.6.6': optional: true @@ -15527,6 +15777,9 @@ snapshots: '@rspack/binding-linux-arm64-gnu@2.0.0-beta.9': optional: true + '@rspack/binding-linux-arm64-gnu@2.0.1': + optional: true + '@rspack/binding-linux-arm64-musl@1.6.6': optional: true @@ -15536,6 +15789,9 @@ snapshots: '@rspack/binding-linux-arm64-musl@2.0.0-beta.9': optional: true + '@rspack/binding-linux-arm64-musl@2.0.1': + optional: true + '@rspack/binding-linux-x64-gnu@1.6.6': optional: true @@ -15545,6 +15801,9 @@ snapshots: '@rspack/binding-linux-x64-gnu@2.0.0-beta.9': optional: true + '@rspack/binding-linux-x64-gnu@2.0.1': + optional: true + '@rspack/binding-linux-x64-musl@1.6.6': optional: true @@ -15554,6 +15813,9 @@ snapshots: '@rspack/binding-linux-x64-musl@2.0.0-beta.9': optional: true + '@rspack/binding-linux-x64-musl@2.0.1': + optional: true + '@rspack/binding-wasm32-wasi@1.6.6': dependencies: '@napi-rs/wasm-runtime': 1.0.7 @@ -15569,6 +15831,13 @@ snapshots: '@napi-rs/wasm-runtime': 1.1.1 optional: true + '@rspack/binding-wasm32-wasi@2.0.1': + dependencies: + '@emnapi/core': 1.10.0 + '@emnapi/runtime': 1.10.0 + '@napi-rs/wasm-runtime': 1.1.4(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0) + optional: true + '@rspack/binding-win32-arm64-msvc@1.6.6': optional: true @@ -15578,6 +15847,9 @@ snapshots: '@rspack/binding-win32-arm64-msvc@2.0.0-beta.9': optional: true + '@rspack/binding-win32-arm64-msvc@2.0.1': + optional: true + '@rspack/binding-win32-ia32-msvc@1.6.6': optional: true @@ -15587,6 +15859,9 @@ snapshots: '@rspack/binding-win32-ia32-msvc@2.0.0-beta.9': optional: true + '@rspack/binding-win32-ia32-msvc@2.0.1': + optional: true + '@rspack/binding-win32-x64-msvc@1.6.6': optional: true @@ -15596,6 +15871,9 @@ snapshots: '@rspack/binding-win32-x64-msvc@2.0.0-beta.9': optional: true + '@rspack/binding-win32-x64-msvc@2.0.1': + optional: true + '@rspack/binding@1.6.6': optionalDependencies: '@rspack/binding-darwin-arm64': 1.6.6 @@ -15635,6 +15913,19 @@ snapshots: '@rspack/binding-win32-ia32-msvc': 2.0.0-beta.9 '@rspack/binding-win32-x64-msvc': 2.0.0-beta.9 + '@rspack/binding@2.0.1': + optionalDependencies: + '@rspack/binding-darwin-arm64': 2.0.1 + '@rspack/binding-darwin-x64': 2.0.1 + '@rspack/binding-linux-arm64-gnu': 2.0.1 + '@rspack/binding-linux-arm64-musl': 2.0.1 + '@rspack/binding-linux-x64-gnu': 2.0.1 + '@rspack/binding-linux-x64-musl': 2.0.1 + '@rspack/binding-wasm32-wasi': 2.0.1 + '@rspack/binding-win32-arm64-msvc': 2.0.1 + '@rspack/binding-win32-ia32-msvc': 2.0.1 + '@rspack/binding-win32-x64-msvc': 2.0.1 + '@rspack/core@1.6.6(@swc/helpers@0.5.21)': dependencies: '@module-federation/runtime-tools': 0.21.6 @@ -15657,6 +15948,12 @@ snapshots: optionalDependencies: '@swc/helpers': 0.5.21 + '@rspack/core@2.0.1(@swc/helpers@0.5.21)': + dependencies: + '@rspack/binding': 2.0.1 + optionalDependencies: + '@swc/helpers': 0.5.21 + '@rspack/lite-tapable@1.1.0': {} '@rspack/plugin-react-refresh@1.5.1(react-refresh@0.17.0)': @@ -15682,10 +15979,10 @@ snapshots: error-stack-parser: 2.1.4 react-refresh: 0.18.0 - '@rspress/core@2.0.8(@types/mdast@4.0.4)(@types/react@19.1.5)(core-js@3.47.0)(micromark-util-types@2.0.2)(micromark@4.0.2)': + '@rspress/core@2.0.8(@types/mdast@4.0.4)(@types/react@19.2.14)(core-js@3.47.0)(micromark-util-types@2.0.2)(micromark@4.0.2)': dependencies: '@mdx-js/mdx': 3.1.1 - '@mdx-js/react': 3.1.1(@types/react@19.1.5)(react@19.2.4) + '@mdx-js/react': 3.1.1(@types/react@19.2.14)(react@19.2.4) '@rsbuild/core': 2.0.0-beta.11(core-js@3.47.0) '@rsbuild/plugin-react': 1.4.6(@rsbuild/core@2.0.0-beta.11(core-js@3.47.0)) '@rspress/shared': 2.0.8(core-js@3.47.0) @@ -15739,13 +16036,13 @@ snapshots: - supports-color - webpack-hot-middleware - '@rspress/plugin-client-redirects@2.0.8(@rspress/core@2.0.8(@types/mdast@4.0.4)(@types/react@19.1.5)(core-js@3.47.0)(micromark-util-types@2.0.2)(micromark@4.0.2))': + '@rspress/plugin-client-redirects@2.0.8(@rspress/core@2.0.8(@types/mdast@4.0.4)(@types/react@19.2.14)(core-js@3.47.0)(micromark-util-types@2.0.2)(micromark@4.0.2))': dependencies: - '@rspress/core': 2.0.8(@types/mdast@4.0.4)(@types/react@19.1.5)(core-js@3.47.0)(micromark-util-types@2.0.2)(micromark@4.0.2) + '@rspress/core': 2.0.8(@types/mdast@4.0.4)(@types/react@19.2.14)(core-js@3.47.0)(micromark-util-types@2.0.2)(micromark@4.0.2) - '@rspress/plugin-llms@2.0.8(@rspress/core@2.0.8(@types/mdast@4.0.4)(@types/react@19.1.5)(core-js@3.47.0)(micromark-util-types@2.0.2)(micromark@4.0.2))': + '@rspress/plugin-llms@2.0.8(@rspress/core@2.0.8(@types/mdast@4.0.4)(@types/react@19.2.14)(core-js@3.47.0)(micromark-util-types@2.0.2)(micromark@4.0.2))': dependencies: - '@rspress/core': 2.0.8(@types/mdast@4.0.4)(@types/react@19.1.5)(core-js@3.47.0)(micromark-util-types@2.0.2)(micromark@4.0.2) + '@rspress/core': 2.0.8(@types/mdast@4.0.4)(@types/react@19.2.14)(core-js@3.47.0)(micromark-util-types@2.0.2)(micromark@4.0.2) remark-mdx: 3.1.1 remark-parse: 11.0.0 remark-stringify: 11.0.0 @@ -15754,9 +16051,9 @@ snapshots: transitivePeerDependencies: - supports-color - '@rspress/plugin-sitemap@2.0.8(@rspress/core@2.0.8(@types/mdast@4.0.4)(@types/react@19.1.5)(core-js@3.47.0)(micromark-util-types@2.0.2)(micromark@4.0.2))': + '@rspress/plugin-sitemap@2.0.8(@rspress/core@2.0.8(@types/mdast@4.0.4)(@types/react@19.2.14)(core-js@3.47.0)(micromark-util-types@2.0.2)(micromark@4.0.2))': dependencies: - '@rspress/core': 2.0.8(@types/mdast@4.0.4)(@types/react@19.1.5)(core-js@3.47.0)(micromark-util-types@2.0.2)(micromark@4.0.2) + '@rspress/core': 2.0.8(@types/mdast@4.0.4)(@types/react@19.2.14)(core-js@3.47.0)(micromark-util-types@2.0.2)(micromark@4.0.2) '@rspress/shared@2.0.8(core-js@3.47.0)': dependencies: @@ -15769,6 +16066,17 @@ snapshots: - '@module-federation/runtime-tools' - core-js + '@rstest/core@0.9.9(core-js@3.47.0)(jsdom@29.0.2)': + dependencies: + '@rsbuild/core': 2.0.3(core-js@3.47.0) + '@types/chai': 5.2.3 + tinypool: 2.1.0 + optionalDependencies: + jsdom: 29.0.2 + transitivePeerDependencies: + - '@module-federation/runtime-tools' + - core-js + '@rushstack/node-core-library@5.14.0(@types/node@18.19.118)': dependencies: ajv: 8.13.0 @@ -15777,12 +16085,26 @@ snapshots: fs-extra: 11.3.4 import-lazy: 4.0.0 jju: 1.4.0 - resolve: 1.22.11 + resolve: 1.22.12 semver: 7.5.4 optionalDependencies: '@types/node': 18.19.118 optional: true + '@rushstack/node-core-library@5.14.0(@types/node@18.19.130)': + dependencies: + ajv: 8.13.0 + ajv-draft-04: 1.0.0(ajv@8.13.0) + ajv-formats: 3.0.1(ajv@8.13.0) + fs-extra: 11.3.4 + import-lazy: 4.0.0 + jju: 1.4.0 + resolve: 1.22.12 + semver: 7.5.4 + optionalDependencies: + '@types/node': 18.19.130 + optional: true + '@rushstack/node-core-library@5.14.0(@types/node@18.19.62)': dependencies: ajv: 8.13.0 @@ -15791,13 +16113,13 @@ snapshots: fs-extra: 11.3.4 import-lazy: 4.0.0 jju: 1.4.0 - resolve: 1.22.11 + resolve: 1.22.12 semver: 7.5.4 optionalDependencies: '@types/node': 18.19.62 optional: true - '@rushstack/node-core-library@5.14.0(@types/node@25.5.2)': + '@rushstack/node-core-library@5.14.0(@types/node@25.6.0)': dependencies: ajv: 8.13.0 ajv-draft-04: 1.0.0(ajv@8.13.0) @@ -15805,15 +16127,15 @@ snapshots: fs-extra: 11.3.4 import-lazy: 4.0.0 jju: 1.4.0 - resolve: 1.22.11 + resolve: 1.22.12 semver: 7.5.4 optionalDependencies: - '@types/node': 25.5.2 + '@types/node': 25.6.0 optional: true '@rushstack/rig-package@0.5.3': dependencies: - resolve: 1.22.11 + resolve: 1.22.12 strip-json-comments: 3.1.1 optional: true @@ -15825,6 +16147,14 @@ snapshots: '@types/node': 18.19.118 optional: true + '@rushstack/terminal@0.15.4(@types/node@18.19.130)': + dependencies: + '@rushstack/node-core-library': 5.14.0(@types/node@18.19.130) + supports-color: 8.1.1 + optionalDependencies: + '@types/node': 18.19.130 + optional: true + '@rushstack/terminal@0.15.4(@types/node@18.19.62)': dependencies: '@rushstack/node-core-library': 5.14.0(@types/node@18.19.62) @@ -15833,12 +16163,12 @@ snapshots: '@types/node': 18.19.62 optional: true - '@rushstack/terminal@0.15.4(@types/node@25.5.2)': + '@rushstack/terminal@0.15.4(@types/node@25.6.0)': dependencies: - '@rushstack/node-core-library': 5.14.0(@types/node@25.5.2) + '@rushstack/node-core-library': 5.14.0(@types/node@25.6.0) supports-color: 8.1.1 optionalDependencies: - '@types/node': 25.5.2 + '@types/node': 25.6.0 optional: true '@rushstack/ts-command-line@5.0.2(@types/node@18.19.118)': @@ -15851,6 +16181,16 @@ snapshots: - '@types/node' optional: true + '@rushstack/ts-command-line@5.0.2(@types/node@18.19.130)': + dependencies: + '@rushstack/terminal': 0.15.4(@types/node@18.19.130) + '@types/argparse': 1.0.38 + argparse: 1.0.10 + string-argv: 0.3.2 + transitivePeerDependencies: + - '@types/node' + optional: true + '@rushstack/ts-command-line@5.0.2(@types/node@18.19.62)': dependencies: '@rushstack/terminal': 0.15.4(@types/node@18.19.62) @@ -15861,9 +16201,9 @@ snapshots: - '@types/node' optional: true - '@rushstack/ts-command-line@5.0.2(@types/node@25.5.2)': + '@rushstack/ts-command-line@5.0.2(@types/node@25.6.0)': dependencies: - '@rushstack/terminal': 0.15.4(@types/node@25.5.2) + '@rushstack/terminal': 0.15.4(@types/node@25.6.0) '@types/argparse': 1.0.38 argparse: 1.0.10 string-argv: 0.3.2 @@ -16135,6 +16475,11 @@ snapshots: '@types/node': 18.19.130 '@types/responselike': 1.0.3 + '@types/chai@5.2.3': + dependencies: + '@types/deep-eql': 4.0.2 + assertion-error: 2.0.1 + '@types/chrome@0.0.279': dependencies: '@types/filesystem': 0.0.36 @@ -16158,6 +16503,8 @@ snapshots: dependencies: '@types/ms': 0.7.34 + '@types/deep-eql@4.0.2': {} + '@types/eslint-scope@3.7.7': dependencies: '@types/eslint': 9.6.1 @@ -16280,9 +16627,9 @@ snapshots: dependencies: undici-types: 7.16.0 - '@types/node@25.5.2': + '@types/node@25.6.0': dependencies: - undici-types: 7.18.2 + undici-types: 7.19.2 '@types/normalize-package-data@2.4.4': {} @@ -16298,9 +16645,9 @@ snapshots: dependencies: '@types/react': 18.3.23 - '@types/react-dom@19.1.5(@types/react@19.1.5)': + '@types/react-dom@19.2.3(@types/react@19.2.14)': dependencies: - '@types/react': 19.1.5 + '@types/react': 19.2.14 optional: true '@types/react@18.3.23': @@ -16308,7 +16655,7 @@ snapshots: '@types/prop-types': 15.7.13 csstype: 3.2.3 - '@types/react@19.1.5': + '@types/react@19.2.14': dependencies: csstype: 3.2.3 @@ -16384,45 +16731,45 @@ snapshots: chai: 5.2.0 tinyrainbow: 2.0.0 - '@vitest/mocker@3.0.5(vite@5.4.10(@types/node@18.19.118)(less@4.3.0)(lightningcss@1.30.1)(sass-embedded@1.86.3)(terser@5.46.1))': + '@vitest/mocker@3.0.5(vite@5.4.10(@types/node@18.19.118)(less@4.3.0)(lightningcss@1.30.1)(sass-embedded@1.86.3)(terser@5.46.2))': dependencies: '@vitest/spy': 3.0.5 estree-walker: 3.0.3 magic-string: 0.30.17 optionalDependencies: - vite: 5.4.10(@types/node@18.19.118)(less@4.3.0)(lightningcss@1.30.1)(sass-embedded@1.86.3)(terser@5.46.1) + vite: 5.4.10(@types/node@18.19.118)(less@4.3.0)(lightningcss@1.30.1)(sass-embedded@1.86.3)(terser@5.46.2) - '@vitest/mocker@3.0.5(vite@5.4.10(@types/node@18.19.130)(less@4.3.0)(lightningcss@1.30.1)(sass-embedded@1.86.3)(terser@5.46.1))': + '@vitest/mocker@3.0.5(vite@5.4.10(@types/node@18.19.130)(less@4.3.0)(lightningcss@1.30.1)(sass-embedded@1.86.3)(terser@5.46.2))': dependencies: '@vitest/spy': 3.0.5 estree-walker: 3.0.3 magic-string: 0.30.17 optionalDependencies: - vite: 5.4.10(@types/node@18.19.130)(less@4.3.0)(lightningcss@1.30.1)(sass-embedded@1.86.3)(terser@5.46.1) + vite: 5.4.10(@types/node@18.19.130)(less@4.3.0)(lightningcss@1.30.1)(sass-embedded@1.86.3)(terser@5.46.2) - '@vitest/mocker@3.0.5(vite@5.4.10(@types/node@18.19.62)(less@4.3.0)(lightningcss@1.30.1)(sass-embedded@1.86.3)(terser@5.46.1))': + '@vitest/mocker@3.0.5(vite@5.4.10(@types/node@18.19.62)(less@4.3.0)(lightningcss@1.30.1)(sass-embedded@1.86.3)(terser@5.46.2))': dependencies: '@vitest/spy': 3.0.5 estree-walker: 3.0.3 magic-string: 0.30.17 optionalDependencies: - vite: 5.4.10(@types/node@18.19.62)(less@4.3.0)(lightningcss@1.30.1)(sass-embedded@1.86.3)(terser@5.46.1) + vite: 5.4.10(@types/node@18.19.62)(less@4.3.0)(lightningcss@1.30.1)(sass-embedded@1.86.3)(terser@5.46.2) - '@vitest/mocker@3.0.5(vite@5.4.10(@types/node@25.5.2)(less@4.2.2)(lightningcss@1.30.1)(sass-embedded@1.86.3)(terser@5.46.1))': + '@vitest/mocker@3.0.5(vite@5.4.10(@types/node@25.6.0)(less@4.2.2)(lightningcss@1.30.1)(sass-embedded@1.86.3)(terser@5.46.2))': dependencies: '@vitest/spy': 3.0.5 estree-walker: 3.0.3 magic-string: 0.30.17 optionalDependencies: - vite: 5.4.10(@types/node@25.5.2)(less@4.2.2)(lightningcss@1.30.1)(sass-embedded@1.86.3)(terser@5.46.1) + vite: 5.4.10(@types/node@25.6.0)(less@4.2.2)(lightningcss@1.30.1)(sass-embedded@1.86.3)(terser@5.46.2) - '@vitest/mocker@3.0.5(vite@5.4.10(@types/node@25.5.2)(less@4.3.0)(lightningcss@1.30.1)(sass-embedded@1.86.3)(terser@5.46.1))': + '@vitest/mocker@3.0.5(vite@5.4.10(@types/node@25.6.0)(less@4.3.0)(lightningcss@1.30.1)(sass-embedded@1.86.3)(terser@5.46.2))': dependencies: '@vitest/spy': 3.0.5 estree-walker: 3.0.3 magic-string: 0.30.17 optionalDependencies: - vite: 5.4.10(@types/node@25.5.2)(less@4.3.0)(lightningcss@1.30.1)(sass-embedded@1.86.3)(terser@5.46.1) + vite: 5.4.10(@types/node@25.6.0)(less@4.3.0)(lightningcss@1.30.1)(sass-embedded@1.86.3)(terser@5.46.2) '@vitest/pretty-format@3.0.5': dependencies: @@ -16710,18 +17057,18 @@ snapshots: ajv: 8.13.0 optional: true - ajv-formats@2.1.1(ajv@8.18.0): + ajv-formats@2.1.1(ajv@8.20.0): optionalDependencies: - ajv: 8.18.0 + ajv: 8.20.0 ajv-formats@3.0.1(ajv@8.13.0): optionalDependencies: ajv: 8.13.0 optional: true - ajv-keywords@5.1.0(ajv@8.18.0): + ajv-keywords@5.1.0(ajv@8.20.0): dependencies: - ajv: 8.18.0 + ajv: 8.20.0 fast-deep-equal: 3.1.3 ajv@6.12.6: @@ -16754,7 +17101,7 @@ snapshots: json-schema-traverse: 1.0.0 require-from-string: 2.0.2 - ajv@8.18.0: + ajv@8.20.0: dependencies: fast-deep-equal: 3.1.3 fast-uri: 3.1.0 @@ -17444,7 +17791,7 @@ snapshots: centra@2.7.0: dependencies: - follow-redirects: 1.15.11 + follow-redirects: 1.16.0 transitivePeerDependencies: - debug @@ -17619,12 +17966,12 @@ snapshots: clsx@2.1.1: {} - cmdk@1.1.1(@types/react-dom@19.1.5(@types/react@19.1.5))(@types/react@19.1.5)(react-dom@18.3.1(react@18.3.1))(react@18.3.1): + cmdk@1.1.1(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@18.3.1(react@18.3.1))(react@18.3.1): dependencies: - '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.1.5)(react@18.3.1) - '@radix-ui/react-dialog': 1.1.14(@types/react-dom@19.1.5(@types/react@19.1.5))(@types/react@19.1.5)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@radix-ui/react-id': 1.1.1(@types/react@19.1.5)(react@18.3.1) - '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.1.5(@types/react@19.1.5))(@types/react@19.1.5)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.14)(react@18.3.1) + '@radix-ui/react-dialog': 1.1.14(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-id': 1.1.1(@types/react@19.2.14)(react@18.3.1) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) react: 18.3.1 react-dom: 18.3.1(react@18.3.1) transitivePeerDependencies: @@ -17693,10 +18040,10 @@ snapshots: commander@9.5.0: {} - commitizen@4.2.5(@types/node@25.5.2)(typescript@5.8.3): + commitizen@4.2.5(@types/node@25.6.0)(typescript@5.8.3): dependencies: cachedir: 2.3.0 - cz-conventional-changelog: 3.3.0(@types/node@25.5.2)(typescript@5.8.3) + cz-conventional-changelog: 3.3.0(@types/node@25.6.0)(typescript@5.8.3) dedent: 0.7.0 detect-indent: 6.1.0 find-node-modules: 2.1.3 @@ -17857,9 +18204,9 @@ snapshots: corser@2.0.1: {} - cosmiconfig-typescript-loader@6.2.0(@types/node@25.5.2)(cosmiconfig@9.0.0(typescript@5.8.3))(typescript@5.8.3): + cosmiconfig-typescript-loader@6.2.0(@types/node@25.6.0)(cosmiconfig@9.0.0(typescript@5.8.3))(typescript@5.8.3): dependencies: - '@types/node': 25.5.2 + '@types/node': 25.6.0 cosmiconfig: 9.0.0(typescript@5.8.3) jiti: 2.6.1 typescript: 5.8.3 @@ -17877,7 +18224,7 @@ snapshots: dependencies: env-paths: 2.2.1 import-fresh: 3.3.1 - js-yaml: 4.1.1 + js-yaml: 4.1.0 parse-json: 5.2.0 optionalDependencies: typescript: 5.8.3 @@ -17990,8 +18337,6 @@ snapshots: dependencies: css-tree: 2.2.1 - csstype@3.1.3: {} - csstype@3.2.3: {} csv-generate@3.4.3: {} @@ -18007,16 +18352,16 @@ snapshots: csv-stringify: 5.6.5 stream-transform: 2.1.3 - cz-conventional-changelog@3.3.0(@types/node@25.5.2)(typescript@5.8.3): + cz-conventional-changelog@3.3.0(@types/node@25.6.0)(typescript@5.8.3): dependencies: chalk: 2.4.2 - commitizen: 4.2.5(@types/node@25.5.2)(typescript@5.8.3) + commitizen: 4.2.5(@types/node@25.6.0)(typescript@5.8.3) conventional-commit-types: 3.0.0 lodash.map: 4.6.0 longest: 2.0.1 word-wrap: 1.2.5 optionalDependencies: - '@commitlint/load': 20.1.0(@types/node@25.5.2)(typescript@5.8.3) + '@commitlint/load': 20.1.0(@types/node@25.6.0)(typescript@5.8.3) transitivePeerDependencies: - '@types/node' - typescript @@ -18379,6 +18724,11 @@ snapshots: graceful-fs: 4.2.11 tapable: 2.3.2 + enhanced-resolve@5.21.0: + dependencies: + graceful-fs: 4.2.11 + tapable: 2.3.3 + enquirer@2.3.6: dependencies: ansi-colors: 4.1.3 @@ -19082,6 +19432,8 @@ snapshots: follow-redirects@1.15.9: {} + follow-redirects@1.16.0: {} + for-each@0.3.5: dependencies: is-callable: 1.2.7 @@ -20483,7 +20835,7 @@ snapshots: pify: 4.0.1 strip-bom: 3.0.0 - loader-runner@4.3.1: {} + loader-runner@4.3.2: {} loader-utils@3.3.1: {} @@ -21889,11 +22241,11 @@ snapshots: dependencies: find-up: 5.0.0 - playwright-core@1.58.1: {} + playwright-core@1.59.1: {} - playwright@1.58.1: + playwright@1.59.1: dependencies: - playwright-core: 1.58.1 + playwright-core: 1.59.1 optionalDependencies: fsevents: 2.3.2 @@ -21993,14 +22345,14 @@ snapshots: dependencies: '@protobufjs/aspromise': 1.1.2 '@protobufjs/base64': 1.1.2 - '@protobufjs/codegen': 2.0.4 + '@protobufjs/codegen': 2.0.5 '@protobufjs/eventemitter': 1.1.0 '@protobufjs/fetch': 1.1.0 '@protobufjs/float': 1.0.2 - '@protobufjs/inquire': 1.1.0 + '@protobufjs/inquire': 1.1.1 '@protobufjs/path': 1.1.2 '@protobufjs/pool': 1.1.0 - '@protobufjs/utf8': 1.1.0 + '@protobufjs/utf8': 1.1.1 '@types/node': 18.19.130 long: 5.3.2 optional: true @@ -22506,24 +22858,24 @@ snapshots: react-refresh@0.18.0: {} - react-remove-scroll-bar@2.3.8(@types/react@19.1.5)(react@18.3.1): + react-remove-scroll-bar@2.3.8(@types/react@19.2.14)(react@18.3.1): dependencies: react: 18.3.1 - react-style-singleton: 2.2.3(@types/react@19.1.5)(react@18.3.1) + react-style-singleton: 2.2.3(@types/react@19.2.14)(react@18.3.1) tslib: 2.8.1 optionalDependencies: - '@types/react': 19.1.5 + '@types/react': 19.2.14 - react-remove-scroll@2.7.1(@types/react@19.1.5)(react@18.3.1): + react-remove-scroll@2.7.1(@types/react@19.2.14)(react@18.3.1): dependencies: react: 18.3.1 - react-remove-scroll-bar: 2.3.8(@types/react@19.1.5)(react@18.3.1) - react-style-singleton: 2.2.3(@types/react@19.1.5)(react@18.3.1) + react-remove-scroll-bar: 2.3.8(@types/react@19.2.14)(react@18.3.1) + react-style-singleton: 2.2.3(@types/react@19.2.14)(react@18.3.1) tslib: 2.8.1 - use-callback-ref: 1.3.3(@types/react@19.1.5)(react@18.3.1) - use-sidecar: 1.1.3(@types/react@19.1.5)(react@18.3.1) + use-callback-ref: 1.3.3(@types/react@19.2.14)(react@18.3.1) + use-sidecar: 1.1.3(@types/react@19.2.14)(react@18.3.1) optionalDependencies: - '@types/react': 19.1.5 + '@types/react': 19.2.14 react-render-to-markdown@19.0.1(react@19.2.4): dependencies: @@ -22554,13 +22906,13 @@ snapshots: react: 18.3.1 react-dom: 18.3.1(react@18.3.1) - react-style-singleton@2.2.3(@types/react@19.1.5)(react@18.3.1): + react-style-singleton@2.2.3(@types/react@19.2.14)(react@18.3.1): dependencies: get-nonce: 1.0.1 react: 18.3.1 tslib: 2.8.1 optionalDependencies: - '@types/react': 19.1.5 + '@types/react': 19.2.14 react@18.3.1: dependencies: @@ -22851,6 +23203,14 @@ snapshots: path-parse: 1.0.7 supports-preserve-symlinks-flag: 1.0.0 + resolve@1.22.12: + dependencies: + es-errors: 1.3.0 + is-core-module: 2.16.1 + path-parse: 1.0.7 + supports-preserve-symlinks-flag: 1.0.0 + optional: true + responselike@2.0.1: dependencies: lowercase-keys: 2.0.0 @@ -22944,6 +23304,14 @@ snapshots: '@microsoft/api-extractor': 7.52.10(@types/node@18.19.118) typescript: 5.8.3 + rsbuild-plugin-dts@0.18.3(@microsoft/api-extractor@7.52.10(@types/node@18.19.130))(@rsbuild/core@1.6.15)(typescript@5.8.3): + dependencies: + '@ast-grep/napi': 0.37.0 + '@rsbuild/core': 1.6.15 + optionalDependencies: + '@microsoft/api-extractor': 7.52.10(@types/node@18.19.130) + typescript: 5.8.3 + rsbuild-plugin-dts@0.18.3(@microsoft/api-extractor@7.52.10(@types/node@18.19.62))(@rsbuild/core@1.6.15)(typescript@5.8.3): dependencies: '@ast-grep/napi': 0.37.0 @@ -22952,12 +23320,12 @@ snapshots: '@microsoft/api-extractor': 7.52.10(@types/node@18.19.62) typescript: 5.8.3 - rsbuild-plugin-dts@0.18.3(@microsoft/api-extractor@7.52.10(@types/node@25.5.2))(@rsbuild/core@1.6.15)(typescript@5.8.3): + rsbuild-plugin-dts@0.18.3(@microsoft/api-extractor@7.52.10(@types/node@25.6.0))(@rsbuild/core@1.6.15)(typescript@5.8.3): dependencies: '@ast-grep/napi': 0.37.0 '@rsbuild/core': 1.6.15 optionalDependencies: - '@microsoft/api-extractor': 7.52.10(@types/node@25.5.2) + '@microsoft/api-extractor': 7.52.10(@types/node@25.6.0) typescript: 5.8.3 rsbuild-plugin-workspace-dev@0.0.1(@rsbuild/core@1.6.15): @@ -23082,7 +23450,7 @@ snapshots: sass-embedded@1.86.3: dependencies: - '@bufbuild/protobuf': 2.11.0 + '@bufbuild/protobuf': 2.12.0 buffer-builder: 0.2.0 colorjs.io: 0.5.2 immutable: 5.1.5 @@ -23130,9 +23498,9 @@ snapshots: schema-utils@4.3.3: dependencies: '@types/json-schema': 7.0.15 - ajv: 8.18.0 - ajv-formats: 2.1.1(ajv@8.18.0) - ajv-keywords: 5.1.0(ajv@8.18.0) + ajv: 8.20.0 + ajv-formats: 2.1.1(ajv@8.20.0) + ajv-keywords: 5.1.0(ajv@8.20.0) screenshot-desktop@1.15.3: dependencies: @@ -23566,6 +23934,8 @@ snapshots: std-env@3.9.0: {} + std-env@4.1.0: {} + stop-iteration-iterator@1.1.0: dependencies: es-errors: 1.3.0 @@ -23798,6 +24168,8 @@ snapshots: tapable@2.3.2: {} + tapable@2.3.3: {} + tar-fs@3.1.1: dependencies: pump: 3.0.4 @@ -23847,15 +24219,15 @@ snapshots: term-size@2.2.1: {} - terser-webpack-plugin@5.4.0(webpack@5.99.5): + terser-webpack-plugin@5.5.0(webpack@5.99.5): dependencies: '@jridgewell/trace-mapping': 0.3.31 jest-worker: 27.5.1 schema-utils: 4.3.3 - terser: 5.46.1 + terser: 5.46.2 webpack: 5.99.5 - terser@5.46.1: + terser@5.46.2: dependencies: '@jridgewell/source-map': 0.3.11 acorn: 8.16.0 @@ -23912,6 +24284,8 @@ snapshots: tinypool@1.1.1: {} + tinypool@2.1.0: {} + tinyrainbow@2.0.0: {} tinyspy@3.0.2: {} @@ -23990,7 +24364,7 @@ snapshots: optionalDependencies: '@rspack/core': 1.6.8(@swc/helpers@0.5.21) - ts-checker-rspack-plugin@1.2.2(@rspack/core@2.0.0-beta.9(@swc/helpers@0.5.21))(typescript@5.8.3): + ts-checker-rspack-plugin@1.2.2(@rspack/core@2.0.1(@swc/helpers@0.5.21))(typescript@5.8.3): dependencies: '@babel/code-frame': 7.27.1 '@rspack/lite-tapable': 1.1.0 @@ -24001,7 +24375,7 @@ snapshots: picocolors: 1.1.1 typescript: 5.8.3 optionalDependencies: - '@rspack/core': 2.0.0-beta.9(@swc/helpers@0.5.21) + '@rspack/core': 2.0.1(@swc/helpers@0.5.21) ts-node@10.9.2(@types/node@18.19.118)(typescript@5.8.3): dependencies: @@ -24156,7 +24530,7 @@ snapshots: undici-types@7.16.0: {} - undici-types@7.18.2: {} + undici-types@7.19.2: {} undici@6.22.0: {} @@ -24267,20 +24641,20 @@ snapshots: punycode: 1.4.1 qs: 6.14.0 - use-callback-ref@1.3.3(@types/react@19.1.5)(react@18.3.1): + use-callback-ref@1.3.3(@types/react@19.2.14)(react@18.3.1): dependencies: react: 18.3.1 tslib: 2.8.1 optionalDependencies: - '@types/react': 19.1.5 + '@types/react': 19.2.14 - use-sidecar@1.1.3(@types/react@19.1.5)(react@18.3.1): + use-sidecar@1.1.3(@types/react@19.2.14)(react@18.3.1): dependencies: detect-node-es: 1.1.0 react: 18.3.1 tslib: 2.8.1 optionalDependencies: - '@types/react': 19.1.5 + '@types/react': 19.2.14 use-sync-external-store@1.2.0(react@18.3.1): dependencies: @@ -24341,13 +24715,13 @@ snapshots: '@types/unist': 3.0.3 vfile-message: 4.0.2 - vite-node@3.0.5(@types/node@18.19.118)(less@4.3.0)(lightningcss@1.30.1)(sass-embedded@1.86.3)(terser@5.46.1): + vite-node@3.0.5(@types/node@18.19.118)(less@4.3.0)(lightningcss@1.30.1)(sass-embedded@1.86.3)(terser@5.46.2): dependencies: cac: 6.7.14 debug: 4.4.0 es-module-lexer: 1.7.0 pathe: 2.0.3 - vite: 5.4.10(@types/node@18.19.118)(less@4.3.0)(lightningcss@1.30.1)(sass-embedded@1.86.3)(terser@5.46.1) + vite: 5.4.10(@types/node@18.19.118)(less@4.3.0)(lightningcss@1.30.1)(sass-embedded@1.86.3)(terser@5.46.2) transitivePeerDependencies: - '@types/node' - less @@ -24359,13 +24733,13 @@ snapshots: - supports-color - terser - vite-node@3.0.5(@types/node@18.19.130)(less@4.3.0)(lightningcss@1.30.1)(sass-embedded@1.86.3)(terser@5.46.1): + vite-node@3.0.5(@types/node@18.19.130)(less@4.3.0)(lightningcss@1.30.1)(sass-embedded@1.86.3)(terser@5.46.2): dependencies: cac: 6.7.14 debug: 4.4.0 es-module-lexer: 1.7.0 pathe: 2.0.3 - vite: 5.4.10(@types/node@18.19.130)(less@4.3.0)(lightningcss@1.30.1)(sass-embedded@1.86.3)(terser@5.46.1) + vite: 5.4.10(@types/node@18.19.130)(less@4.3.0)(lightningcss@1.30.1)(sass-embedded@1.86.3)(terser@5.46.2) transitivePeerDependencies: - '@types/node' - less @@ -24377,13 +24751,13 @@ snapshots: - supports-color - terser - vite-node@3.0.5(@types/node@18.19.62)(less@4.3.0)(lightningcss@1.30.1)(sass-embedded@1.86.3)(terser@5.46.1): + vite-node@3.0.5(@types/node@18.19.62)(less@4.3.0)(lightningcss@1.30.1)(sass-embedded@1.86.3)(terser@5.46.2): dependencies: cac: 6.7.14 debug: 4.4.0 es-module-lexer: 1.7.0 pathe: 2.0.3 - vite: 5.4.10(@types/node@18.19.62)(less@4.3.0)(lightningcss@1.30.1)(sass-embedded@1.86.3)(terser@5.46.1) + vite: 5.4.10(@types/node@18.19.62)(less@4.3.0)(lightningcss@1.30.1)(sass-embedded@1.86.3)(terser@5.46.2) transitivePeerDependencies: - '@types/node' - less @@ -24395,13 +24769,13 @@ snapshots: - supports-color - terser - vite-node@3.0.5(@types/node@25.5.2)(less@4.2.2)(lightningcss@1.30.1)(sass-embedded@1.86.3)(terser@5.46.1): + vite-node@3.0.5(@types/node@25.6.0)(less@4.2.2)(lightningcss@1.30.1)(sass-embedded@1.86.3)(terser@5.46.2): dependencies: cac: 6.7.14 debug: 4.4.0 es-module-lexer: 1.7.0 pathe: 2.0.3 - vite: 5.4.10(@types/node@25.5.2)(less@4.2.2)(lightningcss@1.30.1)(sass-embedded@1.86.3)(terser@5.46.1) + vite: 5.4.10(@types/node@25.6.0)(less@4.2.2)(lightningcss@1.30.1)(sass-embedded@1.86.3)(terser@5.46.2) transitivePeerDependencies: - '@types/node' - less @@ -24413,13 +24787,13 @@ snapshots: - supports-color - terser - vite-node@3.0.5(@types/node@25.5.2)(less@4.3.0)(lightningcss@1.30.1)(sass-embedded@1.86.3)(terser@5.46.1): + vite-node@3.0.5(@types/node@25.6.0)(less@4.3.0)(lightningcss@1.30.1)(sass-embedded@1.86.3)(terser@5.46.2): dependencies: cac: 6.7.14 debug: 4.4.0 es-module-lexer: 1.7.0 pathe: 2.0.3 - vite: 5.4.10(@types/node@25.5.2)(less@4.3.0)(lightningcss@1.30.1)(sass-embedded@1.86.3)(terser@5.46.1) + vite: 5.4.10(@types/node@25.6.0)(less@4.3.0)(lightningcss@1.30.1)(sass-embedded@1.86.3)(terser@5.46.2) transitivePeerDependencies: - '@types/node' - less @@ -24431,7 +24805,7 @@ snapshots: - supports-color - terser - vite@5.4.10(@types/node@18.19.118)(less@4.3.0)(lightningcss@1.30.1)(sass-embedded@1.86.3)(terser@5.46.1): + vite@5.4.10(@types/node@18.19.118)(less@4.3.0)(lightningcss@1.30.1)(sass-embedded@1.86.3)(terser@5.46.2): dependencies: esbuild: 0.21.5 postcss: 8.5.6 @@ -24442,9 +24816,9 @@ snapshots: less: 4.3.0 lightningcss: 1.30.1 sass-embedded: 1.86.3 - terser: 5.46.1 + terser: 5.46.2 - vite@5.4.10(@types/node@18.19.130)(less@4.3.0)(lightningcss@1.30.1)(sass-embedded@1.86.3)(terser@5.46.1): + vite@5.4.10(@types/node@18.19.130)(less@4.3.0)(lightningcss@1.30.1)(sass-embedded@1.86.3)(terser@5.46.2): dependencies: esbuild: 0.21.5 postcss: 8.5.6 @@ -24455,9 +24829,9 @@ snapshots: less: 4.3.0 lightningcss: 1.30.1 sass-embedded: 1.86.3 - terser: 5.46.1 + terser: 5.46.2 - vite@5.4.10(@types/node@18.19.62)(less@4.3.0)(lightningcss@1.30.1)(sass-embedded@1.86.3)(terser@5.46.1): + vite@5.4.10(@types/node@18.19.62)(less@4.3.0)(lightningcss@1.30.1)(sass-embedded@1.86.3)(terser@5.46.2): dependencies: esbuild: 0.21.5 postcss: 8.5.6 @@ -24468,38 +24842,38 @@ snapshots: less: 4.3.0 lightningcss: 1.30.1 sass-embedded: 1.86.3 - terser: 5.46.1 + terser: 5.46.2 - vite@5.4.10(@types/node@25.5.2)(less@4.2.2)(lightningcss@1.30.1)(sass-embedded@1.86.3)(terser@5.46.1): + vite@5.4.10(@types/node@25.6.0)(less@4.2.2)(lightningcss@1.30.1)(sass-embedded@1.86.3)(terser@5.46.2): dependencies: esbuild: 0.21.5 postcss: 8.5.6 rollup: 4.24.3 optionalDependencies: - '@types/node': 25.5.2 + '@types/node': 25.6.0 fsevents: 2.3.3 less: 4.2.2 lightningcss: 1.30.1 sass-embedded: 1.86.3 - terser: 5.46.1 + terser: 5.46.2 - vite@5.4.10(@types/node@25.5.2)(less@4.3.0)(lightningcss@1.30.1)(sass-embedded@1.86.3)(terser@5.46.1): + vite@5.4.10(@types/node@25.6.0)(less@4.3.0)(lightningcss@1.30.1)(sass-embedded@1.86.3)(terser@5.46.2): dependencies: esbuild: 0.21.5 postcss: 8.5.6 rollup: 4.24.3 optionalDependencies: - '@types/node': 25.5.2 + '@types/node': 25.6.0 fsevents: 2.3.3 less: 4.3.0 lightningcss: 1.30.1 sass-embedded: 1.86.3 - terser: 5.46.1 + terser: 5.46.2 - vitest@3.0.5(@types/debug@4.1.12)(@types/node@18.19.118)(jsdom@29.0.2)(less@4.3.0)(lightningcss@1.30.1)(sass-embedded@1.86.3)(terser@5.46.1): + vitest@3.0.5(@types/debug@4.1.12)(@types/node@18.19.118)(jsdom@29.0.2)(less@4.3.0)(lightningcss@1.30.1)(sass-embedded@1.86.3)(terser@5.46.2): dependencies: '@vitest/expect': 3.0.5 - '@vitest/mocker': 3.0.5(vite@5.4.10(@types/node@18.19.118)(less@4.3.0)(lightningcss@1.30.1)(sass-embedded@1.86.3)(terser@5.46.1)) + '@vitest/mocker': 3.0.5(vite@5.4.10(@types/node@18.19.118)(less@4.3.0)(lightningcss@1.30.1)(sass-embedded@1.86.3)(terser@5.46.2)) '@vitest/pretty-format': 3.1.1 '@vitest/runner': 3.0.5 '@vitest/snapshot': 3.0.5 @@ -24515,8 +24889,8 @@ snapshots: tinyexec: 0.3.2 tinypool: 1.1.1 tinyrainbow: 2.0.0 - vite: 5.4.10(@types/node@18.19.118)(less@4.3.0)(lightningcss@1.30.1)(sass-embedded@1.86.3)(terser@5.46.1) - vite-node: 3.0.5(@types/node@18.19.118)(less@4.3.0)(lightningcss@1.30.1)(sass-embedded@1.86.3)(terser@5.46.1) + vite: 5.4.10(@types/node@18.19.118)(less@4.3.0)(lightningcss@1.30.1)(sass-embedded@1.86.3)(terser@5.46.2) + vite-node: 3.0.5(@types/node@18.19.118)(less@4.3.0)(lightningcss@1.30.1)(sass-embedded@1.86.3)(terser@5.46.2) why-is-node-running: 2.3.0 optionalDependencies: '@types/debug': 4.1.12 @@ -24533,10 +24907,10 @@ snapshots: - supports-color - terser - vitest@3.0.5(@types/debug@4.1.12)(@types/node@18.19.130)(jsdom@29.0.2)(less@4.3.0)(lightningcss@1.30.1)(sass-embedded@1.86.3)(terser@5.46.1): + vitest@3.0.5(@types/debug@4.1.12)(@types/node@18.19.130)(jsdom@29.0.2)(less@4.3.0)(lightningcss@1.30.1)(sass-embedded@1.86.3)(terser@5.46.2): dependencies: '@vitest/expect': 3.0.5 - '@vitest/mocker': 3.0.5(vite@5.4.10(@types/node@18.19.130)(less@4.3.0)(lightningcss@1.30.1)(sass-embedded@1.86.3)(terser@5.46.1)) + '@vitest/mocker': 3.0.5(vite@5.4.10(@types/node@18.19.130)(less@4.3.0)(lightningcss@1.30.1)(sass-embedded@1.86.3)(terser@5.46.2)) '@vitest/pretty-format': 3.1.1 '@vitest/runner': 3.0.5 '@vitest/snapshot': 3.0.5 @@ -24552,8 +24926,8 @@ snapshots: tinyexec: 0.3.2 tinypool: 1.1.1 tinyrainbow: 2.0.0 - vite: 5.4.10(@types/node@18.19.130)(less@4.3.0)(lightningcss@1.30.1)(sass-embedded@1.86.3)(terser@5.46.1) - vite-node: 3.0.5(@types/node@18.19.130)(less@4.3.0)(lightningcss@1.30.1)(sass-embedded@1.86.3)(terser@5.46.1) + vite: 5.4.10(@types/node@18.19.130)(less@4.3.0)(lightningcss@1.30.1)(sass-embedded@1.86.3)(terser@5.46.2) + vite-node: 3.0.5(@types/node@18.19.130)(less@4.3.0)(lightningcss@1.30.1)(sass-embedded@1.86.3)(terser@5.46.2) why-is-node-running: 2.3.0 optionalDependencies: '@types/debug': 4.1.12 @@ -24570,10 +24944,10 @@ snapshots: - supports-color - terser - vitest@3.0.5(@types/debug@4.1.12)(@types/node@18.19.62)(jsdom@29.0.2)(less@4.3.0)(lightningcss@1.30.1)(sass-embedded@1.86.3)(terser@5.46.1): + vitest@3.0.5(@types/debug@4.1.12)(@types/node@18.19.62)(jsdom@29.0.2)(less@4.3.0)(lightningcss@1.30.1)(sass-embedded@1.86.3)(terser@5.46.2): dependencies: '@vitest/expect': 3.0.5 - '@vitest/mocker': 3.0.5(vite@5.4.10(@types/node@18.19.62)(less@4.3.0)(lightningcss@1.30.1)(sass-embedded@1.86.3)(terser@5.46.1)) + '@vitest/mocker': 3.0.5(vite@5.4.10(@types/node@18.19.62)(less@4.3.0)(lightningcss@1.30.1)(sass-embedded@1.86.3)(terser@5.46.2)) '@vitest/pretty-format': 3.1.1 '@vitest/runner': 3.0.5 '@vitest/snapshot': 3.0.5 @@ -24589,8 +24963,8 @@ snapshots: tinyexec: 0.3.2 tinypool: 1.1.1 tinyrainbow: 2.0.0 - vite: 5.4.10(@types/node@18.19.62)(less@4.3.0)(lightningcss@1.30.1)(sass-embedded@1.86.3)(terser@5.46.1) - vite-node: 3.0.5(@types/node@18.19.62)(less@4.3.0)(lightningcss@1.30.1)(sass-embedded@1.86.3)(terser@5.46.1) + vite: 5.4.10(@types/node@18.19.62)(less@4.3.0)(lightningcss@1.30.1)(sass-embedded@1.86.3)(terser@5.46.2) + vite-node: 3.0.5(@types/node@18.19.62)(less@4.3.0)(lightningcss@1.30.1)(sass-embedded@1.86.3)(terser@5.46.2) why-is-node-running: 2.3.0 optionalDependencies: '@types/debug': 4.1.12 @@ -24607,10 +24981,10 @@ snapshots: - supports-color - terser - vitest@3.0.5(@types/debug@4.1.12)(@types/node@25.5.2)(jsdom@29.0.2)(less@4.2.2)(lightningcss@1.30.1)(sass-embedded@1.86.3)(terser@5.46.1): + vitest@3.0.5(@types/debug@4.1.12)(@types/node@25.6.0)(jsdom@29.0.2)(less@4.2.2)(lightningcss@1.30.1)(sass-embedded@1.86.3)(terser@5.46.2): dependencies: '@vitest/expect': 3.0.5 - '@vitest/mocker': 3.0.5(vite@5.4.10(@types/node@25.5.2)(less@4.2.2)(lightningcss@1.30.1)(sass-embedded@1.86.3)(terser@5.46.1)) + '@vitest/mocker': 3.0.5(vite@5.4.10(@types/node@25.6.0)(less@4.2.2)(lightningcss@1.30.1)(sass-embedded@1.86.3)(terser@5.46.2)) '@vitest/pretty-format': 3.1.1 '@vitest/runner': 3.0.5 '@vitest/snapshot': 3.0.5 @@ -24626,12 +25000,12 @@ snapshots: tinyexec: 0.3.2 tinypool: 1.1.1 tinyrainbow: 2.0.0 - vite: 5.4.10(@types/node@25.5.2)(less@4.2.2)(lightningcss@1.30.1)(sass-embedded@1.86.3)(terser@5.46.1) - vite-node: 3.0.5(@types/node@25.5.2)(less@4.2.2)(lightningcss@1.30.1)(sass-embedded@1.86.3)(terser@5.46.1) + vite: 5.4.10(@types/node@25.6.0)(less@4.2.2)(lightningcss@1.30.1)(sass-embedded@1.86.3)(terser@5.46.2) + vite-node: 3.0.5(@types/node@25.6.0)(less@4.2.2)(lightningcss@1.30.1)(sass-embedded@1.86.3)(terser@5.46.2) why-is-node-running: 2.3.0 optionalDependencies: '@types/debug': 4.1.12 - '@types/node': 25.5.2 + '@types/node': 25.6.0 jsdom: 29.0.2 transitivePeerDependencies: - less @@ -24644,10 +25018,10 @@ snapshots: - supports-color - terser - vitest@3.0.5(@types/debug@4.1.12)(@types/node@25.5.2)(jsdom@29.0.2)(less@4.3.0)(lightningcss@1.30.1)(sass-embedded@1.86.3)(terser@5.46.1): + vitest@3.0.5(@types/debug@4.1.12)(@types/node@25.6.0)(jsdom@29.0.2)(less@4.3.0)(lightningcss@1.30.1)(sass-embedded@1.86.3)(terser@5.46.2): dependencies: '@vitest/expect': 3.0.5 - '@vitest/mocker': 3.0.5(vite@5.4.10(@types/node@25.5.2)(less@4.3.0)(lightningcss@1.30.1)(sass-embedded@1.86.3)(terser@5.46.1)) + '@vitest/mocker': 3.0.5(vite@5.4.10(@types/node@25.6.0)(less@4.3.0)(lightningcss@1.30.1)(sass-embedded@1.86.3)(terser@5.46.2)) '@vitest/pretty-format': 3.1.1 '@vitest/runner': 3.0.5 '@vitest/snapshot': 3.0.5 @@ -24663,12 +25037,12 @@ snapshots: tinyexec: 0.3.2 tinypool: 1.1.1 tinyrainbow: 2.0.0 - vite: 5.4.10(@types/node@25.5.2)(less@4.3.0)(lightningcss@1.30.1)(sass-embedded@1.86.3)(terser@5.46.1) - vite-node: 3.0.5(@types/node@25.5.2)(less@4.3.0)(lightningcss@1.30.1)(sass-embedded@1.86.3)(terser@5.46.1) + vite: 5.4.10(@types/node@25.6.0)(less@4.3.0)(lightningcss@1.30.1)(sass-embedded@1.86.3)(terser@5.46.2) + vite-node: 3.0.5(@types/node@25.6.0)(less@4.3.0)(lightningcss@1.30.1)(sass-embedded@1.86.3)(terser@5.46.2) why-is-node-running: 2.3.0 optionalDependencies: '@types/debug': 4.1.12 - '@types/node': 25.5.2 + '@types/node': 25.6.0 jsdom: 29.0.2 transitivePeerDependencies: - less @@ -24767,7 +25141,7 @@ snapshots: - bufferutil - utf-8-validate - webpack-sources@3.3.4: {} + webpack-sources@3.4.0: {} webpack@5.99.5: dependencies: @@ -24779,21 +25153,21 @@ snapshots: acorn: 8.16.0 browserslist: 4.28.0 chrome-trace-event: 1.0.4 - enhanced-resolve: 5.20.1 + enhanced-resolve: 5.21.0 es-module-lexer: 1.7.0 eslint-scope: 5.1.1 events: 3.3.0 glob-to-regexp: 0.4.1 graceful-fs: 4.2.11 json-parse-even-better-errors: 2.3.1 - loader-runner: 4.3.1 + loader-runner: 4.3.2 mime-types: 2.1.35 neo-async: 2.6.2 schema-utils: 4.3.3 - tapable: 2.3.2 - terser-webpack-plugin: 5.4.0(webpack@5.99.5) + tapable: 2.3.3 + terser-webpack-plugin: 5.5.0(webpack@5.99.5) watchpack: 2.5.1 - webpack-sources: 3.3.4 + webpack-sources: 3.4.0 transitivePeerDependencies: - '@swc/core' - esbuild From 367d8cf613062cbf308843a9920739fd05646903 Mon Sep 17 00:00:00 2001 From: fi3ework Date: Mon, 11 May 2026 17:01:58 +0800 Subject: [PATCH 2/3] feat(rstest): expose page/browser escape hatches and align config surface MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Surface raw browser primitives and a secondary-agent factory on `WebTestContext`, matching the native @midscene/web/playwright fixture: - `ctx.page` — raw Playwright/Puppeteer Page (test-scoped) - `ctx.browser` — file-scoped Browser for spinning up extra contexts - `ctx.agentForPage(page, opts?)` — wrap any page in a midscene agent; its report is auto-merged with the primary's at afterEach Re-export `overrideAIConfig` and `WebPageAgentOpt` from each provider entry so programmatic config overrides don't need a second import from `@midscene/web/*`. Drop the public `AgentBundle` / `PageBundle` types in favor of an internal `TestFixture` shape; the lifecycle bundle is now a provider-implementation detail. Rename the demo to `playground.test.ts` to match the monorepo's `demo/playground.*` convention. Docs (en + zh) gain a new "Configure midscene" section covering env vars, `agentOptions`, `overrideAIConfig`, and `DEBUG=midscene:*,rstest:*`, plus examples for the new context surface. --- apps/site/docs/en/integrate-with-rstest.mdx | 91 +++++++++++++- apps/site/docs/zh/integrate-with-rstest.mdx | 91 +++++++++++++- packages/rstest/demo/contacts.test.ts | 58 --------- packages/rstest/demo/playground.test.ts | 91 ++++++++++++++ packages/rstest/src/index.ts | 3 +- packages/rstest/src/lifecycle.ts | 128 ++++++++++++++++--- packages/rstest/src/playwright.ts | 131 ++++++++++++------- packages/rstest/src/puppeteer.ts | 133 +++++++++++++------- 8 files changed, 553 insertions(+), 173 deletions(-) delete mode 100644 packages/rstest/demo/contacts.test.ts create mode 100644 packages/rstest/demo/playground.test.ts diff --git a/apps/site/docs/en/integrate-with-rstest.mdx b/apps/site/docs/en/integrate-with-rstest.mdx index 037e51b21c..482895c7ef 100644 --- a/apps/site/docs/en/integrate-with-rstest.mdx +++ b/apps/site/docs/en/integrate-with-rstest.mdx @@ -99,10 +99,51 @@ describe('Todo list', () => { `createWebTest(url, options)` registers Rstest's `beforeAll` / `beforeEach` / `afterEach` / `afterAll` hooks under the hood. Call it once per `describe` block — every `it` inside it gets its own freshly-navigated page. -:::warning `agent` is only valid inside `it(...)` -The agent is created in `beforeEach` and destroyed in `afterEach`. Reading `ctx.agent` from `describe` scope, a hook, or after the test finishes throws. +:::warning Lifecycle of `ctx` +- `ctx.agent` and `ctx.page` are created in `beforeEach` and torn down in `afterEach`. Reading them from `describe` scope, a hook, or after the test finishes throws. +- `ctx.browser` is launched in `beforeAll` and closed in `afterAll`. Read it inside any hook or `it(...)`. ::: +The context exposes four surfaces, in order of how often you'll reach for them: + +- **`ctx.agent`** — the midscene `PlaywrightAgent` (or `PuppeteerAgent`) bound to the primary page. Default tool for AI actions and assertions. +- **`ctx.page`** — raw Playwright / Puppeteer `Page`. Escape hatch for things `agent` can't or shouldn't do: `page.route(...)` network mocking, `page.evaluate(...)`, `page.context().cookies()`, etc. +- **`ctx.browser`** — the file-scoped `Browser`. Use it to spin up extra contexts or pages mid-test (multi-user, cross-session scenarios). +- **`ctx.agentForPage(page, opts?)`** — build a midscene agent for another page (popup, manually-created page). Its report is merged with the primary's; destroy is automatic. + +```ts +it('mocks the API and verifies the UI', async () => { + const { agent, page } = ctx; + + await page.route('**/api/contacts', (route) => + route.fulfill({ json: [{ name: 'Fake Alice' }] }), + ); + + await agent.aiAssert('the contact list shows "Fake Alice"'); +}); + +it('drives a popup with a second agent', async () => { + const { agent, page, agentForPage } = ctx; + + await agent.aiTap("the 'View details' button"); + const popup = await page.waitForEvent('popup'); + + const popupAgent = await agentForPage(popup); + await popupAgent.aiAssert('the popup shows order details'); +}); + +it('runs two isolated user sessions in one test', async () => { + const { browser, agentForPage } = ctx; + + const sessionB = await browser.newContext({ storageState: 'user-b.json' }); + const pageB = await sessionB.newPage(); + await pageB.goto('http://localhost:5173/'); + const agentB = await agentForPage(pageB); + + await agentB.aiAct('send a message to user A'); +}); +``` + To use Puppeteer instead, change a single import: ```ts @@ -150,6 +191,48 @@ export default defineConfig({ Options passed to `createWebTest(url, options)` shallow-merge over these defaults at the top level — nested fields like `launchOptions` are *replaced*, not deep-merged. Use the function form of a resolver if you need to compose. The Puppeteer provider exposes the same `defineMidsceneDefaults` from `@midscene/rstest/puppeteer`. +## Configure midscene + +Midscene reads configuration from four channels — pick the one that matches the scope you want. + +1. **Environment variables** (model endpoint, API key, vision-family flags, cache dir, report dir, debug toggles): set them however you usually set env vars (`.env`, CI secrets, shell exports). They flow into the midscene runtime automatically — nothing extra to wire up. The full list lives in the [model configuration reference](./model-and-provider). Quick sample: + + ```bash + MIDSCENE_MODEL_BASE_URL=https://api.openai.com/v1 + MIDSCENE_MODEL_API_KEY=sk-... + MIDSCENE_MODEL_NAME=gpt-4o + MIDSCENE_DEBUG_MODEL_RESPONSE=1 # dump raw model responses to disk + MIDSCENE_RUN_DIR=./midscene_run # where reports + cache live + ``` + +2. **`agentOptions`** on each `createWebTest` call — for anything you set per agent. Accepts the full `AgentOpt` surface (`modelConfig`, `aiActionContext`, `cache`, `replanningCycleLimit`, `waitAfterAction`, `screenshotShrinkFactor`, `onTaskStartTip`, `onOpenAIClientCreated`, …). `groupName` and `reportFileName` are reserved by the lifecycle. + + ```ts + const ctx = createWebTest(url, { + agentOptions: { + aiActionContext: 'You are testing a multilingual checkout flow.', + cache: { strategy: 'read-write' }, + waitAfterAction: 500, + }, + }); + ``` + +3. **`overrideAIConfig(...)`** in a setup file — for programmatic global overrides that env vars can't express, or for swapping config between test runs. Re-exported from both provider entry points. + + ```ts title="midscene.setup.ts" + import { overrideAIConfig } from '@midscene/rstest/playwright'; + + overrideAIConfig({ + MIDSCENE_MODEL_NAME: process.env.CI ? 'gpt-4o' : 'gpt-4o-mini', + }); + ``` + +4. **Debug logs** — `@midscene/rstest` uses the `debug` package; turn on what you want via the `DEBUG` env var. + + ```bash + DEBUG=midscene:*,rstest:* npx rstest run + ``` + ## Options ### Playwright (`@midscene/rstest/playwright`) @@ -162,7 +245,7 @@ Options passed to `createWebTest(url, options)` shallow-merge over these default | `contextOptions` | `BrowserContextOptions \| (defaults) => BrowserContextOptions` | — | Forwarded to `browser.newContext(...)`. | | `gotoOptions` | `Parameters[1]` | — | Forwarded to `page.goto(url, ...)`. | | `agentOptions` | `WebPageAgentOpt` | — | Forwarded to `PlaywrightAgent` (e.g. `aiActionContext`, `modelConfig`). `groupName` and `reportFileName` are managed by the lifecycle. | -| `setup` | `(api) => Promise` | — | See [Custom page setup](#custom-page-setup) below. | +| `setup` | `(api) => Promise<{ page; teardown? }>` | — | See [Custom page setup](#custom-page-setup) below. | ### Puppeteer (`@midscene/rstest/puppeteer`) @@ -173,7 +256,7 @@ Options passed to `createWebTest(url, options)` shallow-merge over these default | `launchOptions` | `LaunchOptions \| (defaults) => LaunchOptions` | — | Forwarded to `puppeteer.launch(...)`. | | `gotoOptions` | `GoToOptions` | — | Forwarded to `page.goto(url, ...)`. | | `agentOptions` | `WebPageAgentOpt` | — | Forwarded to `PuppeteerAgent`. | -| `setup` | `(api) => Promise` | — | See [Custom page setup](#custom-page-setup) below. | +| `setup` | `(api) => Promise<{ page; teardown? }>` | — | See [Custom page setup](#custom-page-setup) below. | Resolver fields (`launchOptions`, `contextOptions`) accept either an object — shallow-merged over midscene's defaults — or a function `(defaults) => options` that takes full control. diff --git a/apps/site/docs/zh/integrate-with-rstest.mdx b/apps/site/docs/zh/integrate-with-rstest.mdx index b87ad6234a..d5cc795e85 100644 --- a/apps/site/docs/zh/integrate-with-rstest.mdx +++ b/apps/site/docs/zh/integrate-with-rstest.mdx @@ -99,10 +99,51 @@ describe('Todo list', () => { `createWebTest(url, options)` 会在内部注册 Rstest 的 `beforeAll` / `beforeEach` / `afterEach` / `afterAll` 钩子。在每个 `describe` 块里调用一次即可,里面的每个 `it` 都会拿到一份重新导航过的全新页面。 -:::warning `agent` 仅在 `it(...)` 内可用 -agent 会在 `beforeEach` 中创建、`afterEach` 中销毁。在 `describe` 作用域、其他钩子、或 test 结束之后访问 `ctx.agent` 都会抛错。 +:::warning `ctx` 的生命周期 +- `ctx.agent` 与 `ctx.page` 在 `beforeEach` 中创建、`afterEach` 中销毁。在 `describe` 作用域、其他钩子、或 test 结束之后访问会抛错。 +- `ctx.browser` 在 `beforeAll` 启动、`afterAll` 关闭。任意 hook 或 `it(...)` 内都可读。 ::: +`ctx` 暴露四个 surface,按使用频率排序: + +- **`ctx.agent`** —— 绑在主 page 上的 midscene `PlaywrightAgent`(或 `PuppeteerAgent`)。AI action 和 assertion 的默认入口。 +- **`ctx.page`** —— 底层的 Playwright / Puppeteer `Page`。`agent` 做不到或不该做的事的 escape hatch:`page.route(...)` network mock、`page.evaluate(...)`、`page.context().cookies()` 等。 +- **`ctx.browser`** —— file-scoped 的 `Browser`。在测试中开新 context / 新 page(多用户、跨会话场景)用。 +- **`ctx.agentForPage(page, opts?)`** —— 为另一个 page(popup、手动开的 page)构造一个 midscene agent,报告会和主 agent 合并,销毁自动完成。 + +```ts +it('mocks the API and verifies the UI', async () => { + const { agent, page } = ctx; + + await page.route('**/api/contacts', (route) => + route.fulfill({ json: [{ name: 'Fake Alice' }] }), + ); + + await agent.aiAssert('the contact list shows "Fake Alice"'); +}); + +it('drives a popup with a second agent', async () => { + const { agent, page, agentForPage } = ctx; + + await agent.aiTap("the 'View details' button"); + const popup = await page.waitForEvent('popup'); + + const popupAgent = await agentForPage(popup); + await popupAgent.aiAssert('the popup shows order details'); +}); + +it('runs two isolated user sessions in one test', async () => { + const { browser, agentForPage } = ctx; + + const sessionB = await browser.newContext({ storageState: 'user-b.json' }); + const pageB = await sessionB.newPage(); + await pageB.goto('http://localhost:5173/'); + const agentB = await agentForPage(pageB); + + await agentB.aiAct('send a message to user A'); +}); +``` + 如果想换成 Puppeteer,只需改一行 import: ```ts @@ -150,6 +191,48 @@ export default defineConfig({ 调用 `createWebTest(url, options)` 时传入的 options 会按顶层 key 浅合并到这里的默认值上 —— `launchOptions` 这种嵌套字段是**整体替换**,不是深度合并。需要组合可以用 resolver 的函数形式。Puppeteer provider 在 `@midscene/rstest/puppeteer` 提供同名 `defineMidsceneDefaults`。 +## 配置 midscene + +midscene 从四个渠道读取配置,按你想影响的作用域选一个。 + +1. **环境变量**(模型端点、API key、vision-family 开关、cache 目录、report 目录、debug 开关):用你平时设环境变量的方式即可(`.env`、CI secret、shell export)。midscene runtime 会自动读取,不用额外接线。完整列表见 [model 配置参考](./model-and-provider)。示例: + + ```bash + MIDSCENE_MODEL_BASE_URL=https://api.openai.com/v1 + MIDSCENE_MODEL_API_KEY=sk-... + MIDSCENE_MODEL_NAME=gpt-4o + MIDSCENE_DEBUG_MODEL_RESPONSE=1 # 把模型原始响应 dump 到磁盘 + MIDSCENE_RUN_DIR=./midscene_run # 报告 + 缓存的根目录 + ``` + +2. **`agentOptions`** —— 每次 `createWebTest` 都可以传,针对 per-agent 配置。接收完整 `AgentOpt`(`modelConfig`、`aiActionContext`、`cache`、`replanningCycleLimit`、`waitAfterAction`、`screenshotShrinkFactor`、`onTaskStartTip`、`onOpenAIClientCreated` 等)。`groupName` 和 `reportFileName` 由 lifecycle 保留。 + + ```ts + const ctx = createWebTest(url, { + agentOptions: { + aiActionContext: 'You are testing a multilingual checkout flow.', + cache: { strategy: 'read-write' }, + waitAfterAction: 500, + }, + }); + ``` + +3. **`overrideAIConfig(...)`** —— 在 setup file 里用,做环境变量表达不了的运行时全局覆盖(比如根据是否 CI 切模型)。两个 provider 入口都 re-export 了。 + + ```ts title="midscene.setup.ts" + import { overrideAIConfig } from '@midscene/rstest/playwright'; + + overrideAIConfig({ + MIDSCENE_MODEL_NAME: process.env.CI ? 'gpt-4o' : 'gpt-4o-mini', + }); + ``` + +4. **Debug 日志** —— `@midscene/rstest` 用 `debug` 包,按需通过 `DEBUG` 环境变量打开 topic: + + ```bash + DEBUG=midscene:*,rstest:* npx rstest run + ``` + ## 选项 ### Playwright (`@midscene/rstest/playwright`) @@ -162,7 +245,7 @@ export default defineConfig({ | `contextOptions` | `BrowserContextOptions \| (defaults) => BrowserContextOptions` | — | 透传到 `browser.newContext(...)`。 | | `gotoOptions` | `Parameters[1]` | — | 透传到 `page.goto(url, ...)`。 | | `agentOptions` | `WebPageAgentOpt` | — | 透传给 `PlaywrightAgent`(如 `aiActionContext`、`modelConfig`)。`groupName` 与 `reportFileName` 由 lifecycle 管理。 | -| `setup` | `(api) => Promise` | — | 详见下方 [自定义 page 设置](#自定义-page-设置)。 | +| `setup` | `(api) => Promise<{ page; teardown? }>` | — | 详见下方 [自定义 page 设置](#自定义-page-设置)。 | ### Puppeteer (`@midscene/rstest/puppeteer`) @@ -173,7 +256,7 @@ export default defineConfig({ | `launchOptions` | `LaunchOptions \| (defaults) => LaunchOptions` | — | 透传到 `puppeteer.launch(...)`。 | | `gotoOptions` | `GoToOptions` | — | 透传到 `page.goto(url, ...)`。 | | `agentOptions` | `WebPageAgentOpt` | — | 透传给 `PuppeteerAgent`。 | -| `setup` | `(api) => Promise` | — | 详见下方 [自定义 page 设置](#自定义-page-设置)。 | +| `setup` | `(api) => Promise<{ page; teardown? }>` | — | 详见下方 [自定义 page 设置](#自定义-page-设置)。 | `launchOptions`、`contextOptions` 这类 resolver 字段同时接受两种形式:传对象会和 midscene 的默认值浅合并;传函数 `(defaults) => options` 则完全接管。 diff --git a/packages/rstest/demo/contacts.test.ts b/packages/rstest/demo/contacts.test.ts deleted file mode 100644 index 1a186f202a..0000000000 --- a/packages/rstest/demo/contacts.test.ts +++ /dev/null @@ -1,58 +0,0 @@ -import { createWebTest } from '@midscene/rstest/playwright'; -import { describe, expect, it } from '@rstest/core'; - -const PAGE_URL = - 'https://lf3-static.bytednsdoc.com/obj/eden-cn/nupipfups/Midscene/contacts3.html'; - -describe('Contacts page', () => { - const ctx = createWebTest(PAGE_URL); - - it('renders the smart contacts header and grid', async () => { - const { agent } = ctx; - await agent.aiAssert( - 'the page header reads "Smart Contacts" with the subtitle "Midscene AI-powered Contact Management"', - ); - await agent.aiAssert( - 'a grid of contact cards is visible, each card shows an avatar, a name, a position, and detail rows for phone, email, company, address and last contact date', - ); - }); - - it('lists every contact with the expected fields', async () => { - const { agent } = ctx; - const contacts = await agent.aiQuery< - { name: string; position: string; email: string }[] - >( - 'Array<{name: string, position: string, email: string}>, the name (heading), position (line under the name) and email address shown on every contact card', - ); - - expect(contacts).toHaveLength(5); - const byName = Object.fromEntries(contacts.map((c) => [c.name, c])); - expect(byName['Alice Johnson']?.position).toBe('Senior Software Engineer'); - expect(byName['Alice Johnson']?.email).toBe('alice.johnson@techcorp.com'); - expect(byName['Bob Wilson']?.position).toBe('UI/UX Designer'); - expect(byName['Carol Davis']?.position).toBe('Sales Director'); - expect(byName['David Brown']?.position).toBe('Marketing Manager'); - expect(byName['Emma Taylor']?.position).toBe('HR Manager'); - }); - - it('opens the custom context menu on right-click', async () => { - const { agent } = ctx; - await agent.aiRightClick("Alice Johnson's contact card"); - await agent.aiWaitFor( - 'a context menu is visible with the items "Call Contact", "Send Email", "Send Message", "Edit Contact", "Copy Info" and "Delete Contact"', - { timeoutMs: 10_000 }, - ); - - const items = await agent.aiQuery( - 'string[], the visible text of every item inside the open context menu, in order', - ); - expect(items).toEqual([ - 'Call Contact', - 'Send Email', - 'Send Message', - 'Edit Contact', - 'Copy Info', - 'Delete Contact', - ]); - }); -}); diff --git a/packages/rstest/demo/playground.test.ts b/packages/rstest/demo/playground.test.ts new file mode 100644 index 0000000000..12f83e5604 --- /dev/null +++ b/packages/rstest/demo/playground.test.ts @@ -0,0 +1,91 @@ +import { createWebTest } from '@midscene/rstest/playwright'; +import { describe, expect, it } from '@rstest/core'; + +const PAGE_URL = + 'https://lf3-static.bytednsdoc.com/obj/eden-cn/nupipfups/Midscene/contacts3.html'; + +describe('Contacts page', () => { + const ctx = createWebTest(PAGE_URL); + + // Pattern: semantic UI check via `aiAssert`. + it('renders the smart contacts header and grid', async () => { + const { agent } = ctx; + await agent.aiAssert( + 'the page header reads "Smart Contacts" with a grid of contact cards below it, each card showing an avatar, name, position, and contact details', + ); + }); + + // Pattern: structured data extraction via `aiQuery` + deterministic + // comparison via rstest `expect`. + it('lists every contact with the expected fields', async () => { + const { agent } = ctx; + const contacts = await agent.aiQuery< + { name: string; position: string; email: string }[] + >( + 'Array<{name: string, position: string, email: string}>, the name (heading), position (line under the name) and email address shown on every contact card', + ); + + expect(contacts).toHaveLength(5); + const byName = Object.fromEntries(contacts.map((c) => [c.name, c])); + expect(byName['Alice Johnson']).toEqual({ + name: 'Alice Johnson', + position: 'Senior Software Engineer', + email: 'alice.johnson@techcorp.com', + }); + }); + + // Pattern: AI interaction (`aiRightClick`) + AI synchronization (`aiWaitFor`) + // + AI extraction (`aiQuery`) chained together. + it('opens the custom context menu on right-click', async () => { + const { agent } = ctx; + await agent.aiRightClick("Alice Johnson's contact card"); + await agent.aiWaitFor( + 'a context menu is visible with the items "Call Contact", "Send Email", "Send Message", "Edit Contact", "Copy Info" and "Delete Contact"', + { timeoutMs: 10_000 }, + ); + + const items = await agent.aiQuery( + 'string[], the visible text of every item inside the open context menu, in order', + ); + expect(items).toEqual([ + 'Call Contact', + 'Send Email', + 'Send Message', + 'Edit Contact', + 'Copy Info', + 'Delete Contact', + ]); + }); + + // Escape hatch: raw Playwright `Page` for browser-primitive checks that + // don't need the AI. + it('inspects raw page state via the Playwright page escape hatch', async () => { + const { page } = ctx; + + expect(page.url()).toBe(PAGE_URL); + + const viewport = page.viewportSize(); + expect(viewport?.width).toBeGreaterThan(0); + expect(viewport?.height).toBeGreaterThan(0); + + const title = await page.title(); + expect(title.length).toBeGreaterThan(0); + }); + + // Multi-session: open a second isolated browser context (think "another + // user") and drive it with a separate midscene agent via `agentForPage`. + // The secondary's report is merged alongside the primary's at afterEach; + // destroy is automatic. + it('drives a second isolated session via browser + agentForPage', async () => { + const { browser, agentForPage } = ctx; + + const sessionB = await browser.newContext(); + const pageB = await sessionB.newPage(); + await pageB.goto(PAGE_URL); + + const agentB = await agentForPage(pageB); + await agentB.aiAssert( + 'the contact grid is visible and contains "Alice Johnson"', + ); + }); +}); diff --git a/packages/rstest/src/index.ts b/packages/rstest/src/index.ts index f0fb414731..9d19af6943 100644 --- a/packages/rstest/src/index.ts +++ b/packages/rstest/src/index.ts @@ -1,6 +1,7 @@ export { registerLifecycle, - type AgentBundle, + type LifecycleContext, type LifecycleProvider, + type ReportMeta, } from './lifecycle'; export type { RstestTestContext } from './report-helper'; diff --git a/packages/rstest/src/lifecycle.ts b/packages/rstest/src/lifecycle.ts index 3332f34a05..dbb6bc9245 100644 --- a/packages/rstest/src/lifecycle.ts +++ b/packages/rstest/src/lifecycle.ts @@ -13,14 +13,21 @@ interface SuiteContext { filepath: string; } -export interface AgentBundle { +export interface ReportMeta { + groupName: string; + reportFileName: string; +} + +interface TestFixture { agent: TAgent; + page: TPage; /** Runs before `agent.destroy()` — use for tasks that need the page alive (e.g. stop trace). */ teardown?: (testCtx: RstestTestContext) => Promise; } export interface LifecycleProvider< TAgent extends AgentLike, + TPage, TBrowser, TOptions, > { @@ -30,20 +37,44 @@ export interface LifecycleProvider< browser: TBrowser, url: string, options: TOptions, - meta: { groupName: string; reportFileName: string }, - ): Promise>; + meta: ReportMeta, + ): Promise>; +} + +export interface LifecycleContext { + readonly agent: TAgent; + readonly page: TPage; + readonly browser: TBrowser; + /** + * Build and track a secondary agent for the current test (e.g. for a popup + * or a second tab). The factory receives a unique `ReportMeta` so the + * secondary's report is merged alongside the primary's. Destroy + report + * collection happen automatically in `afterEach`. + */ + spawnSecondaryAgent(build: (meta: ReportMeta) => T): T; } -/** Returned `.agent` is only valid inside `it(...)` — it's created in `beforeEach`. */ -export function registerLifecycle( +/** + * Registers the per-suite lifecycle. `.agent` / `.page` are only valid inside + * `it(...)`; `.browser` is valid between `beforeAll` and `afterAll`. + */ +export function registerLifecycle< + TAgent extends AgentLike, + TPage, + TBrowser, + TOptions, +>( url: string, options: TOptions, - provider: LifecycleProvider, -): { readonly agent: TAgent } { + provider: LifecycleProvider, +): LifecycleContext { const reportHelper = new ReportHelper(); let browser: TBrowser | null = null; let filepath = ''; - let currentBundle: AgentBundle | null = null; + let currentFixture: TestFixture | null = null; + let currentMeta: ReportMeta | null = null; + const secondaryAgents: AgentLike[] = []; + let secondaryCounter = 0; let startTime = 0; beforeAll(async (suite: SuiteContext) => { @@ -54,26 +85,50 @@ export function registerLifecycle( beforeEach(async (testCtx) => { if (!browser) throw new Error('[@midscene/rstest] browser not initialized'); - const meta = buildReportMeta(testCtx as RstestTestContext, filepath); - currentBundle = await provider.createAgent(browser, url, options, meta); + currentMeta = buildReportMeta(testCtx as RstestTestContext, filepath); + secondaryCounter = 0; + currentFixture = await provider.createAgent( + browser, + url, + options, + currentMeta, + ); startTime = performance.now(); }); afterEach(async (testCtx) => { - const bundle = currentBundle; - currentBundle = null; + const fixture = currentFixture; + const secondaries = secondaryAgents.slice(); + currentFixture = null; + currentMeta = null; + secondaryAgents.length = 0; + secondaryCounter = 0; + + // Collect secondaries first so their pages are still alive when destroy() + // writes their report file. + for (const secondary of secondaries) { + try { + await reportHelper.collectReport( + secondary, + startTime, + testCtx as RstestTestContext, + ); + } catch (err) { + debug('secondary agent report failed:', err); + } + } - if (bundle?.teardown) { + if (fixture?.teardown) { try { - await bundle.teardown(testCtx as RstestTestContext); + await fixture.teardown(testCtx as RstestTestContext); } catch (err) { debug('provider teardown failed:', err); } } await reportHelper.collectReport( - bundle?.agent, - bundle ? startTime : undefined, + fixture?.agent, + fixture ? startTime : undefined, testCtx as RstestTestContext, ); }); @@ -86,14 +141,47 @@ export function registerLifecycle( } }); + function requireFixture( + field: K, + ): TestFixture[K] { + if (!currentFixture) { + throw new Error( + `[@midscene/rstest] ${field} is only available inside \`it(...)\` blocks`, + ); + } + return currentFixture[field]; + } + return { - get agent(): TAgent { - if (!currentBundle) { + get agent() { + return requireFixture('agent'); + }, + get page() { + return requireFixture('page'); + }, + get browser(): TBrowser { + if (!browser) { + throw new Error( + '[@midscene/rstest] browser is only available between `beforeAll` and `afterAll`', + ); + } + return browser; + }, + spawnSecondaryAgent( + build: (meta: ReportMeta) => T, + ): T { + if (!currentMeta) { throw new Error( - '[@midscene/rstest] agent is only available inside `it(...)` blocks', + '[@midscene/rstest] secondary agents can only be spawned inside `it(...)` blocks', ); } - return currentBundle.agent; + const idx = ++secondaryCounter; + const agent = build({ + groupName: currentMeta.groupName, + reportFileName: `${currentMeta.reportFileName}-page${idx}`, + }); + secondaryAgents.push(agent); + return agent; }, }; } diff --git a/packages/rstest/src/playwright.ts b/packages/rstest/src/playwright.ts index 03f570d39b..d667f6fb36 100644 --- a/packages/rstest/src/playwright.ts +++ b/packages/rstest/src/playwright.ts @@ -1,6 +1,7 @@ import { PlaywrightAgent, type WebPageAgentOpt, + overrideAIConfig, } from '@midscene/web/playwright'; import { type Browser, @@ -21,6 +22,8 @@ import type { RstestTestContext } from './report-helper'; import { type Resolver, applyResolver } from './resolve'; export type { Resolver }; +export { overrideAIConfig }; +export type { WebPageAgentOpt }; type GoToOptions = NonNullable[1]>; @@ -30,12 +33,6 @@ export interface SetupApi { playwright: typeof playwrightNs; } -export interface PageBundle { - page: Page; - /** Runs before `agent.destroy()`, while the page is still alive. */ - teardown?: (testCtx: RstestTestContext) => Promise; -} - export interface CreateWebTestOptions { /** Default: `true` in CI, `false` locally. */ headless?: boolean; @@ -49,16 +46,43 @@ export interface CreateWebTestOptions { agentOptions?: Omit; /** - * Take over the per-test page lifecycle. When provided, midscene skips its + * Take over the per-test page lifecycle. Return the page midscene should + * drive, plus an optional `teardown` that runs while the page is still + * alive (before `agent.destroy()`). When provided, midscene skips its * default page setup; `headless`, `viewport`, `launchOptions`, * `contextOptions`, and `gotoOptions` are all ignored. Only `agentOptions` * still applies. */ - setup?: (api: SetupApi) => Promise; + setup?: (api: SetupApi) => Promise<{ + page: Page; + teardown?: (testCtx: RstestTestContext) => Promise; + }>; } export interface WebTestContext { readonly agent: PlaywrightAgent; + /** + * Raw Playwright `Page` for advanced scenarios — `page.route`, + * `page.evaluate`, `page.context().cookies()`, etc. Prefer `agent` for AI + * actions and assertions; reach for `page` only when you need + * browser-primitive control. + */ + readonly page: Page; + /** + * The file-scoped Playwright `Browser`. Use it to spin up extra contexts or + * pages mid-test (e.g. a second user session). Valid between `beforeAll` + * and `afterAll`. + */ + readonly browser: Browser; + /** + * Build a midscene agent for another page (popup, new tab, manually-created + * page from `browser.newContext()`). The agent's report is merged alongside + * the primary's. Destroy is automatic in `afterEach`. + */ + agentForPage( + page: Page, + opts?: Omit, + ): Promise; } const defaultsStore = createDefaultsStore(); @@ -71,10 +95,7 @@ const defaultsStore = createDefaultsStore(); */ export const defineMidsceneDefaults = defaultsStore.define; -async function defaultSetup( - api: SetupApi, - opts: CreateWebTestOptions, -): Promise { +async function defaultSetup(api: SetupApi, opts: CreateWebTestOptions) { const contextOptions = await applyResolver(opts.contextOptions, { viewport: opts.viewport ?? DEFAULT_VIEWPORT, }); @@ -100,36 +121,60 @@ export function createWebTest( ): WebTestContext { const merged: CreateWebTestOptions = { ...defaultsStore.get(), ...options }; - return registerLifecycle( - url, - merged, - { - async launchBrowser(opts) { - const launchOptions = await applyResolver(opts.launchOptions, { - headless: opts.headless ?? isCI, - args: DEFAULT_BROWSER_ARGS, - }); - return chromium.launch(launchOptions); - }, - async closeBrowser(browser) { - await browser.close(); - }, - async createAgent(browser, targetUrl, opts, meta) { - const setup = opts.setup ?? ((api) => defaultSetup(api, opts)); - const bundle = await setup({ - url: targetUrl, - browser, - playwright: playwrightNs, - }); - - const agent = new PlaywrightAgent(bundle.page, { - ...opts.agentOptions, - groupName: meta.groupName, - reportFileName: meta.reportFileName, - }); - - return { agent, teardown: bundle.teardown }; - }, + const inner = registerLifecycle< + PlaywrightAgent, + Page, + Browser, + CreateWebTestOptions + >(url, merged, { + async launchBrowser(opts) { + const launchOptions = await applyResolver(opts.launchOptions, { + headless: opts.headless ?? isCI, + args: DEFAULT_BROWSER_ARGS, + }); + return chromium.launch(launchOptions); + }, + async closeBrowser(browser) { + await browser.close(); + }, + async createAgent(browser, targetUrl, opts, meta) { + const setup = opts.setup ?? ((api) => defaultSetup(api, opts)); + const { page, teardown } = await setup({ + url: targetUrl, + browser, + playwright: playwrightNs, + }); + + const agent = new PlaywrightAgent(page, { + ...opts.agentOptions, + groupName: meta.groupName, + reportFileName: meta.reportFileName, + }); + + return { agent, page, teardown }; + }, + }); + + return { + get agent() { + return inner.agent; }, - ); + get page() { + return inner.page; + }, + get browser() { + return inner.browser; + }, + async agentForPage(page, opts) { + return inner.spawnSecondaryAgent( + (meta) => + new PlaywrightAgent(page, { + ...merged.agentOptions, + ...opts, + groupName: meta.groupName, + reportFileName: meta.reportFileName, + }), + ); + }, + }; } diff --git a/packages/rstest/src/puppeteer.ts b/packages/rstest/src/puppeteer.ts index 344a5095b2..7b58b6b50d 100644 --- a/packages/rstest/src/puppeteer.ts +++ b/packages/rstest/src/puppeteer.ts @@ -1,4 +1,8 @@ -import { PuppeteerAgent, type WebPageAgentOpt } from '@midscene/web/puppeteer'; +import { + PuppeteerAgent, + type WebPageAgentOpt, + overrideAIConfig, +} from '@midscene/web/puppeteer'; import puppeteer, { type Browser, type GoToOptions, @@ -16,6 +20,8 @@ import type { RstestTestContext } from './report-helper'; import { type Resolver, applyResolver } from './resolve'; export type { Resolver }; +export { overrideAIConfig }; +export type { WebPageAgentOpt }; export interface SetupApi { url: string; @@ -23,12 +29,6 @@ export interface SetupApi { puppeteer: typeof puppeteer; } -export interface PageBundle { - page: Page; - /** Runs before `agent.destroy()`, while the page is still alive. */ - teardown?: (testCtx: RstestTestContext) => Promise; -} - export interface CreateWebTestOptions { /** Default: `true` in CI, `false` locally. */ headless?: boolean; @@ -41,15 +41,41 @@ export interface CreateWebTestOptions { agentOptions?: Omit; /** - * Take over the per-test page lifecycle. When provided, midscene skips its + * Take over the per-test page lifecycle. Return the page midscene should + * drive, plus an optional `teardown` that runs while the page is still + * alive (before `agent.destroy()`). When provided, midscene skips its * default page setup; `headless`, `viewport`, `launchOptions`, and * `gotoOptions` are all ignored. Only `agentOptions` still applies. */ - setup?: (api: SetupApi) => Promise; + setup?: (api: SetupApi) => Promise<{ + page: Page; + teardown?: (testCtx: RstestTestContext) => Promise; + }>; } export interface WebTestContext { readonly agent: PuppeteerAgent; + /** + * Raw Puppeteer `Page` for advanced scenarios — `page.setRequestInterception`, + * `page.evaluate`, etc. Prefer `agent` for AI actions and assertions; reach + * for `page` only when you need browser-primitive control. + */ + readonly page: Page; + /** + * The file-scoped Puppeteer `Browser`. Use it to open extra pages mid-test + * (e.g. a second user session via `browser.createBrowserContext()` + + * `context.newPage()`). Valid between `beforeAll` and `afterAll`. + */ + readonly browser: Browser; + /** + * Build a midscene agent for another page (popup, manually-created page). + * The agent's report is merged alongside the primary's. Destroy is automatic + * in `afterEach`. + */ + agentForPage( + page: Page, + opts?: Omit, + ): Promise; } const defaultsStore = createDefaultsStore(); @@ -62,10 +88,7 @@ const defaultsStore = createDefaultsStore(); */ export const defineMidsceneDefaults = defaultsStore.define; -async function defaultSetup( - api: SetupApi, - opts: CreateWebTestOptions, -): Promise { +async function defaultSetup(api: SetupApi, opts: CreateWebTestOptions) { const page = await api.browser.newPage(); await page.goto(api.url, opts.gotoOptions); return { @@ -86,37 +109,61 @@ export function createWebTest( ): WebTestContext { const merged: CreateWebTestOptions = { ...defaultsStore.get(), ...options }; - return registerLifecycle( - url, - merged, - { - async launchBrowser(opts) { - const launchOptions = await applyResolver(opts.launchOptions, { - headless: opts.headless ?? isCI, - args: DEFAULT_BROWSER_ARGS, - defaultViewport: opts.viewport ?? DEFAULT_VIEWPORT, - }); - return puppeteer.launch(launchOptions); - }, - async closeBrowser(browser) { - await browser.close(); - }, - async createAgent(browser, targetUrl, opts, meta) { - const setup = opts.setup ?? ((api) => defaultSetup(api, opts)); - const bundle = await setup({ - url: targetUrl, - browser, - puppeteer, - }); + const inner = registerLifecycle< + PuppeteerAgent, + Page, + Browser, + CreateWebTestOptions + >(url, merged, { + async launchBrowser(opts) { + const launchOptions = await applyResolver(opts.launchOptions, { + headless: opts.headless ?? isCI, + args: DEFAULT_BROWSER_ARGS, + defaultViewport: opts.viewport ?? DEFAULT_VIEWPORT, + }); + return puppeteer.launch(launchOptions); + }, + async closeBrowser(browser) { + await browser.close(); + }, + async createAgent(browser, targetUrl, opts, meta) { + const setup = opts.setup ?? ((api) => defaultSetup(api, opts)); + const { page, teardown } = await setup({ + url: targetUrl, + browser, + puppeteer, + }); - const agent = new PuppeteerAgent(bundle.page, { - ...opts.agentOptions, - groupName: meta.groupName, - reportFileName: meta.reportFileName, - }); + const agent = new PuppeteerAgent(page, { + ...opts.agentOptions, + groupName: meta.groupName, + reportFileName: meta.reportFileName, + }); - return { agent, teardown: bundle.teardown }; - }, + return { agent, page, teardown }; + }, + }); + + return { + get agent() { + return inner.agent; }, - ); + get page() { + return inner.page; + }, + get browser() { + return inner.browser; + }, + async agentForPage(page, opts) { + return inner.spawnSecondaryAgent( + (meta) => + new PuppeteerAgent(page, { + ...merged.agentOptions, + ...opts, + groupName: meta.groupName, + reportFileName: meta.reportFileName, + }), + ); + }, + }; } From b3c5101849f908901559cbf1a15a7302446870ff Mon Sep 17 00:00:00 2001 From: fi3ework Date: Mon, 11 May 2026 20:13:49 +0800 Subject: [PATCH 3/3] refactor(rstest): replace createWebTest with test.extend fixtures MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Reshape the public API from ambient `createWebTest(url)` hooks to rstest fixtures destructured straight off the test signature: - `agent`, `page`, `browser`, `context`, `agentForPage` are typed fixtures composed via dependency injection. - File-scoped browser is launched lazily — files that don't reach through to `agent` skip browser startup entirely. - Per-file URL via `test.extend({ url })`; custom page lifecycles via overriding the `page` fixture (replaces the `setup` callback). - Promise-based browser singleton avoids the TOCTOU race that would double-launch under `test.concurrent`. - Shared `__reportMeta` private fixture stabilizes the timestamp embedded in primary + secondary report filenames within one test. `test-api-types.ts` works around rstest 0.9.9 not exporting its `TestAPIs` / `TestContext` / `Fixtures` types — delete the file once they become public. Drops `createWebTest` and `registerLifecycle` exports. --- apps/site/docs/en/integrate-with-rstest.mdx | 272 ++++++++------ apps/site/docs/zh/integrate-with-rstest.mdx | 276 ++++++++------ packages/rstest/demo/playground.test.ts | 38 +- packages/rstest/package.json | 6 +- packages/rstest/src/index.ts | 8 +- packages/rstest/src/lifecycle.ts | 187 ---------- packages/rstest/src/playwright.ts | 336 +++++++++++------- packages/rstest/src/puppeteer.ts | 309 +++++++++------- packages/rstest/src/report-helper.ts | 20 +- packages/rstest/src/resolve.ts | 42 ++- packages/rstest/src/test-api-types.ts | 85 +++++ .../rstest/tests/unit-test/resolve.test.ts | 34 +- pnpm-lock.yaml | 31 +- 13 files changed, 919 insertions(+), 725 deletions(-) delete mode 100644 packages/rstest/src/lifecycle.ts create mode 100644 packages/rstest/src/test-api-types.ts diff --git a/apps/site/docs/en/integrate-with-rstest.mdx b/apps/site/docs/en/integrate-with-rstest.mdx index 482895c7ef..0d59195228 100644 --- a/apps/site/docs/en/integrate-with-rstest.mdx +++ b/apps/site/docs/en/integrate-with-rstest.mdx @@ -3,29 +3,26 @@ import { PackageManagerTabs } from '@theme'; # Integrate with Rstest -[Rstest](https://github.com/web-infra-dev/rstest) is a fast Rspack-based testing framework. With `@midscene/rstest` you can write AI-driven end-to-end tests in the same `describe / it` style you already use — Midscene takes care of launching the browser, creating an Agent for each test, and producing an HTML report at the end of every test file. +[Rstest](https://github.com/web-infra-dev/rstest) is a fast Rspack-based testing framework. With `@midscene/rstest` you can write AI-driven end-to-end tests as plain rstest fixtures — Midscene takes care of launching the browser, creating an Agent per test, and producing an HTML report at the end of every test file. ```ts -import { describe, expect, it } from '@rstest/core'; -import { createWebTest } from '@midscene/rstest/playwright'; +import { test as base } from '@midscene/rstest/playwright'; +import { expect } from '@rstest/core'; -describe('Todo list', () => { - const ctx = createWebTest('http://localhost:5173/'); +const test = base.extend({ url: 'http://localhost:5173/' }); - it('adds a todo', async () => { - const { agent } = ctx; - await agent.aiAct("type 'Study AI' in the input and press Enter"); - await agent.aiAssert('the list contains exactly one item: "Study AI"'); - }); +test('adds a todo', async ({ agent }) => { + await agent.aiAct("type 'Study AI' in the input and press Enter"); + await agent.aiAssert('the list contains exactly one item: "Study AI"'); }); ``` What you get out of the box: -- A fresh `agent` per `it` block, ready to call any [Agent API](./api#interaction-methods) (`aiAct`, `aiAssert`, `aiQuery`, `aiTap`, `aiInput`, `aiHover`, …). -- Automatic browser lifecycle — the browser launches once per test file, and a clean page is set up and torn down around every test. +- Typed fixtures — destructure `agent`, `page`, `browser`, `agentForPage` directly from the test context. No `ctx.` indirection, no "only valid inside `it(...)`" runtime errors. +- Lazy browser lifecycle — if no test in a file destructures anything that depends on `browser`, no browser is launched. Files mixing AI tests and pure unit tests pay the AI cost only for the AI tests. +- Composable — `test.extend({ ... })` to override the URL, options, or any individual fixture (e.g. a `page` that injects routes, or a second `adminAgent` persona). - A merged Midscene HTML report per test file, with the path printed to the console after the file finishes. -- Direct pass-through to the underlying Playwright / Puppeteer option types, plus a `setup` hook for lifecycles the defaults don't cover. Two browser providers ship in the package, behind separate subpath exports: @@ -36,7 +33,7 @@ Two browser providers ship in the package, behind separate subpath exports: :::info Example Project -A runnable example lives at [https://github.com/web-infra-dev/midscene-example/tree/main/rstest-demo](https://github.com/web-infra-dev/midscene-example/tree/main/rstest-demo). +A runnable example lives at [https://github.com/web-infra-dev/midscene/tree/main/packages/rstest/demo](https://github.com/web-infra-dev/midscene/tree/main/packages/rstest/demo). ::: @@ -58,8 +55,8 @@ Create `rstest.config.ts` in the root of your test project: ```ts title="rstest.config.ts" import 'dotenv/config'; // load model env vars from .env -import { defineConfig } from '@rstest/core'; import MidsceneReporter from '@midscene/rstest/reporter'; +import { defineConfig } from '@rstest/core'; export default defineConfig({ include: ['e2e/**/*.test.ts'], @@ -75,56 +72,48 @@ export default defineConfig({ ## Step 3: Write a test ```ts title="e2e/todo-list.test.ts" -import { describe, expect, it } from '@rstest/core'; -import { createWebTest } from '@midscene/rstest/playwright'; - -describe('Todo list', () => { - const ctx = createWebTest('http://localhost:5173/'); +import { test as base } from '@midscene/rstest/playwright'; +import { expect } from '@rstest/core'; - it('adds and completes a todo', async () => { - const { agent } = ctx; +const test = base.extend({ url: 'http://localhost:5173/' }); - await agent.aiAct("type 'Study AI' in the input and press Enter"); - await agent.aiAct('click the checkbox of the first item'); +test('adds and completes a todo', async ({ agent }) => { + await agent.aiAct("type 'Study AI' in the input and press Enter"); + await agent.aiAct('click the checkbox of the first item'); - const list = await agent.aiQuery( - 'string[], the complete task list', - ); - expect(list).toHaveLength(1); + const list = await agent.aiQuery( + 'string[], the complete task list', + ); + expect(list).toHaveLength(1); - await agent.aiAssert('the only item is marked as done'); - }); + await agent.aiAssert('the only item is marked as done'); }); ``` -`createWebTest(url, options)` registers Rstest's `beforeAll` / `beforeEach` / `afterEach` / `afterAll` hooks under the hood. Call it once per `describe` block — every `it` inside it gets its own freshly-navigated page. +The `test` exported from `@midscene/rstest/playwright` is an rstest test pre-wired with midscene fixtures. Destructure what you need from the test context — the fixture engine resolves dependencies and runs setup/teardown around each test. -:::warning Lifecycle of `ctx` -- `ctx.agent` and `ctx.page` are created in `beforeEach` and torn down in `afterEach`. Reading them from `describe` scope, a hook, or after the test finishes throws. -- `ctx.browser` is launched in `beforeAll` and closed in `afterAll`. Read it inside any hook or `it(...)`. -::: +The available fixtures are: -The context exposes four surfaces, in order of how often you'll reach for them: - -- **`ctx.agent`** — the midscene `PlaywrightAgent` (or `PuppeteerAgent`) bound to the primary page. Default tool for AI actions and assertions. -- **`ctx.page`** — raw Playwright / Puppeteer `Page`. Escape hatch for things `agent` can't or shouldn't do: `page.route(...)` network mocking, `page.evaluate(...)`, `page.context().cookies()`, etc. -- **`ctx.browser`** — the file-scoped `Browser`. Use it to spin up extra contexts or pages mid-test (multi-user, cross-session scenarios). -- **`ctx.agentForPage(page, opts?)`** — build a midscene agent for another page (popup, manually-created page). Its report is merged with the primary's; destroy is automatic. +- **`agent`** — the midscene `PlaywrightAgent` (or `PuppeteerAgent`) bound to the primary page. Default tool for AI actions and assertions. +- **`page`** — raw Playwright / Puppeteer `Page`. Escape hatch for things `agent` can't or shouldn't do: `page.route(...)` network mocking, `page.evaluate(...)`, `page.context().cookies()`, etc. +- **`browser`** — the file-scoped `Browser`. Use it to spin up extra contexts or pages mid-test (multi-user, cross-session scenarios). +- **`context`** _(Playwright only)_ — the test-scoped `BrowserContext` the primary `page` is created from. Override via `test.extend` if you need a custom context per test. +- **`agentForPage(page, opts?)`** — build a midscene agent for another page (popup, manually-created page). Its report is merged with the primary's; destroy is automatic. +- **`url`** / **`midsceneOptions`** — configuration fixtures. See [Project-wide defaults](#project-wide-defaults-optional) and [Custom page setup](#custom-page-setup). ```ts -it('mocks the API and verifies the UI', async () => { - const { agent, page } = ctx; - +test('mocks the API and verifies the UI', async ({ agent, page }) => { await page.route('**/api/contacts', (route) => route.fulfill({ json: [{ name: 'Fake Alice' }] }), ); - await agent.aiAssert('the contact list shows "Fake Alice"'); }); -it('drives a popup with a second agent', async () => { - const { agent, page, agentForPage } = ctx; - +test('drives a popup with a second agent', async ({ + agent, + page, + agentForPage, +}) => { await agent.aiTap("the 'View details' button"); const popup = await page.waitForEvent('popup'); @@ -132,9 +121,10 @@ it('drives a popup with a second agent', async () => { await popupAgent.aiAssert('the popup shows order details'); }); -it('runs two isolated user sessions in one test', async () => { - const { browser, agentForPage } = ctx; - +test('runs two isolated user sessions in one test', async ({ + browser, + agentForPage, +}) => { const sessionB = await browser.newContext({ storageState: 'user-b.json' }); const pageB = await sessionB.newPage(); await pageB.goto('http://localhost:5173/'); @@ -147,10 +137,10 @@ it('runs two isolated user sessions in one test', async () => { To use Puppeteer instead, change a single import: ```ts -import { createWebTest } from '@midscene/rstest/puppeteer'; +import { test as base } from '@midscene/rstest/puppeteer'; ``` -The `agent` API is identical. +The `agent` API is identical. The Puppeteer provider exposes the same fixtures except `context` (Puppeteer doesn't use a context-as-class — override the `page` fixture if you need one via `browser.createBrowserContext()`). ## Step 4: Run the tests @@ -171,7 +161,7 @@ Reports are written to `midscene_run/report/`. See [Consume report files](./cons ## Project-wide defaults (optional) -If every `createWebTest` call should share the same options, put them in a setup file and reference it from `setupFiles`: +For options that should apply to every file, call `defineMidsceneDefaults` from a setup file and register it via rstest's `setupFiles`: ```ts title="midscene.setup.ts" import { defineMidsceneDefaults } from '@midscene/rstest/playwright'; @@ -184,12 +174,31 @@ defineMidsceneDefaults({ ```ts title="rstest.config.ts" export default defineConfig({ - // ... + // ...other config setupFiles: ['./midscene.setup.ts'], }); ``` -Options passed to `createWebTest(url, options)` shallow-merge over these defaults at the top level — nested fields like `launchOptions` are *replaced*, not deep-merged. Use the function form of a resolver if you need to compose. The Puppeteer provider exposes the same `defineMidsceneDefaults` from `@midscene/rstest/puppeteer`. +:::warning Must be called from a `setupFiles` entry + +Each test file runs in its own isolated module graph (rstest's default `isolate: true`). `defineMidsceneDefaults` populates a module-scoped store, so it only takes effect when called from a file that rstest loads as part of every test file's setup chain. Calling it inside a single test file only affects that file. + +::: + +`defineMidsceneDefaults` is read by the `midsceneOptions` fixture at test time. To override per file, extend the test: + +```ts +const test = base.extend({ + url: 'http://localhost:5173/', + midsceneOptions: { + headless: false, + viewport: { width: 375, height: 667 }, + agentOptions: { cache: true }, // auto-derived cache id from test name + }, +}); +``` + +Nested resolver fields like `launchOptions` and `contextOptions` are **deep-merged** over the package defaults, and arrays concatenate. Use the function form of a resolver (`(defaults) => ...`) when you need to remove a default value or take full control. The Puppeteer provider exposes the same `defineMidsceneDefaults` from `@midscene/rstest/puppeteer`. ## Configure midscene @@ -205,14 +214,17 @@ Midscene reads configuration from four channels — pick the one that matches th MIDSCENE_RUN_DIR=./midscene_run # where reports + cache live ``` -2. **`agentOptions`** on each `createWebTest` call — for anything you set per agent. Accepts the full `AgentOpt` surface (`modelConfig`, `aiActionContext`, `cache`, `replanningCycleLimit`, `waitAfterAction`, `screenshotShrinkFactor`, `onTaskStartTip`, `onOpenAIClientCreated`, …). `groupName` and `reportFileName` are reserved by the lifecycle. +2. **`midsceneOptions.agentOptions`** — for anything you set per agent. Accepts the full `AgentOpt` surface (`modelConfig`, `aiActionContext`, `cache`, `replanningCycleLimit`, `waitAfterAction`, `screenshotShrinkFactor`, `onTaskStartTip`, `onOpenAIClientCreated`, …). `groupName` and `reportFileName` are reserved by the lifecycle. ```ts - const ctx = createWebTest(url, { - agentOptions: { - aiActionContext: 'You are testing a multilingual checkout flow.', - cache: { strategy: 'read-write' }, - waitAfterAction: 500, + const test = base.extend({ + url: 'http://localhost:5173/', + midsceneOptions: { + agentOptions: { + aiActionContext: 'You are testing a multilingual checkout flow.', + cache: { strategy: 'read-write' }, + waitAfterAction: 500, + }, }, }); ``` @@ -235,114 +247,150 @@ Midscene reads configuration from four channels — pick the one that matches th ## Options +`midsceneOptions` carries every per-file knob. Set it via `defineMidsceneDefaults` for repo-wide defaults, or via `test.extend({ midsceneOptions: {...} })` for per-file overrides. + ### Playwright (`@midscene/rstest/playwright`) -| Option | Type | Default | Description | +| Field | Type | Default | Description | | --- | --- | --- | --- | | `headless` | `boolean` | `true` in CI, `false` locally | Convenience for `launchOptions.headless`. | | `viewport` | `{ width, height }` | `1920×1080` | Convenience for `contextOptions.viewport`. | | `launchOptions` | `LaunchOptions \| (defaults) => LaunchOptions` | — | Forwarded to `chromium.launch(...)`. | | `contextOptions` | `BrowserContextOptions \| (defaults) => BrowserContextOptions` | — | Forwarded to `browser.newContext(...)`. | | `gotoOptions` | `Parameters[1]` | — | Forwarded to `page.goto(url, ...)`. | -| `agentOptions` | `WebPageAgentOpt` | — | Forwarded to `PlaywrightAgent` (e.g. `aiActionContext`, `modelConfig`). `groupName` and `reportFileName` are managed by the lifecycle. | -| `setup` | `(api) => Promise<{ page; teardown? }>` | — | See [Custom page setup](#custom-page-setup) below. | +| `agentOptions` | `WebPageAgentOpt` | — | Forwarded to `PlaywrightAgent` (e.g. `aiActionContext`, `modelConfig`). `groupName` and `reportFileName` are managed by the lifecycle. `cache` accepts `true \| false \| { strategy?, id? }` — when `cache: true` or `id` is omitted, a stable id is auto-derived from the test file name and test name so re-runs hit the same cache namespace. | ### Puppeteer (`@midscene/rstest/puppeteer`) -| Option | Type | Default | Description | +| Field | Type | Default | Description | | --- | --- | --- | --- | | `headless` | `boolean` | `true` in CI, `false` locally | Convenience for `launchOptions.headless`. | | `viewport` | `{ width, height }` | `1920×1080` | Convenience for `launchOptions.defaultViewport`. | | `launchOptions` | `LaunchOptions \| (defaults) => LaunchOptions` | — | Forwarded to `puppeteer.launch(...)`. | | `gotoOptions` | `GoToOptions` | — | Forwarded to `page.goto(url, ...)`. | -| `agentOptions` | `WebPageAgentOpt` | — | Forwarded to `PuppeteerAgent`. | -| `setup` | `(api) => Promise<{ page; teardown? }>` | — | See [Custom page setup](#custom-page-setup) below. | +| `agentOptions` | `WebPageAgentOpt` | — | Forwarded to `PuppeteerAgent`. Same `cache` shorthand as the Playwright provider — `cache: true` auto-derives the id. | -Resolver fields (`launchOptions`, `contextOptions`) accept either an object — shallow-merged over midscene's defaults — or a function `(defaults) => options` that takes full control. +Resolver fields (`launchOptions`, `contextOptions`) accept either an object — deep-merged over midscene's defaults, with arrays concatenated — or a function `(defaults) => options` (sync or async) that takes full control. An array of either is also accepted and applied left-to-right. ### Examples -Override one launch field while keeping midscene's defaults (`--no-sandbox`, etc.): +Add a launch arg without losing midscene's CI-critical defaults: ```ts -const ctx = createWebTest(url, { - launchOptions: { proxy: { server: 'http://corp-proxy:8080' } }, +const test = base.extend({ + url: 'http://localhost:5173/', + midsceneOptions: { + // Final `args` is ['--no-sandbox', '--ignore-certificate-errors', '--start-fullscreen']. + launchOptions: { args: ['--start-fullscreen'] }, + }, }); ``` -Compose with the function form: +Tweak a single nested field without erasing siblings: ```ts -const ctx = createWebTest(url, { - launchOptions: (defaults) => ({ - ...defaults, - args: [...(defaults.args ?? []), '--disable-gpu'], - }), +const test = base.extend({ + url: 'http://localhost:5173/', + midsceneOptions: { + // Final viewport is { width: 1440, height: 1080 } — `height` is kept. + contextOptions: { viewport: { width: 1440 } }, + }, +}); +``` + +Use the function form when deep-merge isn't enough — typically when you need to **remove** something from defaults, run async logic, or fully replace the options: + +```ts +const test = base.extend({ + url: 'http://localhost:5173/', + midsceneOptions: { + launchOptions: (defaults) => ({ + ...defaults, + args: defaults.args?.filter((a) => a !== '--no-sandbox'), + }), + }, }); ``` Configure the context (Playwright): ```ts -const ctx = createWebTest(url, { - contextOptions: { - locale: 'zh-CN', - storageState: 'auth.json', - recordVideo: { dir: 'midscene_run/video' }, +const test = base.extend({ + url: 'http://localhost:5173/', + midsceneOptions: { + contextOptions: { + locale: 'zh-CN', + storageState: 'auth.json', + recordVideo: { dir: 'midscene_run/video' }, + }, }, }); ``` -### Custom page setup +## Custom page setup -For lifecycles the defaults don't cover — persistent context, CDP connect, custom trace orchestration, reusing existing fixtures — pass `setup`. It receives the shared browser launched in `beforeAll` and must return the page midscene should attach the agent to. When `setup` is provided, `headless`, `viewport`, `launchOptions`, `contextOptions`, and `gotoOptions` are all ignored; only `agentOptions` still applies. +For lifecycles the defaults don't cover — persistent context, CDP connect, custom trace orchestration, per-test storage state — override the `page` fixture. Code before `await use(page)` is setup; code after is teardown. ```ts -import { createWebTest } from '@midscene/rstest/playwright'; +import { test as base } from '@midscene/rstest/playwright'; import { mkdirSync } from 'node:fs'; import { join } from 'node:path'; -const ctx = createWebTest('https://example.com', { - async setup({ browser, url }) { - const context = await browser.newContext({ - viewport: { width: 1280, height: 720 }, - }); +const test = base.extend({ + url: 'https://example.com', + page: async ({ context, url, task }, use) => { await context.tracing.start({ screenshots: true, snapshots: true }); const page = await context.newPage(); await page.goto(url); - return { - page, - async teardown(testCtx) { - const passed = testCtx.task.result?.status === 'pass'; - if (passed) { - await context.tracing.stop(); - } else { - mkdirSync('midscene_run/trace', { recursive: true }); - await context.tracing.stop({ - path: join('midscene_run/trace', `${testCtx.task.name}.zip`), - }); - } - await context.close(); - }, - }; + await use(page); + + const passed = task.result?.status === 'pass'; + if (passed) { + await context.tracing.stop(); + } else { + mkdirSync('midscene_run/trace', { recursive: true }); + await context.tracing.stop({ + path: join('midscene_run/trace', `${task.name}.zip`), + }); + } }, }); ``` -The `teardown` callback runs **before** `agent.destroy()`, so the page is still alive — exactly the right window to stop traces, save videos, dump cookies, etc. If you don't need it, return `{ page }`. +Teardown of the `page` fixture runs **after** `agent.destroy()` — the page is still alive at that point, exactly the window you want for stopping traces, saving videos, or dumping cookies. The shared `browser` and the default-built `context` are still open; only your custom teardown decides when to close anything you opened yourself. -## Advanced: custom providers +For deeper customization (e.g. a persistent context from scratch), override `context` instead — it depends only on `browser` and `midsceneOptions`, and the default `page` will pick up whatever context you produce. -Need to drive a different browser stack — a remote browser, an Electron app, or your own grid? The lifecycle primitive both providers are built on is exported: +## Personas and composed fixtures + +`test.extend` composes — you can layer fixtures the same way you'd layer options: ```ts -import { registerLifecycle } from '@midscene/rstest'; +const test = base.extend<{ + adminAgent: PlaywrightAgent; +}>({ + url: 'http://localhost:5173/', + adminAgent: async ({ browser }, use) => { + const context = await browser.newContext({ storageState: 'admin.json' }); + const page = await context.newPage(); + await page.goto('http://localhost:5173/'); + const agent = new PlaywrightAgent(page); + await use(agent); + await agent.destroy(); + await context.close(); + }, +}); + +test('only admin can delete', async ({ agent, adminAgent }) => { + await agent.aiAssert('no Delete button is visible'); + await adminAgent.aiAssert('a Delete button is visible'); +}); ``` -It accepts a `LifecycleProvider` describing how to launch a browser, build an Agent, and tear it down. The Playwright and Puppeteer providers are thin wrappers on top of it. +This is the recommended pattern for multi-persona tests, mock-server fixtures, database seeding, and anything else you'd otherwise reach for `beforeEach` to do — fixtures compose by dependency graph and tear down in reverse, so ordering is explicit and correct. ## More - All Agent methods: [API Reference](./api#interaction-methods). -- Working example: [https://github.com/web-infra-dev/midscene-example/tree/main/rstest-demo](https://github.com/web-infra-dev/midscene-example/tree/main/rstest-demo). +- Working example: [https://github.com/web-infra-dev/midscene/tree/main/packages/rstest/demo](https://github.com/web-infra-dev/midscene/tree/main/packages/rstest/demo). diff --git a/apps/site/docs/zh/integrate-with-rstest.mdx b/apps/site/docs/zh/integrate-with-rstest.mdx index d5cc795e85..6c85f76d75 100644 --- a/apps/site/docs/zh/integrate-with-rstest.mdx +++ b/apps/site/docs/zh/integrate-with-rstest.mdx @@ -3,29 +3,26 @@ import { PackageManagerTabs } from '@theme'; # 集成到 Rstest -[Rstest](https://github.com/web-infra-dev/rstest) 是基于 Rspack 的高性能测试框架。`@midscene/rstest` 让你以熟悉的 `describe / it` 风格编写 AI 驱动的端到端测试 —— 浏览器启动、Agent 创建、HTML 报告生成都由 Midscene 自动处理。 +[Rstest](https://github.com/web-infra-dev/rstest) 是基于 Rspack 的高性能测试框架。`@midscene/rstest` 把 midscene 暴露成原生 rstest fixtures —— 浏览器启动、Agent 创建、HTML 报告生成都由 Midscene 自动处理。 ```ts -import { describe, expect, it } from '@rstest/core'; -import { createWebTest } from '@midscene/rstest/playwright'; +import { test as base } from '@midscene/rstest/playwright'; +import { expect } from '@rstest/core'; -describe('Todo list', () => { - const ctx = createWebTest('http://localhost:5173/'); +const test = base.extend({ url: 'http://localhost:5173/' }); - it('adds a todo', async () => { - const { agent } = ctx; - await agent.aiAct("type 'Study AI' in the input and press Enter"); - await agent.aiAssert('the list contains exactly one item: "Study AI"'); - }); +test('adds a todo', async ({ agent }) => { + await agent.aiAct("type 'Study AI' in the input and press Enter"); + await agent.aiAssert('the list contains exactly one item: "Study AI"'); }); ``` 开箱即用的能力: -- 每个 `it` 块都拿到一份全新的 `agent`,可以直接调用 [Agent API](./api#interaction-methods) 中的任意方法(`aiAct`、`aiAssert`、`aiQuery`、`aiTap`、`aiInput`、`aiHover` 等)。 -- 自动管理浏览器生命周期 —— 每个测试文件启动一次浏览器,每个 test 之前打开干净的页面,结束后自动销毁。 -- 每个测试文件结束时输出一份合并后的 Midscene HTML 报告,路径会打印到控制台。 -- 直接 pass-through 到 Playwright / Puppeteer 自身的 options 类型;默认 lifecycle 无法满足时,可用 `setup` 钩子自定义。 +- **类型化 fixtures** —— `agent`、`page`、`browser`、`agentForPage` 直接从 test context 解构,不需要 `ctx.` 间接对象,也没有 "只能在 `it(...)` 里访问" 的运行时错误。 +- **Lazy 浏览器生命周期** —— 一个文件里如果没有 test 解构 `browser`(或它的下游 fixture),浏览器根本不会被启动。AI 测试和纯 unit test 同文件混写时,AI 成本只针对真正用到 agent 的 test。 +- **可组合** —— 用 `test.extend({ ... })` 覆盖 URL、options,或任何单独的 fixture(比如注入 route 的 `page`,或者第二个 `adminAgent` persona)。 +- **每个文件一份合并后的 HTML 报告**,路径在文件结束时打印到控制台。 包内自带 Playwright 与 Puppeteer 两套 provider,通过 subpath export 区分: @@ -36,7 +33,7 @@ describe('Todo list', () => { :::info 样例项目 -可运行的样例:[https://github.com/web-infra-dev/midscene-example/tree/main/rstest-demo](https://github.com/web-infra-dev/midscene-example/tree/main/rstest-demo)。 +可运行的样例:[https://github.com/web-infra-dev/midscene/tree/main/packages/rstest/demo](https://github.com/web-infra-dev/midscene/tree/main/packages/rstest/demo)。 ::: @@ -58,8 +55,8 @@ describe('Todo list', () => { ```ts title="rstest.config.ts" import 'dotenv/config'; // 从 .env 加载模型相关环境变量 -import { defineConfig } from '@rstest/core'; import MidsceneReporter from '@midscene/rstest/reporter'; +import { defineConfig } from '@rstest/core'; export default defineConfig({ include: ['e2e/**/*.test.ts'], @@ -75,56 +72,48 @@ export default defineConfig({ ## 第三步:编写测试 ```ts title="e2e/todo-list.test.ts" -import { describe, expect, it } from '@rstest/core'; -import { createWebTest } from '@midscene/rstest/playwright'; - -describe('Todo list', () => { - const ctx = createWebTest('http://localhost:5173/'); +import { test as base } from '@midscene/rstest/playwright'; +import { expect } from '@rstest/core'; - it('adds and completes a todo', async () => { - const { agent } = ctx; +const test = base.extend({ url: 'http://localhost:5173/' }); - await agent.aiAct("type 'Study AI' in the input and press Enter"); - await agent.aiAct('click the checkbox of the first item'); +test('adds and completes a todo', async ({ agent }) => { + await agent.aiAct("type 'Study AI' in the input and press Enter"); + await agent.aiAct('click the checkbox of the first item'); - const list = await agent.aiQuery( - 'string[], the complete task list', - ); - expect(list).toHaveLength(1); + const list = await agent.aiQuery( + 'string[], the complete task list', + ); + expect(list).toHaveLength(1); - await agent.aiAssert('the only item is marked as done'); - }); + await agent.aiAssert('the only item is marked as done'); }); ``` -`createWebTest(url, options)` 会在内部注册 Rstest 的 `beforeAll` / `beforeEach` / `afterEach` / `afterAll` 钩子。在每个 `describe` 块里调用一次即可,里面的每个 `it` 都会拿到一份重新导航过的全新页面。 +`@midscene/rstest/playwright` 导出的 `test` 是预先挂好了 midscene fixtures 的 rstest test。直接从 test context 解构需要的 fixture,fixture 引擎会按依赖关系拓扑展开 setup 与 teardown。 -:::warning `ctx` 的生命周期 -- `ctx.agent` 与 `ctx.page` 在 `beforeEach` 中创建、`afterEach` 中销毁。在 `describe` 作用域、其他钩子、或 test 结束之后访问会抛错。 -- `ctx.browser` 在 `beforeAll` 启动、`afterAll` 关闭。任意 hook 或 `it(...)` 内都可读。 -::: +可用的 fixtures: -`ctx` 暴露四个 surface,按使用频率排序: - -- **`ctx.agent`** —— 绑在主 page 上的 midscene `PlaywrightAgent`(或 `PuppeteerAgent`)。AI action 和 assertion 的默认入口。 -- **`ctx.page`** —— 底层的 Playwright / Puppeteer `Page`。`agent` 做不到或不该做的事的 escape hatch:`page.route(...)` network mock、`page.evaluate(...)`、`page.context().cookies()` 等。 -- **`ctx.browser`** —— file-scoped 的 `Browser`。在测试中开新 context / 新 page(多用户、跨会话场景)用。 -- **`ctx.agentForPage(page, opts?)`** —— 为另一个 page(popup、手动开的 page)构造一个 midscene agent,报告会和主 agent 合并,销毁自动完成。 +- **`agent`** —— 绑定到主 page 上的 midscene `PlaywrightAgent`(或 `PuppeteerAgent`)。AI action 和 assertion 的默认入口。 +- **`page`** —— 底层的 Playwright / Puppeteer `Page`。`agent` 做不到或不该做的事的 escape hatch:`page.route(...)` network mock、`page.evaluate(...)`、`page.context().cookies()` 等。 +- **`browser`** —— file-scoped 的 `Browser`。在测试中开新 context / 新 page(多用户、跨会话场景)用。 +- **`context`** _(仅 Playwright)_ —— 创建主 `page` 的 test-scoped `BrowserContext`。需要自定义 context 时通过 `test.extend` 覆盖。 +- **`agentForPage(page, opts?)`** —— 为另一个 page(popup、手动开的 page)构造一个 midscene agent。报告会和主 agent 合并,销毁自动完成。 +- **`url`** / **`midsceneOptions`** —— 配置 fixtures。详见 [项目级默认配置](#项目级默认配置可选) 与 [自定义 page 设置](#自定义-page-设置)。 ```ts -it('mocks the API and verifies the UI', async () => { - const { agent, page } = ctx; - +test('mocks the API and verifies the UI', async ({ agent, page }) => { await page.route('**/api/contacts', (route) => route.fulfill({ json: [{ name: 'Fake Alice' }] }), ); - await agent.aiAssert('the contact list shows "Fake Alice"'); }); -it('drives a popup with a second agent', async () => { - const { agent, page, agentForPage } = ctx; - +test('drives a popup with a second agent', async ({ + agent, + page, + agentForPage, +}) => { await agent.aiTap("the 'View details' button"); const popup = await page.waitForEvent('popup'); @@ -132,9 +121,10 @@ it('drives a popup with a second agent', async () => { await popupAgent.aiAssert('the popup shows order details'); }); -it('runs two isolated user sessions in one test', async () => { - const { browser, agentForPage } = ctx; - +test('runs two isolated user sessions in one test', async ({ + browser, + agentForPage, +}) => { const sessionB = await browser.newContext({ storageState: 'user-b.json' }); const pageB = await sessionB.newPage(); await pageB.goto('http://localhost:5173/'); @@ -147,10 +137,10 @@ it('runs two isolated user sessions in one test', async () => { 如果想换成 Puppeteer,只需改一行 import: ```ts -import { createWebTest } from '@midscene/rstest/puppeteer'; +import { test as base } from '@midscene/rstest/puppeteer'; ``` -`agent` 的 API 完全一致。 +`agent` 的 API 完全一致。Puppeteer provider 暴露的 fixtures 与 Playwright 一致,但没有 `context`(puppeteer 不使用 context-as-class —— 需要的话覆盖 `page` fixture 并调用 `browser.createBrowserContext()`)。 ## 第四步:运行测试 @@ -171,7 +161,7 @@ Midscene report: midscene_run/report/.html ## 项目级默认配置(可选) -如果希望所有 `createWebTest` 调用共享同一份配置,把它们写到一个 setup 文件里,并通过 `setupFiles` 注册: +如果希望所有文件共享同一份配置,把 `defineMidsceneDefaults` 写到一个 setup 文件里,通过 rstest 的 `setupFiles` 注册: ```ts title="midscene.setup.ts" import { defineMidsceneDefaults } from '@midscene/rstest/playwright'; @@ -184,18 +174,37 @@ defineMidsceneDefaults({ ```ts title="rstest.config.ts" export default defineConfig({ - // ... + // ...其他配置 setupFiles: ['./midscene.setup.ts'], }); ``` -调用 `createWebTest(url, options)` 时传入的 options 会按顶层 key 浅合并到这里的默认值上 —— `launchOptions` 这种嵌套字段是**整体替换**,不是深度合并。需要组合可以用 resolver 的函数形式。Puppeteer provider 在 `@midscene/rstest/puppeteer` 提供同名 `defineMidsceneDefaults`。 +:::warning 必须从 `setupFiles` 里调用 + +每个 test file 在 rstest 默认的 `isolate: true` 模式下都有自己独立的 module graph,`defineMidsceneDefaults` 写入的是一个 module-scoped store——所以必须从被 rstest 当作每个 test file 启动链路一部分的 setup file 中调用。在某个单一 test file 内部调用只会影响该文件本身。 + +::: + +`defineMidsceneDefaults` 写入的值会在测试运行时由 `midsceneOptions` fixture 读取。需要在某个文件里覆盖时,扩展 test: + +```ts +const test = base.extend({ + url: 'http://localhost:5173/', + midsceneOptions: { + headless: false, + viewport: { width: 375, height: 667 }, + agentOptions: { cache: true }, // 自动从 test 名派生 cache id + }, +}); +``` + +`launchOptions`、`contextOptions` 这类 resolver 字段会在 midscene 默认值之上做**深度合并**,数组自动拼接。需要从默认值里去掉某个字段、或完全接管,用 resolver 的函数形式 `(defaults) => ...`。Puppeteer provider 在 `@midscene/rstest/puppeteer` 提供同名 `defineMidsceneDefaults`。 ## 配置 midscene midscene 从四个渠道读取配置,按你想影响的作用域选一个。 -1. **环境变量**(模型端点、API key、vision-family 开关、cache 目录、report 目录、debug 开关):用你平时设环境变量的方式即可(`.env`、CI secret、shell export)。midscene runtime 会自动读取,不用额外接线。完整列表见 [model 配置参考](./model-and-provider)。示例: +1. **环境变量**(模型端点、API Key、vision-family 开关、cache 目录、report 目录、debug 开关):用你平时设环境变量的方式即可(`.env`、CI secret、shell export)。midscene runtime 会自动读取,不用额外接线。完整列表见 [model 配置参考](./model-and-provider)。示例: ```bash MIDSCENE_MODEL_BASE_URL=https://api.openai.com/v1 @@ -205,14 +214,17 @@ midscene 从四个渠道读取配置,按你想影响的作用域选一个。 MIDSCENE_RUN_DIR=./midscene_run # 报告 + 缓存的根目录 ``` -2. **`agentOptions`** —— 每次 `createWebTest` 都可以传,针对 per-agent 配置。接收完整 `AgentOpt`(`modelConfig`、`aiActionContext`、`cache`、`replanningCycleLimit`、`waitAfterAction`、`screenshotShrinkFactor`、`onTaskStartTip`、`onOpenAIClientCreated` 等)。`groupName` 和 `reportFileName` 由 lifecycle 保留。 +2. **`midsceneOptions.agentOptions`** —— 每个文件可独立配置,针对 per-agent 配置。接收完整 `AgentOpt`(`modelConfig`、`aiActionContext`、`cache`、`replanningCycleLimit`、`waitAfterAction`、`screenshotShrinkFactor`、`onTaskStartTip`、`onOpenAIClientCreated` 等)。`groupName` 和 `reportFileName` 由 lifecycle 保留。 ```ts - const ctx = createWebTest(url, { - agentOptions: { - aiActionContext: 'You are testing a multilingual checkout flow.', - cache: { strategy: 'read-write' }, - waitAfterAction: 500, + const test = base.extend({ + url: 'http://localhost:5173/', + midsceneOptions: { + agentOptions: { + aiActionContext: 'You are testing a multilingual checkout flow.', + cache: { strategy: 'read-write' }, + waitAfterAction: 500, + }, }, }); ``` @@ -235,114 +247,150 @@ midscene 从四个渠道读取配置,按你想影响的作用域选一个。 ## 选项 +`midsceneOptions` 集中承载所有 per-file 配置。可以通过 `defineMidsceneDefaults` 设仓库级默认,或通过 `test.extend({ midsceneOptions: {...} })` 在单个文件覆盖。 + ### Playwright (`@midscene/rstest/playwright`) -| 选项 | 类型 | 默认值 | 说明 | +| 字段 | 类型 | 默认值 | 说明 | | --- | --- | --- | --- | | `headless` | `boolean` | CI 中 `true`,本地 `false` | `launchOptions.headless` 的便捷写法。 | | `viewport` | `{ width, height }` | `1920×1080` | `contextOptions.viewport` 的便捷写法。 | | `launchOptions` | `LaunchOptions \| (defaults) => LaunchOptions` | — | 透传到 `chromium.launch(...)`。 | | `contextOptions` | `BrowserContextOptions \| (defaults) => BrowserContextOptions` | — | 透传到 `browser.newContext(...)`。 | | `gotoOptions` | `Parameters[1]` | — | 透传到 `page.goto(url, ...)`。 | -| `agentOptions` | `WebPageAgentOpt` | — | 透传给 `PlaywrightAgent`(如 `aiActionContext`、`modelConfig`)。`groupName` 与 `reportFileName` 由 lifecycle 管理。 | -| `setup` | `(api) => Promise<{ page; teardown? }>` | — | 详见下方 [自定义 page 设置](#自定义-page-设置)。 | +| `agentOptions` | `WebPageAgentOpt` | — | 透传给 `PlaywrightAgent`(如 `aiActionContext`、`modelConfig`)。`groupName` 与 `reportFileName` 由 lifecycle 管理。`cache` 接受 `true \| false \| { strategy?, id? }`——当 `cache: true` 或 `id` 缺省时,会自动从测试文件名 + test 名派生一个稳定 id,让重复跑同一个测试复用同一个 cache namespace。 | ### Puppeteer (`@midscene/rstest/puppeteer`) -| 选项 | 类型 | 默认值 | 说明 | +| 字段 | 类型 | 默认值 | 说明 | | --- | --- | --- | --- | | `headless` | `boolean` | CI 中 `true`,本地 `false` | `launchOptions.headless` 的便捷写法。 | | `viewport` | `{ width, height }` | `1920×1080` | `launchOptions.defaultViewport` 的便捷写法。 | | `launchOptions` | `LaunchOptions \| (defaults) => LaunchOptions` | — | 透传到 `puppeteer.launch(...)`。 | | `gotoOptions` | `GoToOptions` | — | 透传到 `page.goto(url, ...)`。 | -| `agentOptions` | `WebPageAgentOpt` | — | 透传给 `PuppeteerAgent`。 | -| `setup` | `(api) => Promise<{ page; teardown? }>` | — | 详见下方 [自定义 page 设置](#自定义-page-设置)。 | +| `agentOptions` | `WebPageAgentOpt` | — | 透传给 `PuppeteerAgent`。`cache` 简写同 Playwright provider——`cache: true` 自动派生 id。 | -`launchOptions`、`contextOptions` 这类 resolver 字段同时接受两种形式:传对象会和 midscene 的默认值浅合并;传函数 `(defaults) => options` 则完全接管。 +`launchOptions`、`contextOptions` 这类 resolver 字段同时接受对象、函数 `(defaults) => options`(同步或异步)、以及它们组成的数组——对象形态会和 midscene 默认值做**深度合并**、数组自动拼接,函数形态则可以完全接管。 ### 示例 -只覆盖一个 launch 字段,保留 midscene 的其它默认(`--no-sandbox` 等): +加一个 launch flag,同时保留 midscene 在 CI 上必需的默认 args: ```ts -const ctx = createWebTest(url, { - launchOptions: { proxy: { server: 'http://corp-proxy:8080' } }, +const test = base.extend({ + url: 'http://localhost:5173/', + midsceneOptions: { + // 最终 `args` 为 ['--no-sandbox', '--ignore-certificate-errors', '--start-fullscreen']。 + launchOptions: { args: ['--start-fullscreen'] }, + }, }); ``` -用函数形式做组合: +只改动一个嵌套字段,兄弟字段不丢: ```ts -const ctx = createWebTest(url, { - launchOptions: (defaults) => ({ - ...defaults, - args: [...(defaults.args ?? []), '--disable-gpu'], - }), +const test = base.extend({ + url: 'http://localhost:5173/', + midsceneOptions: { + // 最终 viewport 为 { width: 1440, height: 1080 }——height 仍然保留。 + contextOptions: { viewport: { width: 1440 } }, + }, +}); +``` + +深度合并解决不了的场景用函数形式——通常是想从默认值里**去掉**某些字段、写异步逻辑、或者完全替换: + +```ts +const test = base.extend({ + url: 'http://localhost:5173/', + midsceneOptions: { + launchOptions: (defaults) => ({ + ...defaults, + args: defaults.args?.filter((a) => a !== '--no-sandbox'), + }), + }, }); ``` 配置 context(Playwright): ```ts -const ctx = createWebTest(url, { - contextOptions: { - locale: 'zh-CN', - storageState: 'auth.json', - recordVideo: { dir: 'midscene_run/video' }, +const test = base.extend({ + url: 'http://localhost:5173/', + midsceneOptions: { + contextOptions: { + locale: 'zh-CN', + storageState: 'auth.json', + recordVideo: { dir: 'midscene_run/video' }, + }, }, }); ``` -### 自定义 page 设置 +## 自定义 page 设置 -默认 lifecycle 无法满足的场景 —— persistent context、CDP connect、自定义 trace 编排、复用现有 fixture —— 用 `setup`。它接收 `beforeAll` 启动好的共享浏览器,返回 midscene 要附着 agent 的 page。传了 `setup` 之后,`headless`、`viewport`、`launchOptions`、`contextOptions`、`gotoOptions` 全部被忽略,只有 `agentOptions` 仍然生效。 +默认 lifecycle 无法满足的场景 —— persistent context、CDP connect、自定义 trace 编排、per-test storage state —— 直接覆盖 `page` fixture。`await use(page)` 之前是 setup,之后是 teardown。 ```ts -import { createWebTest } from '@midscene/rstest/playwright'; +import { test as base } from '@midscene/rstest/playwright'; import { mkdirSync } from 'node:fs'; import { join } from 'node:path'; -const ctx = createWebTest('https://example.com', { - async setup({ browser, url }) { - const context = await browser.newContext({ - viewport: { width: 1280, height: 720 }, - }); +const test = base.extend({ + url: 'https://example.com', + page: async ({ context, url, task }, use) => { await context.tracing.start({ screenshots: true, snapshots: true }); const page = await context.newPage(); await page.goto(url); - return { - page, - async teardown(testCtx) { - const passed = testCtx.task.result?.status === 'pass'; - if (passed) { - await context.tracing.stop(); - } else { - mkdirSync('midscene_run/trace', { recursive: true }); - await context.tracing.stop({ - path: join('midscene_run/trace', `${testCtx.task.name}.zip`), - }); - } - await context.close(); - }, - }; + await use(page); + + const passed = task.result?.status === 'pass'; + if (passed) { + await context.tracing.stop(); + } else { + mkdirSync('midscene_run/trace', { recursive: true }); + await context.tracing.stop({ + path: join('midscene_run/trace', `${task.name}.zip`), + }); + } }, }); ``` -`teardown` 在 `agent.destroy()` **之前**触发,page 还是活的 —— 这正是停 trace、保存录屏、dump cookies 等操作的窗口期。不需要的话直接返回 `{ page }`。 +`page` fixture 的 teardown 在 `agent.destroy()` **之后**触发,此时 page 仍然存活 —— 这正是停 trace、保存录屏、dump cookies 等操作的窗口期。共享的 `browser` 和默认构造的 `context` 也都还活着;只有你自己开的资源由你自己的 teardown 决定何时关闭。 -## 进阶:自定义 provider +如果需要更深层定制(比如完全自建一个 persistent context),覆盖 `context` fixture —— 它只依赖 `browser` 与 `midsceneOptions`,默认的 `page` 会自动复用你产出的 context。 -如果需要驱动其他浏览器栈(远程浏览器、Electron 应用、自建 grid 等),可以使用底层原语: +## 多 Persona 与组合 fixtures + +`test.extend` 是可组合的 —— 像叠 options 一样叠 fixtures: ```ts -import { registerLifecycle } from '@midscene/rstest'; +const test = base.extend<{ + adminAgent: PlaywrightAgent; +}>({ + url: 'http://localhost:5173/', + adminAgent: async ({ browser }, use) => { + const context = await browser.newContext({ storageState: 'admin.json' }); + const page = await context.newPage(); + await page.goto('http://localhost:5173/'); + const agent = new PlaywrightAgent(page); + await use(agent); + await agent.destroy(); + await context.close(); + }, +}); + +test('only admin can delete', async ({ agent, adminAgent }) => { + await agent.aiAssert('no Delete button is visible'); + await adminAgent.aiAssert('a Delete button is visible'); +}); ``` -它接受一个 `LifecycleProvider`,描述如何启动浏览器、构造 Agent、销毁资源。包内的 Playwright / Puppeteer provider 都是它的薄封装。 +这是多 persona 测试、mock server fixture、数据库 seed 以及其它你过去写在 `beforeEach` 里的东西的推荐写法 —— fixtures 按依赖图组合,按反向顺序 teardown,顺序显式且确定。 ## 更多 - 所有 Agent 方法:[API Reference](./api#interaction-methods)。 -- 样例项目:[https://github.com/web-infra-dev/midscene-example/tree/main/rstest-demo](https://github.com/web-infra-dev/midscene-example/tree/main/rstest-demo)。 +- 样例项目:[https://github.com/web-infra-dev/midscene/tree/main/packages/rstest/demo](https://github.com/web-infra-dev/midscene/tree/main/packages/rstest/demo)。 diff --git a/packages/rstest/demo/playground.test.ts b/packages/rstest/demo/playground.test.ts index 12f83e5604..23d8dc147c 100644 --- a/packages/rstest/demo/playground.test.ts +++ b/packages/rstest/demo/playground.test.ts @@ -1,15 +1,15 @@ -import { createWebTest } from '@midscene/rstest/playwright'; -import { describe, expect, it } from '@rstest/core'; +import { test as base } from '@midscene/rstest/playwright'; +import { describe, expect } from '@rstest/core'; const PAGE_URL = 'https://lf3-static.bytednsdoc.com/obj/eden-cn/nupipfups/Midscene/contacts3.html'; -describe('Contacts page', () => { - const ctx = createWebTest(PAGE_URL); +// Per-file URL override. The `page` fixture navigates here for every test. +const test = base.extend({ url: PAGE_URL }); +describe('Contacts page', () => { // Pattern: semantic UI check via `aiAssert`. - it('renders the smart contacts header and grid', async () => { - const { agent } = ctx; + test('renders the smart contacts header and grid', async ({ agent }) => { await agent.aiAssert( 'the page header reads "Smart Contacts" with a grid of contact cards below it, each card showing an avatar, name, position, and contact details', ); @@ -17,8 +17,7 @@ describe('Contacts page', () => { // Pattern: structured data extraction via `aiQuery` + deterministic // comparison via rstest `expect`. - it('lists every contact with the expected fields', async () => { - const { agent } = ctx; + test('lists every contact with the expected fields', async ({ agent }) => { const contacts = await agent.aiQuery< { name: string; position: string; email: string }[] >( @@ -36,8 +35,7 @@ describe('Contacts page', () => { // Pattern: AI interaction (`aiRightClick`) + AI synchronization (`aiWaitFor`) // + AI extraction (`aiQuery`) chained together. - it('opens the custom context menu on right-click', async () => { - const { agent } = ctx; + test('opens the custom context menu on right-click', async ({ agent }) => { await agent.aiRightClick("Alice Johnson's contact card"); await agent.aiWaitFor( 'a context menu is visible with the items "Call Contact", "Send Email", "Send Message", "Edit Contact", "Copy Info" and "Delete Contact"', @@ -58,10 +56,11 @@ describe('Contacts page', () => { }); // Escape hatch: raw Playwright `Page` for browser-primitive checks that - // don't need the AI. - it('inspects raw page state via the Playwright page escape hatch', async () => { - const { page } = ctx; - + // don't need the AI. Destructure only `page` — the `agent` fixture is not + // created, but `page` still pulls in `browser`/`context`/auto-navigation. + test('inspects raw page state via the Playwright page escape hatch', async ({ + page, + }) => { expect(page.url()).toBe(PAGE_URL); const viewport = page.viewportSize(); @@ -74,11 +73,12 @@ describe('Contacts page', () => { // Multi-session: open a second isolated browser context (think "another // user") and drive it with a separate midscene agent via `agentForPage`. - // The secondary's report is merged alongside the primary's at afterEach; - // destroy is automatic. - it('drives a second isolated session via browser + agentForPage', async () => { - const { browser, agentForPage } = ctx; - + // The secondary's report is merged alongside the primary's at fixture + // teardown; destroy is automatic. + test('drives a second isolated session via browser + agentForPage', async ({ + browser, + agentForPage, + }) => { const sessionB = await browser.newContext(); const pageB = await sessionB.newPage(); await pageB.goto(PAGE_URL); diff --git a/packages/rstest/package.json b/packages/rstest/package.json index 17d6778835..dcd1fbe180 100644 --- a/packages/rstest/package.json +++ b/packages/rstest/package.json @@ -44,8 +44,7 @@ "typecheck": "tsc --noEmit" }, "dependencies": { - "@midscene/shared": "workspace:*", - "std-env": "^4.1.0" + "@midscene/shared": "workspace:*" }, "peerDependencies": { "@midscene/core": "workspace:*", @@ -73,9 +72,12 @@ "@rslib/core": "^0.18.3", "@rstest/core": "^0.9.9", "@types/node": "^18.0.0", + "deepmerge": "^4.3.1", "dotenv": "^16.4.5", "playwright": "^1.45.0", "puppeteer": "24.6.0", + "reduce-configs": "^1.1.2", + "std-env": "^4.1.0", "typescript": "^5.8.3" }, "engines": { diff --git a/packages/rstest/src/index.ts b/packages/rstest/src/index.ts index 9d19af6943..79b5ec5da8 100644 --- a/packages/rstest/src/index.ts +++ b/packages/rstest/src/index.ts @@ -1,7 +1 @@ -export { - registerLifecycle, - type LifecycleContext, - type LifecycleProvider, - type ReportMeta, -} from './lifecycle'; -export type { RstestTestContext } from './report-helper'; +export type { Resolver } from './resolve'; diff --git a/packages/rstest/src/lifecycle.ts b/packages/rstest/src/lifecycle.ts deleted file mode 100644 index dbb6bc9245..0000000000 --- a/packages/rstest/src/lifecycle.ts +++ /dev/null @@ -1,187 +0,0 @@ -import { getDebug } from '@midscene/shared/logger'; -import { afterAll, afterEach, beforeAll, beforeEach } from '@rstest/core'; -import { - type AgentLike, - ReportHelper, - type RstestTestContext, - buildReportMeta, -} from './report-helper'; - -const debug = getDebug('rstest:lifecycle', { console: true }); - -interface SuiteContext { - filepath: string; -} - -export interface ReportMeta { - groupName: string; - reportFileName: string; -} - -interface TestFixture { - agent: TAgent; - page: TPage; - /** Runs before `agent.destroy()` — use for tasks that need the page alive (e.g. stop trace). */ - teardown?: (testCtx: RstestTestContext) => Promise; -} - -export interface LifecycleProvider< - TAgent extends AgentLike, - TPage, - TBrowser, - TOptions, -> { - launchBrowser(options: TOptions): Promise; - closeBrowser(browser: TBrowser): Promise; - createAgent( - browser: TBrowser, - url: string, - options: TOptions, - meta: ReportMeta, - ): Promise>; -} - -export interface LifecycleContext { - readonly agent: TAgent; - readonly page: TPage; - readonly browser: TBrowser; - /** - * Build and track a secondary agent for the current test (e.g. for a popup - * or a second tab). The factory receives a unique `ReportMeta` so the - * secondary's report is merged alongside the primary's. Destroy + report - * collection happen automatically in `afterEach`. - */ - spawnSecondaryAgent(build: (meta: ReportMeta) => T): T; -} - -/** - * Registers the per-suite lifecycle. `.agent` / `.page` are only valid inside - * `it(...)`; `.browser` is valid between `beforeAll` and `afterAll`. - */ -export function registerLifecycle< - TAgent extends AgentLike, - TPage, - TBrowser, - TOptions, ->( - url: string, - options: TOptions, - provider: LifecycleProvider, -): LifecycleContext { - const reportHelper = new ReportHelper(); - let browser: TBrowser | null = null; - let filepath = ''; - let currentFixture: TestFixture | null = null; - let currentMeta: ReportMeta | null = null; - const secondaryAgents: AgentLike[] = []; - let secondaryCounter = 0; - let startTime = 0; - - beforeAll(async (suite: SuiteContext) => { - filepath = suite.filepath; - reportHelper.reset(); - browser = await provider.launchBrowser(options); - }); - - beforeEach(async (testCtx) => { - if (!browser) throw new Error('[@midscene/rstest] browser not initialized'); - currentMeta = buildReportMeta(testCtx as RstestTestContext, filepath); - secondaryCounter = 0; - currentFixture = await provider.createAgent( - browser, - url, - options, - currentMeta, - ); - startTime = performance.now(); - }); - - afterEach(async (testCtx) => { - const fixture = currentFixture; - const secondaries = secondaryAgents.slice(); - currentFixture = null; - currentMeta = null; - secondaryAgents.length = 0; - secondaryCounter = 0; - - // Collect secondaries first so their pages are still alive when destroy() - // writes their report file. - for (const secondary of secondaries) { - try { - await reportHelper.collectReport( - secondary, - startTime, - testCtx as RstestTestContext, - ); - } catch (err) { - debug('secondary agent report failed:', err); - } - } - - if (fixture?.teardown) { - try { - await fixture.teardown(testCtx as RstestTestContext); - } catch (err) { - debug('provider teardown failed:', err); - } - } - - await reportHelper.collectReport( - fixture?.agent, - fixture ? startTime : undefined, - testCtx as RstestTestContext, - ); - }); - - afterAll(async (suite: SuiteContext) => { - reportHelper.mergeReports(suite.filepath); - if (browser) { - await provider.closeBrowser(browser); - browser = null; - } - }); - - function requireFixture( - field: K, - ): TestFixture[K] { - if (!currentFixture) { - throw new Error( - `[@midscene/rstest] ${field} is only available inside \`it(...)\` blocks`, - ); - } - return currentFixture[field]; - } - - return { - get agent() { - return requireFixture('agent'); - }, - get page() { - return requireFixture('page'); - }, - get browser(): TBrowser { - if (!browser) { - throw new Error( - '[@midscene/rstest] browser is only available between `beforeAll` and `afterAll`', - ); - } - return browser; - }, - spawnSecondaryAgent( - build: (meta: ReportMeta) => T, - ): T { - if (!currentMeta) { - throw new Error( - '[@midscene/rstest] secondary agents can only be spawned inside `it(...)` blocks', - ); - } - const idx = ++secondaryCounter; - const agent = build({ - groupName: currentMeta.groupName, - reportFileName: `${currentMeta.reportFileName}-page${idx}`, - }); - secondaryAgents.push(agent); - return agent; - }, - }; -} diff --git a/packages/rstest/src/playwright.ts b/packages/rstest/src/playwright.ts index d667f6fb36..e5bf7e77a3 100644 --- a/packages/rstest/src/playwright.ts +++ b/packages/rstest/src/playwright.ts @@ -1,180 +1,258 @@ +import type { Cache } from '@midscene/core'; +import { processCacheConfig } from '@midscene/core/utils'; +import { getDebug } from '@midscene/shared/logger'; import { PlaywrightAgent, type WebPageAgentOpt, overrideAIConfig, } from '@midscene/web/playwright'; +import { afterAll, test as baseTest, beforeAll } from '@rstest/core'; import { type Browser, + type BrowserContext, type BrowserContextOptions, type LaunchOptions, type Page, chromium, } from 'playwright'; -import * as playwrightNs from 'playwright'; import { isCI } from 'std-env'; -import { registerLifecycle } from './lifecycle'; import { DEFAULT_BROWSER_ARGS, DEFAULT_VIEWPORT, createDefaultsStore, } from './provider-shared'; -import type { RstestTestContext } from './report-helper'; +import { + ReportHelper, + type ReportMeta, + buildReportMeta, +} from './report-helper'; import { type Resolver, applyResolver } from './resolve'; +import type { TestApi } from './test-api-types'; + +type GoToOptions = NonNullable[1]>; + +/** + * Cache configuration shape exposed to rstest users. `id` is optional — when + * omitted (or when `cache: true`), the package fills in a stable id derived + * from the test's file basename and name, so re-runs of the same test reuse + * the same cache namespace without the user having to manage id strings. + */ +export type RstestCache = + | false + | true + | { strategy?: 'read-only' | 'read-write' | 'write-only'; id?: string }; + +type AgentOptions = Omit< + WebPageAgentOpt, + 'groupName' | 'reportFileName' | 'cache' +> & { + cache?: RstestCache; +}; export type { Resolver }; export { overrideAIConfig }; export type { WebPageAgentOpt }; -type GoToOptions = NonNullable[1]>; - -export interface SetupApi { - url: string; - browser: Browser; - playwright: typeof playwrightNs; -} +const debug = getDebug('rstest:playwright', { console: true }); -export interface CreateWebTestOptions { +export interface MidsceneOptions { /** Default: `true` in CI, `false` locally. */ headless?: boolean; - /** Default: 1920×1080. Routed into the default `contextOptions.viewport`. */ + /** + * Default: 1920×1080. Routed into the default `contextOptions.viewport`. + * Nested fields in `contextOptions.viewport` override this — e.g. passing + * `viewport: { width: 1440, height: 900 }` together with + * `contextOptions: { viewport: { width: 1920 } }` yields a final viewport of + * `{ width: 1920, height: 900 }`. + */ viewport?: { width: number; height: number }; - launchOptions?: Resolver; contextOptions?: Resolver; gotoOptions?: GoToOptions; - - agentOptions?: Omit; - - /** - * Take over the per-test page lifecycle. Return the page midscene should - * drive, plus an optional `teardown` that runs while the page is still - * alive (before `agent.destroy()`). When provided, midscene skips its - * default page setup; `headless`, `viewport`, `launchOptions`, - * `contextOptions`, and `gotoOptions` are all ignored. Only `agentOptions` - * still applies. - */ - setup?: (api: SetupApi) => Promise<{ - page: Page; - teardown?: (testCtx: RstestTestContext) => Promise; - }>; + agentOptions?: AgentOptions; } -export interface WebTestContext { - readonly agent: PlaywrightAgent; +export type AgentForPage = ( + page: Page, + opts?: AgentOptions, +) => Promise; + +export interface MidsceneFixtures { + midsceneOptions: MidsceneOptions; /** - * Raw Playwright `Page` for advanced scenarios — `page.route`, - * `page.evaluate`, `page.context().cookies()`, etc. Prefer `agent` for AI - * actions and assertions; reach for `page` only when you need - * browser-primitive control. + * Target URL the default `page` fixture navigates to. Empty string disables + * auto-navigation (page stays on `about:blank`). */ - readonly page: Page; + url: string; /** - * The file-scoped Playwright `Browser`. Use it to spin up extra contexts or - * pages mid-test (e.g. a second user session). Valid between `beforeAll` - * and `afterAll`. + * File-scoped Playwright `Browser`. Launched lazily — if no test in the file + * destructures anything that depends on `browser`, no browser is started. */ - readonly browser: Browser; + browser: Browser; + context: BrowserContext; + page: Page; + agent: PlaywrightAgent; /** - * Build a midscene agent for another page (popup, new tab, manually-created - * page from `browser.newContext()`). The agent's report is merged alongside - * the primary's. Destroy is automatic in `afterEach`. + * Factory for secondary agents bound to popups / extra contexts. Reports are + * merged alongside the primary's. Destroy is automatic in fixture teardown. */ - agentForPage( - page: Page, - opts?: Omit, - ): Promise; + agentForPage: AgentForPage; } -const defaultsStore = createDefaultsStore(); +// Private fixture — shared meta + startTime so primary and secondaries get +// the same timestamp in their reportFileName. Hidden from the public type via +// the cast at the bottom. +interface InternalFixtures extends MidsceneFixtures { + __reportMeta: { meta: ReportMeta; startTime: number }; +} + +const defaultsStore = createDefaultsStore(); /** - * Project-wide defaults for `createWebTest`. Call from a `setupFiles` entry. - * Per-call options shallow-merge over these at the top-level key; nested - * fields like `launchOptions` are replaced, not deep-merged. Use the function - * form of a resolver to compose with defaults. + * Set repo-wide `midsceneOptions` defaults that every test file picks up via + * the `midsceneOptions` fixture. + * + * **Must be called from a file referenced in rstest's `setupFiles` config** — + * each test file runs in its own isolated module graph, so the store is + * populated per file at setup time. Calling this inside a single test file + * only affects that file. + * + * Multiple calls shallow-merge: later calls overwrite previously-set top-level + * keys but keep untouched ones. Per-file overrides go via + * `test.extend({ midsceneOptions: { ... } })`. */ export const defineMidsceneDefaults = defaultsStore.define; -async function defaultSetup(api: SetupApi, opts: CreateWebTestOptions) { - const contextOptions = await applyResolver(opts.contextOptions, { - viewport: opts.viewport ?? DEFAULT_VIEWPORT, - }); - const context = await api.browser.newContext(contextOptions); - const page = await context.newPage(); - await page.goto(api.url, opts.gotoOptions); - - return { - page, - async teardown() { - try { - await context.close(); - } catch { - // The user's setup teardown may have already closed the context. - } - }, - }; +// rstest's default `isolate: true` gives each test file a fresh module graph, +// so these vars start clean per file. +let _filepath = ''; +let _browserPromise: Promise | null = null; +const _reportHelper = new ReportHelper(); + +beforeAll(async (suite: { filepath: string }) => { + if (_filepath && _filepath !== suite.filepath) { + throw new Error( + `@midscene/rstest requires test isolation but detected module-graph reuse across files (${_filepath} -> ${suite.filepath}). Remove \`isolate: false\` from your rstest config.`, + ); + } + _filepath = suite.filepath; +}); + +afterAll(async () => { + _reportHelper.mergeReports(_filepath); + if (_browserPromise) { + try { + await (await _browserPromise).close(); + } catch (err) { + debug('browser close failed:', err); + } + _browserPromise = null; + } +}); + +function acquireBrowser(opts: MidsceneOptions): Promise { + if (!_browserPromise) { + _browserPromise = applyResolver(opts.launchOptions, { + headless: opts.headless ?? isCI, + args: DEFAULT_BROWSER_ARGS, + }).then((launchOptions) => chromium.launch(launchOptions)); + } + return _browserPromise; } -export function createWebTest( - url: string, - options: CreateWebTestOptions = {}, -): WebTestContext { - const merged: CreateWebTestOptions = { ...defaultsStore.get(), ...options }; - - const inner = registerLifecycle< - PlaywrightAgent, - Page, - Browser, - CreateWebTestOptions - >(url, merged, { - async launchBrowser(opts) { - const launchOptions = await applyResolver(opts.launchOptions, { - headless: opts.headless ?? isCI, - args: DEFAULT_BROWSER_ARGS, - }); - return chromium.launch(launchOptions); - }, - async closeBrowser(browser) { - await browser.close(); - }, - async createAgent(browser, targetUrl, opts, meta) { - const setup = opts.setup ?? ((api) => defaultSetup(api, opts)); - const { page, teardown } = await setup({ - url: targetUrl, - browser, - playwright: playwrightNs, - }); +// rstest 0.9.9 doesn't export the type of `extend`'s return value; cast via the +// hand-rolled `TestApi` (see `test-api-types.ts`). +export const test = baseTest.extend({ + midsceneOptions: async (_ctx, use) => { + await use(defaultsStore.get()); + }, - const agent = new PlaywrightAgent(page, { - ...opts.agentOptions, - groupName: meta.groupName, - reportFileName: meta.reportFileName, - }); + url: '', + + __reportMeta: async ({ task }, use) => { + await use({ + meta: buildReportMeta({ task }, _filepath), + startTime: performance.now(), + }); + }, - return { agent, page, teardown }; - }, - }); - - return { - get agent() { - return inner.agent; - }, - get page() { - return inner.page; - }, - get browser() { - return inner.browser; - }, - async agentForPage(page, opts) { - return inner.spawnSecondaryAgent( - (meta) => - new PlaywrightAgent(page, { - ...merged.agentOptions, - ...opts, - groupName: meta.groupName, - reportFileName: meta.reportFileName, - }), + browser: async ({ midsceneOptions }, use) => { + await use(await acquireBrowser(midsceneOptions)); + }, + + context: async ({ browser, midsceneOptions }, use) => { + const contextOptions = await applyResolver(midsceneOptions.contextOptions, { + viewport: midsceneOptions.viewport ?? DEFAULT_VIEWPORT, + }); + const context = await browser.newContext(contextOptions); + await use(context); + try { + await context.close(); + } catch (err) { + debug('context close failed:', err); + } + }, + + page: async ({ context, url, midsceneOptions }, use) => { + const page = await context.newPage(); + if (url) { + await page.goto(url, midsceneOptions.gotoOptions); + } + await use(page); + }, + + agent: async ({ page, midsceneOptions, __reportMeta, task }, use) => { + const { cache: rawCache, ...rest } = midsceneOptions.agentOptions ?? {}; + const cache = processCacheConfig( + rawCache as Cache | undefined, + __reportMeta.meta.cacheId, + ); + const agent = new PlaywrightAgent(page, { + ...rest, + cache, + groupName: __reportMeta.meta.groupName, + reportFileName: __reportMeta.meta.reportFileName, + }); + await use(agent); + await _reportHelper.collectReport(agent, __reportMeta.startTime, { task }); + }, + + // Depends on `agent` so its teardown runs BEFORE the primary's — secondaries + // are collected while their pages are still alive. + agentForPage: async ({ agent, midsceneOptions, __reportMeta, task }, use) => { + const secondaries: PlaywrightAgent[] = []; + let counter = 0; + + const helper: AgentForPage = async (secondaryPage, opts) => { + counter += 1; + const { cache: optsCache, ...optsRest } = opts ?? {}; + const { cache: fixtureCache, ...fixtureRest } = + midsceneOptions.agentOptions ?? {}; + const cache = processCacheConfig( + (optsCache ?? fixtureCache) as Cache | undefined, + __reportMeta.meta.cacheId, ); - }, - }; -} + const secondary = new PlaywrightAgent(secondaryPage, { + ...fixtureRest, + ...optsRest, + cache, + groupName: __reportMeta.meta.groupName, + reportFileName: `${__reportMeta.meta.reportFileName}-page${counter}`, + }); + secondaries.push(secondary); + return secondary; + }; + await use(helper); + + for (const secondary of secondaries) { + try { + await _reportHelper.collectReport(secondary, __reportMeta.startTime, { + task, + }); + } catch (err) { + debug('secondary agent report failed:', err); + } + } + void agent; + }, +}) as unknown as TestApi; diff --git a/packages/rstest/src/puppeteer.ts b/packages/rstest/src/puppeteer.ts index 7b58b6b50d..755c538ee5 100644 --- a/packages/rstest/src/puppeteer.ts +++ b/packages/rstest/src/puppeteer.ts @@ -1,8 +1,12 @@ +import type { Cache } from '@midscene/core'; +import { processCacheConfig } from '@midscene/core/utils'; +import { getDebug } from '@midscene/shared/logger'; import { PuppeteerAgent, type WebPageAgentOpt, overrideAIConfig, } from '@midscene/web/puppeteer'; +import { afterAll, test as baseTest, beforeAll } from '@rstest/core'; import puppeteer, { type Browser, type GoToOptions, @@ -10,160 +14,225 @@ import puppeteer, { type Page, } from 'puppeteer'; import { isCI } from 'std-env'; -import { registerLifecycle } from './lifecycle'; import { DEFAULT_BROWSER_ARGS, DEFAULT_VIEWPORT, createDefaultsStore, } from './provider-shared'; -import type { RstestTestContext } from './report-helper'; +import { + ReportHelper, + type ReportMeta, + buildReportMeta, +} from './report-helper'; import { type Resolver, applyResolver } from './resolve'; +import type { TestApi } from './test-api-types'; + +/** + * Cache configuration shape exposed to rstest users. `id` is optional — when + * omitted (or when `cache: true`), the package fills in a stable id derived + * from the test's file basename and name, so re-runs of the same test reuse + * the same cache namespace without the user having to manage id strings. + */ +export type RstestCache = + | false + | true + | { strategy?: 'read-only' | 'read-write' | 'write-only'; id?: string }; + +type AgentOptions = Omit< + WebPageAgentOpt, + 'groupName' | 'reportFileName' | 'cache' +> & { + cache?: RstestCache; +}; export type { Resolver }; export { overrideAIConfig }; export type { WebPageAgentOpt }; -export interface SetupApi { - url: string; - browser: Browser; - puppeteer: typeof puppeteer; -} +const debug = getDebug('rstest:puppeteer', { console: true }); -export interface CreateWebTestOptions { +export interface MidsceneOptions { /** Default: `true` in CI, `false` locally. */ headless?: boolean; - /** Default: 1920×1080. Routed into the default `launchOptions.defaultViewport`. */ + /** + * Default: 1920×1080. Routed into the default `launchOptions.defaultViewport`. + * Nested fields in `launchOptions.defaultViewport` override this — e.g. + * passing `viewport: { width: 1440, height: 900 }` together with + * `launchOptions: { defaultViewport: { width: 1920 } }` yields a final + * default viewport of `{ width: 1920, height: 900 }`. + */ viewport?: { width: number; height: number }; - launchOptions?: Resolver; gotoOptions?: GoToOptions; - - agentOptions?: Omit; - - /** - * Take over the per-test page lifecycle. Return the page midscene should - * drive, plus an optional `teardown` that runs while the page is still - * alive (before `agent.destroy()`). When provided, midscene skips its - * default page setup; `headless`, `viewport`, `launchOptions`, and - * `gotoOptions` are all ignored. Only `agentOptions` still applies. - */ - setup?: (api: SetupApi) => Promise<{ - page: Page; - teardown?: (testCtx: RstestTestContext) => Promise; - }>; + agentOptions?: AgentOptions; } -export interface WebTestContext { - readonly agent: PuppeteerAgent; +export type AgentForPage = ( + page: Page, + opts?: AgentOptions, +) => Promise; + +export interface MidsceneFixtures { + midsceneOptions: MidsceneOptions; /** - * Raw Puppeteer `Page` for advanced scenarios — `page.setRequestInterception`, - * `page.evaluate`, etc. Prefer `agent` for AI actions and assertions; reach - * for `page` only when you need browser-primitive control. + * Target URL the default `page` fixture navigates to. Empty string disables + * auto-navigation. */ - readonly page: Page; + url: string; /** - * The file-scoped Puppeteer `Browser`. Use it to open extra pages mid-test - * (e.g. a second user session via `browser.createBrowserContext()` + - * `context.newPage()`). Valid between `beforeAll` and `afterAll`. + * File-scoped Puppeteer `Browser`. Launched lazily — if no test in the file + * destructures anything that depends on `browser`, no browser is started. */ - readonly browser: Browser; + browser: Browser; + page: Page; + agent: PuppeteerAgent; /** - * Build a midscene agent for another page (popup, manually-created page). - * The agent's report is merged alongside the primary's. Destroy is automatic - * in `afterEach`. + * Factory for secondary agents bound to popups / extra pages. Reports are + * merged alongside the primary's. Destroy is automatic in fixture teardown. */ - agentForPage( - page: Page, - opts?: Omit, - ): Promise; + agentForPage: AgentForPage; +} + +// Private fixture (see playwright.ts). +interface InternalFixtures extends MidsceneFixtures { + __reportMeta: { meta: ReportMeta; startTime: number }; } -const defaultsStore = createDefaultsStore(); +const defaultsStore = createDefaultsStore(); /** - * Project-wide defaults for `createWebTest`. Call from a `setupFiles` entry. - * Per-call options shallow-merge over these at the top-level key; nested - * fields like `launchOptions` are replaced, not deep-merged. Use the function - * form of a resolver to compose with defaults. + * Set repo-wide `midsceneOptions` defaults that every test file picks up via + * the `midsceneOptions` fixture. + * + * **Must be called from a file referenced in rstest's `setupFiles` config** — + * each test file runs in its own isolated module graph, so the store is + * populated per file at setup time. Calling this inside a single test file + * only affects that file. + * + * Multiple calls shallow-merge: later calls overwrite previously-set top-level + * keys but keep untouched ones. Per-file overrides go via + * `test.extend({ midsceneOptions: { ... } })`. */ export const defineMidsceneDefaults = defaultsStore.define; -async function defaultSetup(api: SetupApi, opts: CreateWebTestOptions) { - const page = await api.browser.newPage(); - await page.goto(api.url, opts.gotoOptions); - return { - page, - async teardown() { - try { - await page.close(); - } catch { - // The user's setup teardown may have already closed the page. - } - }, - }; +let _filepath = ''; +let _browserPromise: Promise | null = null; +const _reportHelper = new ReportHelper(); + +beforeAll(async (suite: { filepath: string }) => { + if (_filepath && _filepath !== suite.filepath) { + throw new Error( + `@midscene/rstest requires test isolation but detected module-graph reuse across files (${_filepath} -> ${suite.filepath}). Remove \`isolate: false\` from your rstest config.`, + ); + } + _filepath = suite.filepath; +}); + +afterAll(async () => { + _reportHelper.mergeReports(_filepath); + if (_browserPromise) { + try { + await (await _browserPromise).close(); + } catch (err) { + debug('browser close failed:', err); + } + _browserPromise = null; + } +}); + +function acquireBrowser(opts: MidsceneOptions): Promise { + if (!_browserPromise) { + _browserPromise = applyResolver(opts.launchOptions, { + headless: opts.headless ?? isCI, + args: DEFAULT_BROWSER_ARGS, + defaultViewport: opts.viewport ?? DEFAULT_VIEWPORT, + }).then((launchOptions) => puppeteer.launch(launchOptions)); + } + return _browserPromise; } -export function createWebTest( - url: string, - options: CreateWebTestOptions = {}, -): WebTestContext { - const merged: CreateWebTestOptions = { ...defaultsStore.get(), ...options }; - - const inner = registerLifecycle< - PuppeteerAgent, - Page, - Browser, - CreateWebTestOptions - >(url, merged, { - async launchBrowser(opts) { - const launchOptions = await applyResolver(opts.launchOptions, { - headless: opts.headless ?? isCI, - args: DEFAULT_BROWSER_ARGS, - defaultViewport: opts.viewport ?? DEFAULT_VIEWPORT, - }); - return puppeteer.launch(launchOptions); - }, - async closeBrowser(browser) { - await browser.close(); - }, - async createAgent(browser, targetUrl, opts, meta) { - const setup = opts.setup ?? ((api) => defaultSetup(api, opts)); - const { page, teardown } = await setup({ - url: targetUrl, - browser, - puppeteer, - }); +export const test = baseTest.extend({ + midsceneOptions: async (_ctx, use) => { + await use(defaultsStore.get()); + }, - const agent = new PuppeteerAgent(page, { - ...opts.agentOptions, - groupName: meta.groupName, - reportFileName: meta.reportFileName, - }); + url: '', + + __reportMeta: async ({ task }, use) => { + await use({ + meta: buildReportMeta({ task }, _filepath), + startTime: performance.now(), + }); + }, - return { agent, page, teardown }; - }, - }); - - return { - get agent() { - return inner.agent; - }, - get page() { - return inner.page; - }, - get browser() { - return inner.browser; - }, - async agentForPage(page, opts) { - return inner.spawnSecondaryAgent( - (meta) => - new PuppeteerAgent(page, { - ...merged.agentOptions, - ...opts, - groupName: meta.groupName, - reportFileName: meta.reportFileName, - }), + browser: async ({ midsceneOptions }, use) => { + await use(await acquireBrowser(midsceneOptions)); + }, + + page: async ({ browser, url, midsceneOptions }, use) => { + const page = await browser.newPage(); + if (url) { + await page.goto(url, midsceneOptions.gotoOptions); + } + await use(page); + try { + await page.close(); + } catch (err) { + debug('page close failed:', err); + } + }, + + agent: async ({ page, midsceneOptions, __reportMeta, task }, use) => { + const { cache: rawCache, ...rest } = midsceneOptions.agentOptions ?? {}; + const cache = processCacheConfig( + rawCache as Cache | undefined, + __reportMeta.meta.cacheId, + ); + const agent = new PuppeteerAgent(page, { + ...rest, + cache, + groupName: __reportMeta.meta.groupName, + reportFileName: __reportMeta.meta.reportFileName, + }); + await use(agent); + await _reportHelper.collectReport(agent, __reportMeta.startTime, { task }); + }, + + // Depends on `agent` so its teardown runs BEFORE the primary's. + agentForPage: async ({ agent, midsceneOptions, __reportMeta, task }, use) => { + const secondaries: PuppeteerAgent[] = []; + let counter = 0; + + const helper: AgentForPage = async (secondaryPage, opts) => { + counter += 1; + const { cache: optsCache, ...optsRest } = opts ?? {}; + const { cache: fixtureCache, ...fixtureRest } = + midsceneOptions.agentOptions ?? {}; + const cache = processCacheConfig( + (optsCache ?? fixtureCache) as Cache | undefined, + __reportMeta.meta.cacheId, ); - }, - }; -} + const secondary = new PuppeteerAgent(secondaryPage, { + ...fixtureRest, + ...optsRest, + cache, + groupName: __reportMeta.meta.groupName, + reportFileName: `${__reportMeta.meta.reportFileName}-page${counter}`, + }); + secondaries.push(secondary); + return secondary; + }; + await use(helper); + + for (const secondary of secondaries) { + try { + await _reportHelper.collectReport(secondary, __reportMeta.startTime, { + task, + }); + } catch (err) { + debug('secondary agent report failed:', err); + } + } + void agent; + }, +}) as unknown as TestApi; diff --git a/packages/rstest/src/report-helper.ts b/packages/rstest/src/report-helper.ts index 313737afca..42af17ca55 100644 --- a/packages/rstest/src/report-helper.ts +++ b/packages/rstest/src/report-helper.ts @@ -2,6 +2,7 @@ import { mkdirSync, writeFileSync } from 'node:fs'; import { basename, extname, join } from 'node:path'; import type { TestStatus } from '@midscene/core'; import { ReportMergingTool } from '@midscene/core/report'; +import { replaceIllegalPathCharsAndSpace } from '@midscene/shared/utils'; import { MANIFEST_DIR, generateTimestamp, manifestKey } from './utils'; export interface RstestTestContext { @@ -20,6 +21,17 @@ export interface AgentLike { destroy(): Promise; } +export interface ReportMeta { + groupName: string; + reportFileName: string; + /** + * Stable cache id derived from `${file}(${task.name})`. Unlike + * `reportFileName` this carries no timestamp, so retries and re-runs of the + * same test reuse the same cache namespace. + */ + cacheId: string; +} + const STATUS_MAP: Record = { pass: 'passed', fail: 'failed', @@ -36,11 +48,6 @@ export class ReportHelper { private reportTool = new ReportMergingTool(); private firstReport: string | null = null; - reset(): void { - this.reportTool = new ReportMergingTool(); - this.firstReport = null; - } - async collectReport( agent: AgentLike | undefined, startTime: number | undefined, @@ -93,12 +100,13 @@ export class ReportHelper { export function buildReportMeta( testCtx: RstestTestContext, filepath: string, -): { groupName: string; reportFileName: string } { +): ReportMeta { const base = basename(filepath, extname(filepath)) || 'UnnamedGroup'; const taskName = testCtx.task.name; return { groupName: `E2E: ${base}`, reportFileName: `E2E-${base}-${taskName}-${generateTimestamp()}`, + cacheId: replaceIllegalPathCharsAndSpace(`${base}(${taskName})`), }; } diff --git a/packages/rstest/src/resolve.ts b/packages/rstest/src/resolve.ts index cae9da5f96..1b9c269f8b 100644 --- a/packages/rstest/src/resolve.ts +++ b/packages/rstest/src/resolve.ts @@ -1,17 +1,39 @@ +import deepmerge from 'deepmerge'; +import { + type ConfigChainAsyncWithContext, + reduceConfigsAsyncWithContext, +} from 'reduce-configs'; + /** - * An options "resolver". Pass an object to shallow-merge over the resolved - * defaults, or a function to receive the defaults and return a fully-controlled - * value (sync or async). + * An options "resolver". Accepts: + * - an object — deep-merged over the resolved defaults; arrays concatenate, + * - a function `(defaults) => value` (sync or async) that takes full control, or + * - an array of either, applied left-to-right. + * + * Deep-merge keeps sibling fields: passing `{ viewport: { width: 1440 } }` over + * a base of `{ viewport: { width: 1920, height: 1080 } }` produces + * `{ viewport: { width: 1440, height: 1080 } }`. Arrays concatenate, so + * `{ args: ['--start-fullscreen'] }` appends to the package's default browser + * args rather than replacing them. To fully replace, use the function form: + * `(defaults) => ({ ...defaults, args: ['--only-this'] })`. */ -export type Resolver = T | ((defaults: T) => T | Promise); +// Inlined from reduce-configs so the generated .d.ts doesn't reference a +// devDependency. `reduce-configs` is bundled by rslib at build time but isn't +// shipped as a runtime dep, so consumers must not see it in our type surface. +// The runtime supports `void`-returning mutate-in-place functions too, but +// they're an antipattern — we only advertise the return-new-value form. +export type Resolver = + | T + | ((config: T) => T | Promise) + | ReadonlyArray T | Promise)>; -export async function applyResolver( +export function applyResolver( input: Resolver | undefined, base: T, ): Promise { - if (input === undefined) return base; - if (typeof input === 'function') { - return await (input as (defaults: T) => T | Promise)(base); - } - return { ...base, ...input }; + return reduceConfigsAsyncWithContext({ + initial: base, + config: input as ConfigChainAsyncWithContext, + mergeFn: deepmerge as unknown as typeof Object.assign, + }); } diff --git a/packages/rstest/src/test-api-types.ts b/packages/rstest/src/test-api-types.ts new file mode 100644 index 0000000000..887ea39db2 --- /dev/null +++ b/packages/rstest/src/test-api-types.ts @@ -0,0 +1,85 @@ +/** + * rstest 0.9.9 doesn't `export` its internal `TestAPIs` / `TestContext` / + * `FixtureOptions` types — they're `declare`d only. That makes the type of + * `baseTest.extend(...)` unnameable from outside the module, which + * crashes `tsc --declaration` with TS4023 the moment we try to re-export it. + * + * The interfaces below mirror the public surface users actually touch + * (call signature + variants + `extend` chain + `each` / `for`), built only + * from public types so the emitted `.d.ts` doesn't reference any unexported + * rstest internals. + * + * If a future rstest release exports these types directly, delete this file + * and use them straight from `@rstest/core`. + */ + +import type { Expect } from '@rstest/core'; + +type MaybePromise = T | Promise; + +export interface TestTaskInfo { + id: string; + name: string; + result?: { + status: 'pass' | 'fail' | 'skip' | 'todo'; + errors?: Array<{ message?: string }>; + }; +} + +export interface BaseTestContext { + task: TestTaskInfo; + expect: Expect; + onTestFinished: (fn: (ctx: BaseTestContext) => MaybePromise) => void; + onTestFailed: (fn: (ctx: BaseTestContext) => MaybePromise) => void; +} + +export type TestCallback = ( + ctx: BaseTestContext & Extra, +) => MaybePromise; + +export type FixtureFn = ( + ctx: BaseTestContext & Extra, + use: (value: Value) => Promise, +) => Promise; + +export type FixtureDecl = + | Value + | FixtureFn + | [FixtureFn, { auto?: boolean }]; + +export type FixturesDecl = { + [K in keyof T]: FixtureDecl>; +}; + +export interface TestApi { + (description: string, fn?: TestCallback, timeout?: number): void; + + skip: TestApi; + only: TestApi; + todo: TestApi; + fails: TestApi; + concurrent: TestApi; + sequential: TestApi; + runIf: (condition: boolean) => TestApi; + skipIf: (condition: boolean) => TestApi; + + extend>( + fixtures: FixturesDecl, + ): TestApi; + + each: ( + cases: readonly T[], + ) => ( + description: string, + fn?: (param: T, ctx: BaseTestContext & Extra) => MaybePromise, + timeout?: number, + ) => void; + + for: ( + cases: readonly T[], + ) => ( + description: string, + fn?: (param: T, ctx: BaseTestContext & Extra) => MaybePromise, + timeout?: number, + ) => void; +} diff --git a/packages/rstest/tests/unit-test/resolve.test.ts b/packages/rstest/tests/unit-test/resolve.test.ts index cd53496c0a..1b8e9e1e23 100644 --- a/packages/rstest/tests/unit-test/resolve.test.ts +++ b/packages/rstest/tests/unit-test/resolve.test.ts @@ -7,19 +7,34 @@ describe('applyResolver', () => { expect(await applyResolver(undefined, base)).toEqual(base); }); - it('shallow-merges object input over base', async () => { + it('deep-merges object input over base', async () => { const base = { a: 1, b: 2, c: 3 }; expect(await applyResolver({ b: 20 }, base)).toEqual({ a: 1, b: 20, c: 3 }); }); - it('replaces nested values entirely (no deep merge)', async () => { - const base = { args: ['--no-sandbox'], proxy: { server: 'a' } }; + it('preserves sibling fields in nested objects', async () => { + const base = { viewport: { width: 1920, height: 1080 } }; expect( await applyResolver( - { proxy: { server: 'b' } } as { proxy: { server: string } }, + { viewport: { width: 1440 } } as Partial, base, ), - ).toEqual({ args: ['--no-sandbox'], proxy: { server: 'b' } }); + ).toEqual({ viewport: { width: 1440, height: 1080 } }); + }); + + it('concatenates arrays instead of replacing them', async () => { + const base = { + args: ['--no-sandbox', '--ignore-certificate-errors'] as string[], + }; + expect(await applyResolver({ args: ['--start-fullscreen'] }, base)).toEqual( + { + args: [ + '--no-sandbox', + '--ignore-certificate-errors', + '--start-fullscreen', + ], + }, + ); }); it('calls function with resolved defaults and returns its result', async () => { @@ -51,4 +66,13 @@ describe('applyResolver', () => { const result = await applyResolver(() => ({ a: 99, b: 99 }), base); expect(result).toEqual({ a: 99, b: 99 }); }); + + it('applies an array of overrides left-to-right', async () => { + const base = { a: 1, b: 2, args: ['x'] as string[] }; + const result = await applyResolver( + [{ a: 10 }, (d) => ({ ...d, b: d.b + 100 }), { args: ['y'] }], + base, + ); + expect(result).toEqual({ a: 10, b: 102, args: ['x', 'y'] }); + }); }); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 9745895781..cce475fea0 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -466,7 +466,7 @@ importers: version: 5.8.3 vitest: specifier: 3.0.5 - version: 3.0.5(@types/debug@4.1.12)(@types/node@25.5.2)(jsdom@29.0.2)(less@4.2.2)(lightningcss@1.30.1)(sass-embedded@1.86.3)(terser@5.46.1) + version: 3.0.5(@types/debug@4.1.12)(@types/node@25.6.0)(jsdom@29.0.2)(less@4.2.2)(lightningcss@1.30.1)(sass-embedded@1.86.3)(terser@5.46.2) apps/site: dependencies: @@ -1560,9 +1560,6 @@ importers: '@midscene/shared': specifier: workspace:* version: link:../shared - std-env: - specifier: ^4.1.0 - version: 4.1.0 devDependencies: '@midscene/core': specifier: workspace:* @@ -1582,6 +1579,9 @@ importers: '@types/node': specifier: ^18.0.0 version: 18.19.130 + deepmerge: + specifier: ^4.3.1 + version: 4.3.1 dotenv: specifier: ^16.4.5 version: 16.4.7 @@ -1591,6 +1591,12 @@ importers: puppeteer: specifier: 24.6.0 version: 24.6.0(bufferutil@4.0.9)(typescript@5.8.3)(utf-8-validate@6.0.5) + reduce-configs: + specifier: ^1.1.2 + version: 1.1.2 + std-env: + specifier: ^4.1.0 + version: 4.1.0 typescript: specifier: ^5.8.3 version: 5.8.3 @@ -2340,19 +2346,16 @@ packages: '@computer-use/libnut-darwin@2.7.1': resolution: {integrity: sha512-7B/aPcIYS4a4S7D3IYIHSpZ4B4m8Z3CjlYq0efTr+/JYmEu+LlO67ZhPvisLKifhagxf7goqEfnphg1F4jq5jw==} engines: {node: '>=10.15.3'} - cpu: [x64, arm64] os: [darwin, linux, win32] '@computer-use/libnut-linux@2.7.1': resolution: {integrity: sha512-QJD5URTFJ/2+JwBwRyajRF2BB+3eXpd4+t5btGeRVeiRQLKQ4lgorbMHySo6IrfAbSfnU1OVOrxAUygGxj0cFg==} engines: {node: '>=10.15.3'} - cpu: [x64, arm64] os: [darwin, linux, win32] '@computer-use/libnut-win32@2.7.1': resolution: {integrity: sha512-nDvH5kP1zoO2cBtFYWV0om9xtTu523cc1LIk8r/wizqPrIAm0wCizTU+odF3Fi42zcKJWT6J+Pguy8fKrZyIuA==} engines: {node: '>=10.15.3'} - cpu: [x64, arm64] os: [darwin, linux, win32] '@computer-use/libnut@4.2.0': @@ -10508,8 +10511,8 @@ packages: resolution: {integrity: sha512-6tDA8g98We0zd0GvVeMT9arEOnTw9qM03L9cJXaCjrip1OO764RDBLBfrB4cwzNGDj5OA5ioymC9GkizgWJDUg==} engines: {node: '>=8'} - reduce-configs@1.1.1: - resolution: {integrity: sha512-EYtsVGAQarE8daT54cnaY1PIknF2VB78ug6Zre2rs36EsJfC40EG6hmTU2A2P1ZuXnKAt2KI0fzOGHcX7wzdPw==} + reduce-configs@1.1.2: + resolution: {integrity: sha512-AgBP55V8FC7NaqoOP2RCbTpu6LE+YuX3LUZkNAoitcfyS3/PIC8Obg/TJrBzTkJ+lDvZv0TTAeDpLkzjTtYlbw==} reflect.getprototypeof@1.0.10: resolution: {integrity: sha512-00o4I+DVrefhv+nX0ulyi3biSHCPDe+yLv5o/p6d/UVlirijB8E16FtfwSAi4g3tcqrQ4lRAqQSoFEZJehYEcw==} @@ -15421,13 +15424,13 @@ snapshots: dependencies: '@rsbuild/core': 1.6.15 deepmerge: 4.3.1 - reduce-configs: 1.1.1 + reduce-configs: 1.1.2 '@rsbuild/plugin-less@1.5.0(@rsbuild/core@2.0.3(core-js@3.47.0))': dependencies: '@rsbuild/core': 2.0.3(core-js@3.47.0) deepmerge: 4.3.1 - reduce-configs: 1.1.1 + reduce-configs: 1.1.2 '@rsbuild/plugin-node-polyfill@1.4.2(@rsbuild/core@1.6.15)': dependencies: @@ -15567,7 +15570,7 @@ snapshots: dependencies: deepmerge: 4.3.1 json5: 2.2.3 - reduce-configs: 1.1.1 + reduce-configs: 1.1.2 ts-checker-rspack-plugin: 1.2.2(@rspack/core@1.6.8(@swc/helpers@0.5.21))(typescript@5.8.3) optionalDependencies: '@rsbuild/core': 1.6.15 @@ -15579,7 +15582,7 @@ snapshots: dependencies: deepmerge: 4.3.1 json5: 2.2.3 - reduce-configs: 1.1.1 + reduce-configs: 1.1.2 ts-checker-rspack-plugin: 1.2.2(@rspack/core@2.0.1(@swc/helpers@0.5.21))(typescript@5.8.3) optionalDependencies: '@rsbuild/core': 1.6.15 @@ -23031,7 +23034,7 @@ snapshots: indent-string: 4.0.0 strip-indent: 3.0.0 - reduce-configs@1.1.1: {} + reduce-configs@1.1.2: {} reflect.getprototypeof@1.0.10: dependencies: