diff --git a/apps/site/docs/en/integrate-with-playwright.mdx b/apps/site/docs/en/integrate-with-playwright.mdx index bb4b0d9c75..a5679eaaeb 100644 --- a/apps/site/docs/en/integrate-with-playwright.mdx +++ b/apps/site/docs/en/integrate-with-playwright.mdx @@ -174,6 +174,8 @@ export const test = base.extend( ); ``` +`PlaywrightAiFixture()` accepts all `PlaywrightAgent` options, so you can configure shared agent behavior directly at fixture creation time. Fixture-managed metadata like `testId`, `reportFileName`, `groupName`, and `groupDescription` are still generated automatically. + ### Step 3: Write test cases Review the full catalog of action, query, and utility methods in the [Agent API reference](./api#interaction-methods). When you need lower-level control, you can use `agentForPage` to obtain the underlying `PageAgent` instance and call any API directly: diff --git a/apps/site/docs/en/web-api-reference.mdx b/apps/site/docs/en/web-api-reference.mdx index fa84249298..3be0a0b16d 100644 --- a/apps/site/docs/en/web-api-reference.mdx +++ b/apps/site/docs/en/web-api-reference.mdx @@ -194,6 +194,8 @@ test('search flow', async ({ agentForPage, page }) => { }); ``` +The fixture accepts all `PlaywrightAgent` options, so you can configure shared agent behavior once at fixture creation time. Per-test metadata such as `testId`, `reportFileName`, `groupName`, and `groupDescription` remain fixture-managed. + ### See also - [Integrate with Playwright](./integrate-with-playwright) for setup, fixtures, and advanced configuration. diff --git a/apps/site/docs/zh/integrate-with-playwright.mdx b/apps/site/docs/zh/integrate-with-playwright.mdx index cf209388c9..a56bfc1a21 100644 --- a/apps/site/docs/zh/integrate-with-playwright.mdx +++ b/apps/site/docs/zh/integrate-with-playwright.mdx @@ -174,6 +174,8 @@ export const test = base.extend( ); ``` +`PlaywrightAiFixture()` 也支持传入 `PlaywrightAgent` 的全部配置,因此你可以在创建 fixture 时统一配置共享的 Agent 行为。`testId`、`reportFileName`、`groupName`、`groupDescription` 这类由 fixture 管理的元信息仍会自动生成。 + ### 第三步:编写测试用例 完整的交互、查询和辅助 API 请参考 [Agent API 参考](./api#interaction-methods)。如果需要调用更底层的能力,可以使用 `agentForPage` 获取 `PageAgent` 实例,再直接调用对应的方法: diff --git a/apps/site/docs/zh/web-api-reference.mdx b/apps/site/docs/zh/web-api-reference.mdx index 20f5482ebc..9419908dfb 100644 --- a/apps/site/docs/zh/web-api-reference.mdx +++ b/apps/site/docs/zh/web-api-reference.mdx @@ -194,6 +194,8 @@ test('search flow', async ({ agentForPage, page }) => { }); ``` +这个 fixture 也支持传入 `PlaywrightAgent` 的全部配置,因此你可以在创建 fixture 时统一配置共享的 Agent 行为。`testId`、`reportFileName`、`groupName`、`groupDescription` 这类按测试自动生成的元信息仍由 fixture 管理。 + ### 另请参阅 - [集成到 Playwright](./integrate-with-playwright) 获取安装、Fixture 用法和更多配置。 diff --git a/packages/web-integration/src/playwright/ai-fixture.ts b/packages/web-integration/src/playwright/ai-fixture.ts index 2ec14ed4fa..48ebe4177f 100644 --- a/packages/web-integration/src/playwright/ai-fixture.ts +++ b/packages/web-integration/src/playwright/ai-fixture.ts @@ -1,7 +1,7 @@ import { PlaywrightAgent, type PlaywrightWebPage } from '@/playwright/index'; import type { WebPageAgentOpt } from '@/web-element'; import type { Cache } from '@midscene/core'; -import type { AgentOpt, Agent as PageAgent } from '@midscene/core/agent'; +import type { Agent as PageAgent } from '@midscene/core/agent'; import { processCacheConfig } from '@midscene/core/utils'; import { DEFAULT_WAIT_FOR_NAVIGATION_TIMEOUT, @@ -56,17 +56,25 @@ type PlaywrightCacheConfig = { }; type PlaywrightCache = false | true | PlaywrightCacheConfig; -export const PlaywrightAiFixture = (options?: { - forceSameTabNavigation?: boolean; - waitForNetworkIdleTimeout?: number; - waitForNavigationTimeout?: number; +export type PlaywrightAiFixtureOptions = Omit< + WebPageAgentOpt, + | 'testId' + | 'cacheId' + | 'groupName' + | 'groupDescription' + | 'reportFileName' + | 'cache' +> & { cache?: PlaywrightCache; -}) => { +}; + +export const PlaywrightAiFixture = (options?: PlaywrightAiFixtureOptions) => { const { forceSameTabNavigation = true, waitForNetworkIdleTimeout = DEFAULT_WAIT_FOR_NETWORK_IDLE_TIMEOUT, waitForNavigationTimeout = DEFAULT_WAIT_FOR_NAVIGATION_TIMEOUT, cache, + ...sharedAgentOptions } = options ?? {}; // Helper function to process cache configuration and auto-generate ID from test info @@ -139,6 +147,7 @@ export const PlaywrightAiFixture = (options?: { groupName: title, groupDescription: file, generateReport: true, + ...sharedAgentOptions, ...opts, }); pageAgentMap[idForPage] = agent; @@ -261,7 +270,7 @@ export const PlaywrightAiFixture = (options?: { await use( async ( propsPage?: OriginPlaywrightPage | undefined, - opts?: AgentOpt, + opts?: WebPageAgentOpt, ) => { const cacheConfig = processTestCacheConfig(testInfo); @@ -606,8 +615,8 @@ export const PlaywrightAiFixture = (options?: { export type PlayWrightAiFixtureType = { agentForPage: ( - page?: any, - opts?: any, + page?: OriginPlaywrightPage, + opts?: WebPageAgentOpt, ) => Promise>; ai: (...args: Parameters) => Promise; aiAct: ( diff --git a/packages/web-integration/src/playwright/index.ts b/packages/web-integration/src/playwright/index.ts index 29642e3c59..623197a3df 100644 --- a/packages/web-integration/src/playwright/index.ts +++ b/packages/web-integration/src/playwright/index.ts @@ -2,7 +2,10 @@ import { Agent as PageAgent } from '@midscene/core/agent'; import type { Page as PlaywrightPage } from 'playwright'; import { WebPage as PlaywrightWebPage } from './page'; -export type { PlayWrightAiFixtureType } from './ai-fixture'; +export type { + PlayWrightAiFixtureType, + PlaywrightAiFixtureOptions, +} from './ai-fixture'; export { PlaywrightAiFixture } from './ai-fixture'; export { overrideAIConfig } from '@midscene/shared/env'; export { WebPage as PlaywrightWebPage } from './page'; diff --git a/packages/web-integration/tests/unit-test/playwright-ai-fixture-options.test.ts b/packages/web-integration/tests/unit-test/playwright-ai-fixture-options.test.ts new file mode 100644 index 0000000000..f46fcf043a --- /dev/null +++ b/packages/web-integration/tests/unit-test/playwright-ai-fixture-options.test.ts @@ -0,0 +1,127 @@ +import { beforeEach, describe, expect, it, vi } from 'vitest'; + +const mockState = vi.hoisted(() => ({ + ctorOpts: [] as any[], + instances: [] as any[], +})); + +vi.mock('@/playwright/index', () => { + class MockPlaywrightAgent { + reportFile?: string; + + constructor(_page: any, opts: any) { + mockState.ctorOpts.push(opts); + mockState.instances.push(this); + this.reportFile = opts?.generateReport ? 'mock-report.html' : undefined; + } + + async destroy() {} + } + + return { + PlaywrightAgent: MockPlaywrightAgent, + }; +}); + +import { PlaywrightAiFixture } from '@/playwright/ai-fixture'; + +describe('PlaywrightAiFixture option forwarding', () => { + beforeEach(() => { + mockState.ctorOpts.length = 0; + mockState.instances.length = 0; + }); + + const createPage = () => + ({ + on: vi.fn(), + }) as any; + + const createTestInfo = () => + ({ + testId: 'test-id', + titlePath: ['fixture.spec.ts', 'forwards options'], + annotations: [], + retry: 0, + }) as any; + + it('should forward fixture-level AgentOpt and WebPageOpt to the first agent creation', async () => { + const fixture = PlaywrightAiFixture({ + autoPrintReportMsg: false, + outputFormat: 'html-and-external-assets', + waitAfterAction: 120, + enableTouchEventsInActionSpace: true, + forceChromeSelectRendering: true, + }); + + await fixture.ai({ page: createPage() }, async () => {}, createTestInfo()); + + expect(mockState.ctorOpts).toHaveLength(1); + expect(mockState.ctorOpts[0]).toMatchObject({ + autoPrintReportMsg: false, + outputFormat: 'html-and-external-assets', + waitAfterAction: 120, + enableTouchEventsInActionSpace: true, + forceChromeSelectRendering: true, + generateReport: true, + }); + }); + + it('should allow the first agentForPage call to override fixture defaults', async () => { + const fixture = PlaywrightAiFixture({ + autoPrintReportMsg: true, + waitAfterAction: 300, + enableTouchEventsInActionSpace: true, + }); + + let getAgentForPage: any; + await fixture.agentForPage( + { page: createPage() }, + async (agentForPage: any) => { + getAgentForPage = agentForPage; + }, + createTestInfo(), + ); + + await getAgentForPage(createPage(), { + autoPrintReportMsg: false, + waitAfterAction: 50, + enableTouchEventsInActionSpace: false, + }); + + expect(mockState.ctorOpts).toHaveLength(1); + expect(mockState.ctorOpts[0]).toMatchObject({ + autoPrintReportMsg: false, + waitAfterAction: 50, + enableTouchEventsInActionSpace: false, + }); + }); + + it('should reuse the existing agent instead of recreating it with later overrides', async () => { + const page = createPage(); + const fixture = PlaywrightAiFixture({ + autoPrintReportMsg: true, + }); + + let getAgentForPage: any; + await fixture.agentForPage( + { page }, + async (agentForPage: any) => { + getAgentForPage = agentForPage; + }, + createTestInfo(), + ); + + const firstAgent = await getAgentForPage(page, { + autoPrintReportMsg: false, + }); + const secondAgent = await getAgentForPage(page, { + autoPrintReportMsg: true, + }); + + expect(firstAgent).toBe(secondAgent); + expect(mockState.ctorOpts).toHaveLength(1); + expect(mockState.ctorOpts[0]).toMatchObject({ + autoPrintReportMsg: false, + }); + }); +});