diff --git a/docs/src/api/class-page.md b/docs/src/api/class-page.md index 0300069b02d49..fa84dbaa61078 100644 --- a/docs/src/api/class-page.md +++ b/docs/src/api/class-page.md @@ -710,54 +710,6 @@ current working directory. Raw CSS content to be injected into frame. -## async method: Page.agent -* since: v1.58 -* langs: js -- returns: <[PageAgent]> - -Initialize page agent with the llm provider and cache. - -### option: Page.agent.cache -* since: v1.58 -- `cache` <[Object]> - - `cacheFile` ?<[string]> Cache file to use/generate code for performed actions into. Cache is not used if not specified (default). - - `cacheOutFile` ?<[string]> When specified, generated entries are written into the `cacheOutFile` instead of updating the `cacheFile`. - -### option: Page.agent.expect -* since: v1.58 -- `expect` <[Object]> - - `timeout` ?<[int]> Default timeout for expect calls in milliseconds, defaults to 5000ms. - -### option: Page.agent.limits -* since: v1.58 -- `limits` <[Object]> - - `maxTokens` ?<[int]> Maximum number of tokens to consume. The agentic loop will stop after input + output tokens exceed this value. Defaults to unlimited. - - `maxActions` ?<[int]> Maximum number of agentic actions to generate, defaults to 10. - - `maxActionRetries` ?<[int]> Maximum number retries per action, defaults to 3. - -Limits to use for the agentic loop. - -### option: Page.agent.provider -* since: v1.58 -- `provider` <[Object]> - - `api` <[PageAgentAPI]<"openai"|"openai-compatible"|"anthropic"|"google">> API to use. - - `apiEndpoint` ?<[string]> Endpoint to use if different from default. - - `apiKey` <[string]> API key for the LLM provider. - - `apiTimeout` ?<[int]> Amount of time to wait for the provider to respond to each request. - - `model` <[string]> Model identifier within the provider. Required in non-cache mode. - -### option: Page.agent.secrets -* since: v1.58 -- `secrets` ?<[Object]<[string], [string]>> - -Secrets to hide from the LLM. - -### option: Page.agent.systemPrompt -* since: v1.58 -- `systemPrompt` <[string]> - -System prompt for the agent's loop. - ## async method: Page.bringToFront * since: v1.8 diff --git a/docs/src/api/class-pageagent.md b/docs/src/api/class-pageagent.md deleted file mode 100644 index 0f04e2b04d851..0000000000000 --- a/docs/src/api/class-pageagent.md +++ /dev/null @@ -1,134 +0,0 @@ -# class: PageAgent -* since: v1.58 -* langs: js - -## event: PageAgent.turn -* since: v1.58 -- argument: <[Object]> - - `role` <[string]> - - `message` <[string]> - - `usage` ?<[Object]> - - `inputTokens` <[int]> - - `outputTokens` <[int]> - -Emitted when the agent makes a turn. - -## async method: PageAgent.dispose -* since: v1.58 - -Dispose this agent. - -## async method: PageAgent.expect -* since: v1.58 - -Expect certain condition to be met. - -**Usage** - -```js -await agent.expect('"0 items" to be reported'); -``` - -### param: PageAgent.expect.expectation -* since: v1.58 -- `expectation` <[string]> - -Expectation to assert. - -### option: PageAgent.expect.timeout -* since: v1.58 -- `timeout` <[float]> - -Expect timeout in milliseconds. Defaults to `5000`. The default value can be changed via `expect.timeout` option in the config, or by specifying the `expect` property of the [`option: Page.agent.expect`] option. Pass `0` to disable timeout. - -### option: PageAgent.expect.-inline- = %%-page-agent-call-options-v1.58-%% -* since: v1.58 - -## async method: PageAgent.extract -* since: v1.58 -- returns: <[Object]> - - `result` <[any]> - - `usage` <[Object]> - - `turns` <[int]> - - `inputTokens` <[int]> - - `outputTokens` <[int]> - -Extract information from the page using the agentic loop, return it in a given Zod format. - -**Usage** - -```js -await agent.extract('List of items in the cart', z.object({ - title: z.string().describe('Item title to extract'), - price: z.string().describe('Item price to extract'), -}).array()); -``` - -### param: PageAgent.extract.query -* since: v1.58 -- `query` <[string]> - -Task to perform using agentic loop. - -### param: PageAgent.extract.schema -* since: v1.58 -- `schema` <[z.ZodSchema]> - -### option: PageAgent.extract.timeout -* since: v1.58 -- `timeout` <[float]> - -Extract timeout in milliseconds. Defaults to `5000`. The default value can be changed via `actionTimeout` option in the config, or by using the [`method: BrowserContext.setDefaultTimeout`] or -[`method: Page.setDefaultTimeout`] methods. Pass `0` to disable timeout. - -### option: PageAgent.extract.-inline- = %%-page-agent-call-options-v1.58-%% -* since: v1.58 - - -## async method: PageAgent.perform -* since: v1.58 -- returns: <[Object]> - - `usage` <[Object]> - - `turns` <[int]> - - `inputTokens` <[int]> - - `outputTokens` <[int]> - -Perform action using agentic loop. - -**Usage** - -```js -await agent.perform('Click submit button'); -``` - -### param: PageAgent.perform.task -* since: v1.58 -- `task` <[string]> - -Task to perform using agentic loop. - -### option: PageAgent.perform.timeout -* since: v1.58 -- `timeout` <[float]> - -Perform timeout in milliseconds. Defaults to `5000`. The default value can be changed via `actionTimeout` option in the config, or by using the [`method: BrowserContext.setDefaultTimeout`] or -[`method: Page.setDefaultTimeout`] methods. Pass `0` to disable timeout. - -### option: PageAgent.perform.-inline- = %%-page-agent-call-options-v1.58-%% -* since: v1.58 - -## async method: PageAgent.usage -* since: v1.58 -- returns: <[Object]> - - `turns` <[int]> - - `inputTokens` <[int]> - - `outputTokens` <[int]> - -Returns the current token usage for this agent. - -**Usage** - -```js -const usage = await agent.usage(); -console.log(`Tokens used: ${usage.inputTokens} in, ${usage.outputTokens} out`); -``` diff --git a/docs/src/test-api/class-fixtures.md b/docs/src/test-api/class-fixtures.md index 9ec8e33f92cd3..0c08b19da3b2a 100644 --- a/docs/src/test-api/class-fixtures.md +++ b/docs/src/test-api/class-fixtures.md @@ -18,10 +18,6 @@ Given the test above, Playwright Test will set up the `page` fixture before runn Playwright Test comes with builtin fixtures listed below, and you can add your own fixtures as well. Playwright Test also [provides options][TestOptions] to configure [`property: Fixtures.browser`], [`property: Fixtures.context`] and [`property: Fixtures.page`]. -## property: Fixtures.agent -* since: v1.58 -- type: <[PageAgent]> - ## property: Fixtures.browser * since: v1.10 - type: <[Browser]> diff --git a/docs/src/test-api/class-fullconfig.md b/docs/src/test-api/class-fullconfig.md index 1f3ee92df7750..7ce3da974bb71 100644 --- a/docs/src/test-api/class-fullconfig.md +++ b/docs/src/test-api/class-fullconfig.md @@ -104,15 +104,6 @@ See [`property: TestConfig.reportSlowTests`]. Base directory for all relative paths used in the reporters. -## property: FullConfig.runAgents -* since: v1.58 -- type: <['RunAgentsMode]<"all"|"missing"|"none">> - -Whether to run LLM agent for [PageAgent]: -* "all" disregards existing cache and performs all actions via LLM -* "missing" only performs actions that don't have generated cache actions -* "none" does not talk to LLM at all, relies on the cached actions (default) - ## property: FullConfig.shard * since: v1.10 - type: <[null]|[Object]> diff --git a/docs/src/test-api/class-testconfig.md b/docs/src/test-api/class-testconfig.md index fb642f79d6090..88b059a35a145 100644 --- a/docs/src/test-api/class-testconfig.md +++ b/docs/src/test-api/class-testconfig.md @@ -515,15 +515,6 @@ export default defineConfig({ }); ``` -## property: TestConfig.runAgents -* since: v1.58 -- type: ?<['RunAgentsMode]<"all"|"missing"|"none">> - -Whether to run LLM agent for [PageAgent]: -* "all" disregards existing cache and performs all actions via LLM -* "missing" only performs actions that don't have generated cache actions -* "none" does not talk to LLM at all, relies on the cached actions (default) - ## property: TestConfig.shard * since: v1.10 - type: ?<[null]|[Object]> diff --git a/docs/src/test-api/class-testoptions.md b/docs/src/test-api/class-testoptions.md index 1785e86ee5620..722edbc09ce00 100644 --- a/docs/src/test-api/class-testoptions.md +++ b/docs/src/test-api/class-testoptions.md @@ -46,23 +46,6 @@ export default defineConfig({ }); ``` -## property: TestOptions.agentOptions -* since: v1.58 -- type: <[Object]> - - `provider` <[Object]> - - `api` <[PageAgentAPI]<"openai"|"openai-compatible"|"anthropic"|"google">> API to use. - - `apiEndpoint` ?<[string]> Endpoint to use if different from default. - - `apiKey` <[string]> API key for the LLM provider. - - `apiTimeout` ?<[int]> Amount of time to wait for the provider to respond to each request. - - `model` <[string]> Model identifier within the provider. Required in non-cache mode. - - `cachePathTemplate` ?<[string]> Cache file template to use/generate code for performed actions into. - - `limits` <[Object]> - - `maxTokens` ?<[int]> Maximum number of tokens to consume. The agentic loop will stop after input + output tokens exceed this value. Defaults to unlimited. - - `maxActions` ?<[int]> Maximum number of agentic actions to generate, defaults to 10. - - `maxActionRetries` ?<[int]> Maximum number retries per action, defaults to 3. - - `secrets` ?<[Object]<[string], [string]>> Secrets to hide from the LLM. - - `systemPrompt` <[string]> System prompt for the agent's loop. - ## property: TestOptions.baseURL = %%-context-option-baseURL-%% * since: v1.10 diff --git a/examples/todomvc/tests/fixtures.ts b/examples/todomvc/tests/fixtures.ts index 6226e718bf64f..c58c0055c58a0 100644 --- a/examples/todomvc/tests/fixtures.ts +++ b/examples/todomvc/tests/fixtures.ts @@ -5,14 +5,6 @@ import { test as baseTest } from '@playwright/test'; export { expect } from '@playwright/test'; export const test = baseTest.extend({ - agentOptions: { - provider: { - api: 'anthropic', - apiKey: process.env.AZURE_SONNET_API_KEY!, - apiEndpoint: process.env.AZURE_SONNET_ENDPOINT!, - model: 'claude-sonnet-4-5', - }, - }, page: async ({ page }, use) => { await page.goto('https://demo.playwright.dev/todomvc'); await use(page); diff --git a/examples/todomvc/tests/md/adding-todos.spec.md b/examples/todomvc/tests/md/adding-todos.spec.md deleted file mode 100644 index d8aa804fc5d98..0000000000000 --- a/examples/todomvc/tests/md/adding-todos.spec.md +++ /dev/null @@ -1,32 +0,0 @@ -## Adding Todos - -- seed: ./seed.spec.md - -### should add single todo - -- tag: @one @two -- tag: @three -- annotation: link=https://playwright.dev -- annotation: link2=https://demo.playwright.dev - -* Type 'Buy groceries' into the input field -* expect: The text appears in the input field -- Press Enter to submit the todo -- group: Verify todo is added to the list - - expect: The new todo 'Buy groceries' appears in the todo list - - expect: The input field is cleared - - expect: The todo counter shows '1 item left' - -### should add multiple todos - -1. Add first todo 'Buy milk' - - expect: The todo appears in the list - - expect: Counter shows '1 item left' -2. Add second todo 'Walk the dog' - - expect: Both todos appear in the list - - expect: Counter shows '2 items left' -3. // this is a comment -4. Add third todo 'Finish report' - - expect: All three todos appear in the list - - // this is a comment - - expect: Counter shows '3 items left' diff --git a/examples/todomvc/tests/md/adding-todos.spec.md-cache.json b/examples/todomvc/tests/md/adding-todos.spec.md-cache.json deleted file mode 100644 index c2cc9b18a0cb7..0000000000000 --- a/examples/todomvc/tests/md/adding-todos.spec.md-cache.json +++ /dev/null @@ -1,160 +0,0 @@ -{ - "page title contains \"TodoMVC\"": { - "actions": [] - }, - "Press Enter to submit the todo": { - "actions": [ - { - "method": "pressKey", - "key": "Enter", - "code": "await page.keyboard.press('Enter');" - } - ] - }, - "The input field 'What needs to be done?' is visible": { - "actions": [ - { - "method": "expectVisible", - "selector": "internal:role=textbox[name=\"What needs to be done?\"i]", - "code": "await expect(page.getByRole('textbox', { name: 'What needs to be done?' })).toBeVisible();" - } - ] - }, - "The input field is cleared": { - "actions": [ - { - "method": "expectValue", - "selector": "internal:role=textbox[name=\"What needs to be done?\"i]", - "type": "textbox", - "value": "", - "code": "await expect(page.getByRole('textbox', { name: 'What needs to be done?' })).toHaveValue('');" - } - ] - }, - "The new todo 'Buy groceries' appears in the todo list": { - "actions": [ - { - "method": "expectVisible", - "selector": "internal:text=\"Buy groceries\"i", - "code": "await expect(page.getByText('Buy groceries')).toBeVisible();" - } - ] - }, - "The text appears in the input field": { - "actions": [ - { - "method": "expectValue", - "selector": "internal:role=textbox[name=\"What needs to be done?\"i]", - "type": "textbox", - "value": "Buy groceries", - "code": "await expect(page.getByRole('textbox', { name: 'What needs to be done?' })).toHaveValue('Buy groceries');" - } - ] - }, - "The todo counter shows '1 item left'": { - "actions": [ - { - "method": "expectVisible", - "selector": "internal:text=\"1 item left\"i", - "code": "await expect(page.getByText('1 item left')).toBeVisible();" - } - ] - }, - "Type 'Buy groceries' into the input field": { - "actions": [ - { - "method": "fill", - "selector": "internal:role=textbox[name=\"What needs to be done?\"i]", - "text": "Buy groceries", - "code": "await page.getByRole('textbox', { name: 'What needs to be done?' }).fill('Buy groceries');" - } - ] - }, - "Add first todo 'Buy milk'": { - "actions": [ - { - "method": "fill", - "selector": "internal:role=textbox[name=\"What needs to be done?\"i]", - "text": "Buy milk", - "submit": true, - "code": "await page.getByRole('textbox', { name: 'What needs to be done?' }).fill('Buy milk');\nawait page.keyboard.press('Enter');" - } - ] - }, - "Add second todo 'Walk the dog'": { - "actions": [ - { - "method": "fill", - "selector": "internal:role=textbox[name=\"What needs to be done?\"i]", - "text": "Walk the dog", - "submit": true, - "code": "await page.getByRole('textbox', { name: 'What needs to be done?' }).fill('Walk the dog');\nawait page.keyboard.press('Enter');" - } - ] - }, - "Add third todo 'Finish report'": { - "actions": [ - { - "method": "fill", - "selector": "internal:role=textbox[name=\"What needs to be done?\"i]", - "text": "Finish report", - "submit": true, - "code": "await page.getByRole('textbox', { name: 'What needs to be done?' }).fill('Finish report');\nawait page.keyboard.press('Enter');" - } - ] - }, - "All three todos appear in the list": { - "actions": [ - { - "method": "expectAria", - "template": "- list:\n - listitem: Buy milk\n - listitem: Walk the dog\n - listitem: Finish report", - "code": "await expect(page.locator('body')).toMatchAria(`\n- list:\n - listitem: Buy milk\n - listitem: Walk the dog\n - listitem: Finish report\n`);" - } - ] - }, - "Both todos appear in the list": { - "actions": [ - { - "method": "expectAria", - "template": "- list:\n - listitem: Buy milk\n - listitem: Walk the dog", - "code": "await expect(page.locator('body')).toMatchAria(`\n- list:\n - listitem: Buy milk\n - listitem: Walk the dog\n`);" - } - ] - }, - "Counter shows '1 item left'": { - "actions": [ - { - "method": "expectVisible", - "selector": "internal:text=\"1 item left\"i", - "code": "await expect(page.getByText('1 item left')).toBeVisible();" - } - ] - }, - "Counter shows '2 items left'": { - "actions": [ - { - "method": "expectVisible", - "selector": "internal:text=\"2 items left\"i", - "code": "await expect(page.getByText('2 items left')).toBeVisible();" - } - ] - }, - "Counter shows '3 items left'": { - "actions": [ - { - "method": "expectVisible", - "selector": "internal:text=\"3 items left\"i", - "code": "await expect(page.getByText('3 items left')).toBeVisible();" - } - ] - }, - "The todo appears in the list": { - "actions": [ - { - "method": "expectVisible", - "selector": "internal:text=\"Buy milk\"i", - "code": "await expect(page.getByText('Buy milk')).toBeVisible();" - } - ] - } -} \ No newline at end of file diff --git a/examples/todomvc/tests/md/seed.spec.md b/examples/todomvc/tests/md/seed.spec.md deleted file mode 100644 index 60b2be1c674f7..0000000000000 --- a/examples/todomvc/tests/md/seed.spec.md +++ /dev/null @@ -1,14 +0,0 @@ -## Seed - -- fixtures: ../fixtures - -### seed test - -- Navigate to 'https://demo.playwright.dev/todomvc' - ```ts - await page.goto('https://demo.playwright.dev/todomvc'); - ``` - -- expect: page title contains "TodoMVC" - -- expect: The input field 'What needs to be done?' is visible diff --git a/examples/todomvc/tests/md/seed.spec.md-cache.json b/examples/todomvc/tests/md/seed.spec.md-cache.json deleted file mode 100644 index 333dfbf16d630..0000000000000 --- a/examples/todomvc/tests/md/seed.spec.md-cache.json +++ /dev/null @@ -1,14 +0,0 @@ -{ - "page title contains \"TodoMVC\"": { - "actions": [] - }, - "The input field 'What needs to be done?' is visible": { - "actions": [ - { - "method": "expectVisible", - "selector": "internal:role=textbox[name=\"What needs to be done?\"i]", - "code": "await expect(page.getByRole('textbox', { name: 'What needs to be done?' })).toBeVisible();" - } - ] - } -} \ No newline at end of file diff --git a/examples/todomvc/tests/perform/completing-todos/should-complete-multiple-todos.spec.ts b/examples/todomvc/tests/perform/completing-todos/should-complete-multiple-todos.spec.ts deleted file mode 100644 index 18e0476f02da7..0000000000000 --- a/examples/todomvc/tests/perform/completing-todos/should-complete-multiple-todos.spec.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { test } from '../../fixtures'; - -test('should complete multiple todos', async ({ agent }) => { - await agent.expect(`The page loads with an empty todo list`); - - await agent.perform(`Add three todos: 'Buy milk', 'Walk dog', 'Finish report'`); - await agent.expect(`All three todos are visible`); - await agent.expect(`Counter shows '3 items left'`); - - await agent.perform(`Complete the first todo by clicking its checkbox`); - await agent.expect(`First todo is marked as complete`); - await agent.expect(`Counter shows '2 items left'`); - - await agent.perform(`Complete the third todo by clicking its checkbox`); - await agent.expect(`Third todo is marked as complete`); - await agent.expect(`Counter shows '1 item left'`); - await agent.expect(`The 'Clear completed' button appears`); -}); diff --git a/examples/todomvc/tests/perform/completing-todos/should-complete-multiple-todos.spec.ts-cache.json b/examples/todomvc/tests/perform/completing-todos/should-complete-multiple-todos.spec.ts-cache.json deleted file mode 100644 index 1318880339e58..0000000000000 --- a/examples/todomvc/tests/perform/completing-todos/should-complete-multiple-todos.spec.ts-cache.json +++ /dev/null @@ -1,121 +0,0 @@ -{ - "Add three todos: 'Buy milk', 'Walk dog', 'Finish report'": { - "actions": [ - { - "method": "fill", - "selector": "internal:role=textbox[name=\"What needs to be done?\"i]", - "text": "Buy milk", - "submit": true, - "code": "await page.getByRole('textbox', { name: 'What needs to be done?' }).fill('Buy milk');\nawait page.keyboard.press('Enter');" - }, - { - "method": "fill", - "selector": "internal:role=textbox[name=\"What needs to be done?\"i]", - "text": "Walk dog", - "submit": true, - "code": "await page.getByRole('textbox', { name: 'What needs to be done?' }).fill('Walk dog');\nawait page.keyboard.press('Enter');" - }, - { - "method": "fill", - "selector": "internal:role=textbox[name=\"What needs to be done?\"i]", - "text": "Finish report", - "submit": true, - "code": "await page.getByRole('textbox', { name: 'What needs to be done?' }).fill('Finish report');\nawait page.keyboard.press('Enter');" - } - ] - }, - "All three todos are visible": { - "actions": [ - { - "method": "expectAria", - "template": "- list:\n - listitem: Buy milk\n - listitem: Walk dog\n - listitem: Finish report", - "code": "await expect(page.locator('body')).toMatchAria(`\n- list:\n - listitem: Buy milk\n - listitem: Walk dog\n - listitem: Finish report\n`);" - } - ] - }, - "Complete the first todo by clicking its checkbox": { - "actions": [ - { - "method": "click", - "selector": "internal:role=listitem >> internal:has-text=\"Buy milk\"i >> internal:label=\"Toggle Todo\"i", - "code": "await page.getByRole('listitem').filter({ hasText: 'Buy milk' }).getByLabel('Toggle Todo').click();" - } - ] - }, - "Complete the third todo by clicking its checkbox": { - "actions": [ - { - "method": "click", - "selector": "internal:role=listitem >> internal:has-text=\"Finish report\"i >> internal:label=\"Toggle Todo\"i", - "code": "await page.getByRole('listitem').filter({ hasText: 'Finish report' }).getByLabel('Toggle Todo').click();" - } - ] - }, - "Counter shows '1 item left'": { - "actions": [ - { - "method": "expectVisible", - "selector": "internal:text=\"item left\"i", - "code": "await expect(page.getByText('item left')).toBeVisible();" - } - ] - }, - "Counter shows '2 items left'": { - "actions": [ - { - "method": "expectVisible", - "selector": "internal:text=\"2 items left\"i", - "code": "await expect(page.getByText('2 items left')).toBeVisible();" - } - ] - }, - "Counter shows '3 items left'": { - "actions": [ - { - "method": "expectVisible", - "selector": "internal:text=\"3 items left\"i", - "code": "await expect(page.getByText('3 items left')).toBeVisible();" - } - ] - }, - "First todo is marked as complete": { - "actions": [ - { - "method": "expectValue", - "selector": "internal:role=listitem >> internal:has-text=\"Buy milk\"i >> internal:label=\"Toggle Todo\"i", - "type": "checkbox", - "value": "true", - "code": "await expect(page.getByRole('listitem').filter({ hasText: 'Buy milk' }).getByLabel('Toggle Todo')).toBeChecked({ checked: true });" - } - ] - }, - "The 'Clear completed' button appears": { - "actions": [ - { - "method": "expectVisible", - "selector": "internal:role=button[name=\"Clear completed\"i]", - "code": "await expect(page.getByRole('button', { name: 'Clear completed' })).toBeVisible();" - } - ] - }, - "The page loads with an empty todo list": { - "actions": [ - { - "method": "expectVisible", - "selector": "internal:role=textbox[name=\"What needs to be done?\"i]", - "code": "await expect(page.getByRole('textbox', { name: 'What needs to be done?' })).toBeVisible();" - } - ] - }, - "Third todo is marked as complete": { - "actions": [ - { - "method": "expectValue", - "selector": "internal:role=listitem >> internal:has-text=\"Finish report\"i >> internal:label=\"Toggle Todo\"i", - "type": "checkbox", - "value": "true", - "code": "await expect(page.getByRole('listitem').filter({ hasText: 'Finish report' }).getByLabel('Toggle Todo')).toBeChecked({ checked: true });" - } - ] - } -} \ No newline at end of file diff --git a/examples/todomvc/tests/perform/completing-todos/should-complete-single-todo.spec.ts b/examples/todomvc/tests/perform/completing-todos/should-complete-single-todo.spec.ts deleted file mode 100644 index 2b8494a1d42c9..0000000000000 --- a/examples/todomvc/tests/perform/completing-todos/should-complete-single-todo.spec.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { test } from '../../fixtures'; - -test('should complete single todo', async ({ agent }) => { - await agent.expect(`The page loads with an empty todo list`); - - await agent.perform(`Add a todo 'Buy groceries'`); - await agent.expect(`The todo appears as active`); - await agent.expect(`Counter shows '1 item left'`); - - await agent.perform(`Click the checkbox next to the todo`); - await agent.expect(`The checkbox is checked`); - await agent.expect(`Counter shows '0 items left'`); - await agent.expect(`The 'Clear completed' button appears in the footer`); -}); diff --git a/examples/todomvc/tests/perform/completing-todos/should-complete-single-todo.spec.ts-cache.json b/examples/todomvc/tests/perform/completing-todos/should-complete-single-todo.spec.ts-cache.json deleted file mode 100644 index 4263ae994422f..0000000000000 --- a/examples/todomvc/tests/perform/completing-todos/should-complete-single-todo.spec.ts-cache.json +++ /dev/null @@ -1,78 +0,0 @@ -{ - "Add a todo 'Buy groceries'": { - "actions": [ - { - "method": "fill", - "selector": "internal:role=textbox[name=\"What needs to be done?\"i]", - "text": "Buy groceries", - "submit": true, - "code": "await page.getByRole('textbox', { name: 'What needs to be done?' }).fill('Buy groceries');\nawait page.keyboard.press('Enter');" - } - ] - }, - "Click the checkbox next to the todo": { - "actions": [ - { - "method": "click", - "selector": "internal:role=checkbox[name=\"Toggle Todo\"i]", - "code": "await page.getByRole('checkbox', { name: 'Toggle Todo' }).click();" - } - ] - }, - "Counter shows '0 items left'": { - "actions": [ - { - "method": "expectVisible", - "selector": "internal:text=\"0 items left\"i", - "code": "await expect(page.getByText('0 items left')).toBeVisible();" - } - ] - }, - "Counter shows '1 item left'": { - "actions": [ - { - "method": "expectVisible", - "selector": "internal:text=\"1 item left\"i", - "code": "await expect(page.getByText('1 item left')).toBeVisible();" - } - ] - }, - "The 'Clear completed' button appears in the footer": { - "actions": [ - { - "method": "expectVisible", - "selector": "internal:role=button[name=\"Clear completed\"i]", - "code": "await expect(page.getByRole('button', { name: 'Clear completed' })).toBeVisible();" - } - ] - }, - "The checkbox is checked": { - "actions": [ - { - "method": "expectValue", - "selector": "internal:role=checkbox[name=\"Toggle Todo\"i]", - "type": "checkbox", - "value": "true", - "code": "await expect(page.getByRole('checkbox', { name: 'Toggle Todo' })).toBeChecked({ checked: true });" - } - ] - }, - "The page loads with an empty todo list": { - "actions": [ - { - "method": "expectVisible", - "selector": "internal:role=textbox[name=\"What needs to be done?\"i]", - "code": "await expect(page.getByRole('textbox', { name: 'What needs to be done?' })).toBeVisible();" - } - ] - }, - "The todo appears as active": { - "actions": [ - { - "method": "expectVisible", - "selector": "internal:role=generic[name=\"Buy groceries\"i]", - "code": "await expect(page.getByRole('generic', { name: 'Buy groceries' })).toBeVisible();" - } - ] - } -} \ No newline at end of file diff --git a/examples/todomvc/tests/perform/completing-todos/should-toggle-all-todos-complete.spec.ts b/examples/todomvc/tests/perform/completing-todos/should-toggle-all-todos-complete.spec.ts deleted file mode 100644 index 0f3b6e540a304..0000000000000 --- a/examples/todomvc/tests/perform/completing-todos/should-toggle-all-todos-complete.spec.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { test } from '../../fixtures'; - -test('should toggle all todos complete', async ({ agent }) => { - await agent.expect(`The page loads with an empty todo list`); - - await agent.perform(`Add three todos: 'Task 1', 'Task 2', 'Task 3'`); - await agent.expect(`All three todos are visible and active`); - await agent.expect(`Counter shows '3 items left'`); - - await agent.perform(`Click the 'Mark all as complete' checkbox`); - await agent.expect(`All three todos are marked as complete`); - await agent.expect(`All checkboxes are checked`); - await agent.expect(`Counter shows '0 items left'`); - await agent.expect(`The 'Clear completed' button appears`); -}); diff --git a/examples/todomvc/tests/perform/completing-todos/should-toggle-all-todos-complete.spec.ts-cache.json b/examples/todomvc/tests/perform/completing-todos/should-toggle-all-todos-complete.spec.ts-cache.json deleted file mode 100644 index 3e45999276634..0000000000000 --- a/examples/todomvc/tests/perform/completing-todos/should-toggle-all-todos-complete.spec.ts-cache.json +++ /dev/null @@ -1,101 +0,0 @@ -{ - "Add three todos: 'Task 1', 'Task 2', 'Task 3'": { - "actions": [ - { - "method": "fill", - "selector": "internal:role=textbox[name=\"What needs to be done?\"i]", - "text": "Task 1", - "submit": true, - "code": "await page.getByRole('textbox', { name: 'What needs to be done?' }).fill('Task 1');\nawait page.keyboard.press('Enter');" - }, - { - "method": "fill", - "selector": "internal:role=textbox[name=\"What needs to be done?\"i]", - "text": "Task 2", - "submit": true, - "code": "await page.getByRole('textbox', { name: 'What needs to be done?' }).fill('Task 2');\nawait page.keyboard.press('Enter');" - }, - { - "method": "fill", - "selector": "internal:role=textbox[name=\"What needs to be done?\"i]", - "text": "Task 3", - "submit": true, - "code": "await page.getByRole('textbox', { name: 'What needs to be done?' }).fill('Task 3');\nawait page.keyboard.press('Enter');" - } - ] - }, - "All checkboxes are checked": { - "actions": [ - { - "method": "expectVisible", - "selector": "internal:role=checkbox[name=\"❯Mark all as complete\"i]", - "code": "await expect(page.getByRole('checkbox', { name: '❯Mark all as complete' })).toBeVisible();" - } - ] - }, - "All three todos are marked as complete": { - "actions": [ - { - "method": "expectValue", - "selector": "internal:role=listitem >> internal:has-text=\"Task 1\"i >> internal:label=\"Toggle Todo\"i", - "type": "checkbox", - "value": "true", - "code": "await expect(page.getByRole('listitem').filter({ hasText: 'Task 1' }).getByLabel('Toggle Todo')).toBeChecked({ checked: true });" - } - ] - }, - "All three todos are visible and active": { - "actions": [ - { - "method": "expectAria", - "template": "- list:\n - listitem: Task 1\n - listitem: Task 2\n - listitem: Task 3", - "code": "await expect(page.locator('body')).toMatchAria(`\n- list:\n - listitem: Task 1\n - listitem: Task 2\n - listitem: Task 3\n`);" - } - ] - }, - "Click the 'Mark all as complete' checkbox": { - "actions": [ - { - "method": "click", - "selector": "internal:role=checkbox[name=\"❯Mark all as complete\"i]", - "code": "await page.getByRole('checkbox', { name: '❯Mark all as complete' }).click();" - } - ] - }, - "Counter shows '0 items left'": { - "actions": [ - { - "method": "expectVisible", - "selector": "internal:text=\"0 items left\"i", - "code": "await expect(page.getByText('0 items left')).toBeVisible();" - } - ] - }, - "Counter shows '3 items left'": { - "actions": [ - { - "method": "expectVisible", - "selector": "internal:text=\"3 items left\"i", - "code": "await expect(page.getByText('3 items left')).toBeVisible();" - } - ] - }, - "The 'Clear completed' button appears": { - "actions": [ - { - "method": "expectVisible", - "selector": "internal:role=button[name=\"Clear completed\"i]", - "code": "await expect(page.getByRole('button', { name: 'Clear completed' })).toBeVisible();" - } - ] - }, - "The page loads with an empty todo list": { - "actions": [ - { - "method": "expectVisible", - "selector": "internal:role=textbox[name=\"What needs to be done?\"i]", - "code": "await expect(page.getByRole('textbox', { name: 'What needs to be done?' })).toBeVisible();" - } - ] - } -} \ No newline at end of file diff --git a/examples/todomvc/tests/perform/completing-todos/should-toggle-all-todos-incomplete.spec.ts b/examples/todomvc/tests/perform/completing-todos/should-toggle-all-todos-incomplete.spec.ts deleted file mode 100644 index 63a56828f1eff..0000000000000 --- a/examples/todomvc/tests/perform/completing-todos/should-toggle-all-todos-incomplete.spec.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { test } from '../../fixtures'; - -test('should toggle all todos incomplete', async ({ agent }) => { - await agent.expect(`The page loads with an empty todo list`); - - await agent.perform(`Add three todos: 'Task 1', 'Task 2', 'Task 3' and mark all as complete using the toggle all checkbox`); - await agent.expect(`All todos are marked as complete`); - await agent.expect(`Counter shows '0 items left'`); - - await agent.perform(`Click the 'Mark all as complete' checkbox again`); - await agent.expect(`All todos are marked as active`); - await agent.expect(`All checkboxes are unchecked`); - await agent.expect(`Counter shows '3 items left'`); - await agent.expect(`The 'Clear completed' button disappears`); -}); diff --git a/examples/todomvc/tests/perform/completing-todos/should-toggle-all-todos-incomplete.spec.ts-cache.json b/examples/todomvc/tests/perform/completing-todos/should-toggle-all-todos-incomplete.spec.ts-cache.json deleted file mode 100644 index 876cff3f56034..0000000000000 --- a/examples/todomvc/tests/perform/completing-todos/should-toggle-all-todos-incomplete.spec.ts-cache.json +++ /dev/null @@ -1,141 +0,0 @@ -{ - "Add three todos: 'Task 1', 'Task 2', 'Task 3' and mark all as complete using the toggle all checkbox": { - "actions": [ - { - "method": "fill", - "selector": "internal:role=textbox[name=\"What needs to be done?\"i]", - "text": "Task 1", - "submit": true, - "code": "await page.getByRole('textbox', { name: 'What needs to be done?' }).fill('Task 1');\nawait page.keyboard.press('Enter');" - }, - { - "method": "fill", - "selector": "internal:role=textbox[name=\"What needs to be done?\"i]", - "text": "Task 2", - "submit": true, - "code": "await page.getByRole('textbox', { name: 'What needs to be done?' }).fill('Task 2');\nawait page.keyboard.press('Enter');" - }, - { - "method": "fill", - "selector": "internal:role=textbox[name=\"What needs to be done?\"i]", - "text": "Task 3", - "submit": true, - "code": "await page.getByRole('textbox', { name: 'What needs to be done?' }).fill('Task 3');\nawait page.keyboard.press('Enter');" - }, - { - "method": "setChecked", - "selector": "internal:role=checkbox[name=\"❯Mark all as complete\"i]", - "checked": true, - "code": "await page.getByRole('checkbox', { name: '❯Mark all as complete' }).check();" - } - ] - }, - "All checkboxes are unchecked": { - "actions": [ - { - "method": "expectValue", - "selector": "internal:role=listitem >> internal:has-text=\"Task 1\"i >> internal:label=\"Toggle Todo\"i", - "type": "checkbox", - "value": "false", - "code": "await expect(page.getByRole('listitem').filter({ hasText: 'Task 1' }).getByLabel('Toggle Todo')).toBeChecked({ checked: false });" - }, - { - "method": "expectValue", - "selector": "internal:role=listitem >> internal:has-text=\"Task 2\"i >> internal:label=\"Toggle Todo\"i", - "type": "checkbox", - "value": "false", - "code": "await expect(page.getByRole('listitem').filter({ hasText: 'Task 2' }).getByLabel('Toggle Todo')).toBeChecked({ checked: false });" - }, - { - "method": "expectValue", - "selector": "internal:role=listitem >> internal:has-text=\"Task 3\"i >> internal:label=\"Toggle Todo\"i", - "type": "checkbox", - "value": "false", - "code": "await expect(page.getByRole('listitem').filter({ hasText: 'Task 3' }).getByLabel('Toggle Todo')).toBeChecked({ checked: false });" - } - ] - }, - "All todos are marked as active": { - "actions": [ - { - "method": "expectValue", - "selector": "internal:role=listitem >> internal:has-text=\"Task 1\"i >> internal:label=\"Toggle Todo\"i", - "type": "checkbox", - "value": "false", - "isNot": false, - "code": "await expect(page.getByRole('listitem').filter({ hasText: 'Task 1' }).getByLabel('Toggle Todo')).toBeChecked({ checked: false });" - } - ] - }, - "All todos are marked as complete": { - "actions": [ - { - "method": "expectValue", - "selector": "internal:role=listitem >> internal:has-text=\"Task 1\"i >> internal:label=\"Toggle Todo\"i", - "type": "checkbox", - "value": "true", - "code": "await expect(page.getByRole('listitem').filter({ hasText: 'Task 1' }).getByLabel('Toggle Todo')).toBeChecked({ checked: true });" - }, - { - "method": "expectValue", - "selector": "internal:role=listitem >> internal:has-text=\"Task 2\"i >> internal:label=\"Toggle Todo\"i", - "type": "checkbox", - "value": "true", - "code": "await expect(page.getByRole('listitem').filter({ hasText: 'Task 2' }).getByLabel('Toggle Todo')).toBeChecked({ checked: true });" - }, - { - "method": "expectValue", - "selector": "internal:role=listitem >> internal:has-text=\"Task 3\"i >> internal:label=\"Toggle Todo\"i", - "type": "checkbox", - "value": "true", - "code": "await expect(page.getByRole('listitem').filter({ hasText: 'Task 3' }).getByLabel('Toggle Todo')).toBeChecked({ checked: true });" - } - ] - }, - "Click the 'Mark all as complete' checkbox again": { - "actions": [ - { - "method": "click", - "selector": "internal:role=checkbox[name=\"❯Mark all as complete\"i]", - "code": "await page.getByRole('checkbox', { name: '❯Mark all as complete' }).click();" - } - ] - }, - "Counter shows '0 items left'": { - "actions": [ - { - "method": "expectVisible", - "selector": "internal:text=\"0 items left\"i", - "code": "await expect(page.getByText('0 items left')).toBeVisible();" - } - ] - }, - "Counter shows '3 items left'": { - "actions": [ - { - "method": "expectVisible", - "selector": "internal:text=\"3 items left\"i", - "code": "await expect(page.getByText('3 items left')).toBeVisible();" - } - ] - }, - "The 'Clear completed' button disappears": { - "actions": [ - { - "method": "expectVisible", - "selector": "internal:role=button[name=\"Clear completed\"i]", - "isNot": true, - "code": "await expect(page.getByRole('button', { name: 'Clear completed' })).not.toBeVisible();" - } - ] - }, - "The page loads with an empty todo list": { - "actions": [ - { - "method": "expectVisible", - "selector": "internal:role=textbox[name=\"What needs to be done?\"i]", - "code": "await expect(page.getByRole('textbox', { name: 'What needs to be done?' })).toBeVisible();" - } - ] - } -} \ No newline at end of file diff --git a/examples/todomvc/tests/perform/completing-todos/should-uncomplete-completed-todo.spec.ts b/examples/todomvc/tests/perform/completing-todos/should-uncomplete-completed-todo.spec.ts deleted file mode 100644 index 96ef73e2e7b65..0000000000000 --- a/examples/todomvc/tests/perform/completing-todos/should-uncomplete-completed-todo.spec.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { test } from '../../fixtures'; - -test('should uncomplete completed todo', async ({ agent }) => { - await agent.expect(`The page loads with an empty todo list`); - - await agent.perform(`Add a todo 'Buy groceries' and mark it as complete by clicking its checkbox`); - await agent.expect(`The todo is marked as complete`); - await agent.expect(`Counter shows '0 items left'`); - - await agent.perform(`Click the checkbox again to uncomplete it`); - await agent.expect(`The checkbox is unchecked`); - await agent.expect(`Counter shows '1 item left'`); - await agent.expect(`The 'Clear completed' button disappears`); -}); diff --git a/examples/todomvc/tests/perform/completing-todos/should-uncomplete-completed-todo.spec.ts-cache.json b/examples/todomvc/tests/perform/completing-todos/should-uncomplete-completed-todo.spec.ts-cache.json deleted file mode 100644 index bad79f7e96f30..0000000000000 --- a/examples/todomvc/tests/perform/completing-todos/should-uncomplete-completed-todo.spec.ts-cache.json +++ /dev/null @@ -1,86 +0,0 @@ -{ - "Add a todo 'Buy groceries' and mark it as complete by clicking its checkbox": { - "actions": [ - { - "method": "fill", - "selector": "internal:role=textbox[name=\"What needs to be done?\"i]", - "text": "Buy groceries", - "submit": true, - "code": "await page.getByRole('textbox', { name: 'What needs to be done?' }).fill('Buy groceries');\nawait page.keyboard.press('Enter');" - }, - { - "method": "click", - "selector": "internal:role=checkbox[name=\"Toggle Todo\"i]", - "code": "await page.getByRole('checkbox', { name: 'Toggle Todo' }).click();" - } - ] - }, - "Click the checkbox again to uncomplete it": { - "actions": [ - { - "method": "click", - "selector": "internal:role=checkbox[name=\"Toggle Todo\"i]", - "code": "await page.getByRole('checkbox', { name: 'Toggle Todo' }).click();" - } - ] - }, - "Counter shows '0 items left'": { - "actions": [ - { - "method": "expectVisible", - "selector": "internal:text=\"0 items left\"i", - "code": "await expect(page.getByText('0 items left')).toBeVisible();" - } - ] - }, - "Counter shows '1 item left'": { - "actions": [ - { - "method": "expectVisible", - "selector": "internal:text=\"1 item left\"i", - "code": "await expect(page.getByText('1 item left')).toBeVisible();" - } - ] - }, - "The 'Clear completed' button disappears": { - "actions": [ - { - "method": "expectVisible", - "selector": "internal:role=button[name=\"Clear completed\"i]", - "isNot": true, - "code": "await expect(page.getByRole('button', { name: 'Clear completed' })).not.toBeVisible();" - } - ] - }, - "The checkbox is unchecked": { - "actions": [ - { - "method": "expectValue", - "selector": "internal:role=checkbox[name=\"Toggle Todo\"i]", - "type": "checkbox", - "value": "false", - "code": "await expect(page.getByRole('checkbox', { name: 'Toggle Todo' })).toBeChecked({ checked: false });" - } - ] - }, - "The page loads with an empty todo list": { - "actions": [ - { - "method": "expectVisible", - "selector": "internal:text=\"What needs to be done?\"i", - "code": "await expect(page.getByText('What needs to be done?')).toBeVisible();" - } - ] - }, - "The todo is marked as complete": { - "actions": [ - { - "method": "expectValue", - "selector": "internal:role=checkbox[name=\"Toggle Todo\"i]", - "type": "checkbox", - "value": "true", - "code": "await expect(page.getByRole('checkbox', { name: 'Toggle Todo' })).toBeChecked({ checked: true });" - } - ] - } -} \ No newline at end of file diff --git a/examples/todomvc/tests/perform/deleting-todos/should-clear-all-completed-todos.spec.ts b/examples/todomvc/tests/perform/deleting-todos/should-clear-all-completed-todos.spec.ts deleted file mode 100644 index 0491f03ce8bca..0000000000000 --- a/examples/todomvc/tests/perform/deleting-todos/should-clear-all-completed-todos.spec.ts +++ /dev/null @@ -1,19 +0,0 @@ -import { test } from '../../fixtures'; - -test('should clear all completed todos', async ({ agent }) => { - await agent.expect(`The page loads with an empty todo list`); - - await agent.perform(`Add three todos: 'Task 1', 'Task 2', 'Task 3'`); - await agent.expect(`All three todos are visible`); - - await agent.perform(`Mark 'Task 1' and 'Task 3' as complete by clicking their checkboxes`); - await agent.expect(`Two todos are marked as complete`); - await agent.expect(`Counter shows '1 item left'`); - await agent.expect(`The 'Clear completed' button appears`); - - await agent.perform(`Click the 'Clear completed' button`); - await agent.expect(`'Task 1' and 'Task 3' are removed from the list`); - await agent.expect(`Only 'Task 2' remains visible`); - await agent.expect(`Counter shows '1 item left'`); - await agent.expect(`The 'Clear completed' button disappears`); -}); diff --git a/examples/todomvc/tests/perform/deleting-todos/should-clear-all-completed-todos.spec.ts-cache.json b/examples/todomvc/tests/perform/deleting-todos/should-clear-all-completed-todos.spec.ts-cache.json deleted file mode 100644 index 687b9196bb874..0000000000000 --- a/examples/todomvc/tests/perform/deleting-todos/should-clear-all-completed-todos.spec.ts-cache.json +++ /dev/null @@ -1,132 +0,0 @@ -{ - "'Task 1' and 'Task 3' are removed from the list": { - "actions": [ - { - "method": "expectAria", - "template": "- list:\n - listitem: Task 2", - "code": "await expect(page.locator('body')).toMatchAria(`\n- list:\n - listitem: Task 2\n`);" - } - ] - }, - "Add three todos: 'Task 1', 'Task 2', 'Task 3'": { - "actions": [ - { - "method": "fill", - "selector": "internal:role=textbox[name=\"What needs to be done?\"i]", - "text": "Task 1", - "submit": true, - "code": "await page.getByRole('textbox', { name: 'What needs to be done?' }).fill('Task 1');\nawait page.keyboard.press('Enter');" - }, - { - "method": "fill", - "selector": "internal:role=textbox[name=\"What needs to be done?\"i]", - "text": "Task 2", - "submit": true, - "code": "await page.getByRole('textbox', { name: 'What needs to be done?' }).fill('Task 2');\nawait page.keyboard.press('Enter');" - }, - { - "method": "fill", - "selector": "internal:role=textbox[name=\"What needs to be done?\"i]", - "text": "Task 3", - "submit": true, - "code": "await page.getByRole('textbox', { name: 'What needs to be done?' }).fill('Task 3');\nawait page.keyboard.press('Enter');" - } - ] - }, - "All three todos are visible": { - "actions": [ - { - "method": "expectAria", - "template": "- list:\n - listitem: Task 1\n - listitem: Task 2\n - listitem: Task 3", - "code": "await expect(page.locator('body')).toMatchAria(`\n- list:\n - listitem: Task 1\n - listitem: Task 2\n - listitem: Task 3\n`);" - } - ] - }, - "Click the 'Clear completed' button": { - "actions": [ - { - "method": "click", - "selector": "internal:role=button[name=\"Clear completed\"i]", - "code": "await page.getByRole('button', { name: 'Clear completed' }).click();" - } - ] - }, - "Counter shows '1 item left'": { - "actions": [ - { - "method": "expectVisible", - "selector": "internal:text=\"1 item left\"i", - "code": "await expect(page.getByText('1 item left')).toBeVisible();" - } - ] - }, - "Mark 'Task 1' and 'Task 3' as complete by clicking their checkboxes": { - "actions": [ - { - "method": "click", - "selector": "internal:role=listitem >> internal:has-text=\"Task 1\"i >> internal:label=\"Toggle Todo\"i", - "code": "await page.getByRole('listitem').filter({ hasText: 'Task 1' }).getByLabel('Toggle Todo').click();" - }, - { - "method": "click", - "selector": "internal:role=listitem >> internal:has-text=\"Task 3\"i >> internal:label=\"Toggle Todo\"i", - "code": "await page.getByRole('listitem').filter({ hasText: 'Task 3' }).getByLabel('Toggle Todo').click();" - } - ] - }, - "Only 'Task 2' remains visible": { - "actions": [ - { - "method": "expectAria", - "template": "- list:\n - listitem: Task 2", - "code": "await expect(page.locator('body')).toMatchAria(`\n- list:\n - listitem: Task 2\n`);" - } - ] - }, - "The 'Clear completed' button appears": { - "actions": [ - { - "method": "expectVisible", - "selector": "internal:role=button[name=\"Clear completed\"i]", - "code": "await expect(page.getByRole('button', { name: 'Clear completed' })).toBeVisible();" - } - ] - }, - "The 'Clear completed' button disappears": { - "actions": [ - { - "method": "expectVisible", - "selector": "internal:role=button[name=\"Clear completed\"i]", - "isNot": true, - "code": "await expect(page.getByRole('button', { name: 'Clear completed' })).not.toBeVisible();" - } - ] - }, - "The page loads with an empty todo list": { - "actions": [ - { - "method": "expectVisible", - "selector": "internal:role=textbox[name=\"What needs to be done?\"i]", - "code": "await expect(page.getByRole('textbox', { name: 'What needs to be done?' })).toBeVisible();" - } - ] - }, - "Two todos are marked as complete": { - "actions": [ - { - "method": "expectValue", - "selector": "internal:role=listitem >> internal:has-text=\"Task 1\"i >> internal:label=\"Toggle Todo\"i", - "type": "checkbox", - "value": "true", - "code": "await expect(page.getByRole('listitem').filter({ hasText: 'Task 1' }).getByLabel('Toggle Todo')).toBeChecked({ checked: true });" - }, - { - "method": "expectValue", - "selector": "internal:role=listitem >> internal:has-text=\"Task 3\"i >> internal:label=\"Toggle Todo\"i", - "type": "checkbox", - "value": "true", - "code": "await expect(page.getByRole('listitem').filter({ hasText: 'Task 3' }).getByLabel('Toggle Todo')).toBeChecked({ checked: true });" - } - ] - } -} \ No newline at end of file diff --git a/examples/todomvc/tests/perform/deleting-todos/should-delete-single-todo.spec.ts b/examples/todomvc/tests/perform/deleting-todos/should-delete-single-todo.spec.ts deleted file mode 100644 index 150e209a93538..0000000000000 --- a/examples/todomvc/tests/perform/deleting-todos/should-delete-single-todo.spec.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { test } from '../../fixtures'; - -test('should delete single todo', async ({ agent }) => { - await agent.expect(`The page loads with an empty todo list`); - - await agent.perform(`Add a todo 'Task to delete'`); - await agent.expect(`The todo appears in the list`); - await agent.expect(`Counter shows '1 item left'`); - - await agent.perform(`Hover over the todo item`); - await agent.expect(`A delete button (x) appears on the right side of the todo`); - - await agent.perform(`Click the delete button`); - await agent.expect(`The todo is removed from the list`); - await agent.expect(`The list is empty`); - await agent.expect(`The footer is hidden or shows '0 items left'`); -}); diff --git a/examples/todomvc/tests/perform/deleting-todos/should-delete-single-todo.spec.ts-cache.json b/examples/todomvc/tests/perform/deleting-todos/should-delete-single-todo.spec.ts-cache.json deleted file mode 100644 index 37f5a8b620960..0000000000000 --- a/examples/todomvc/tests/perform/deleting-todos/should-delete-single-todo.spec.ts-cache.json +++ /dev/null @@ -1,97 +0,0 @@ -{ - "A delete button (x) appears on the right side of the todo": { - "actions": [ - { - "method": "expectVisible", - "selector": "internal:role=button[name=\"Delete\"i]", - "code": "await expect(page.getByRole('button', { name: 'Delete' })).toBeVisible();" - } - ] - }, - "Add a todo 'Task to delete'": { - "actions": [ - { - "method": "fill", - "selector": "internal:role=textbox[name=\"What needs to be done?\"i]", - "text": "Task to delete", - "submit": true, - "code": "await page.getByRole('textbox', { name: 'What needs to be done?' }).fill('Task to delete');\nawait page.keyboard.press('Enter');" - } - ] - }, - "Click the delete button": { - "actions": [ - { - "method": "click", - "selector": "internal:role=button[name=\"Delete\"i]", - "code": "await page.getByRole('button', { name: 'Delete' }).click();" - } - ] - }, - "Counter shows '1 item left'": { - "actions": [ - { - "method": "expectVisible", - "selector": "internal:text=\"item left\"i", - "code": "await expect(page.getByText('item left')).toBeVisible();" - } - ] - }, - "Hover over the todo item": { - "actions": [ - { - "method": "hover", - "selector": "internal:testid=[data-testid=\"todo-title\"s]", - "code": "await page.getByTestId('todo-title').hover();" - } - ] - }, - "The footer is hidden or shows '0 items left'": { - "actions": [ - { - "method": "expectVisible", - "selector": "internal:text=\"0 items left\"i", - "isNot": true, - "code": "await expect(page.getByText('0 items left')).not.toBeVisible();" - } - ] - }, - "The list is empty": { - "actions": [ - { - "method": "expectVisible", - "selector": "internal:text=\"Task to delete\"i", - "isNot": true, - "code": "await expect(page.getByText('Task to delete')).not.toBeVisible();" - } - ] - }, - "The page loads with an empty todo list": { - "actions": [ - { - "method": "expectVisible", - "selector": "internal:role=textbox[name=\"What needs to be done?\"i]", - "code": "await expect(page.getByRole('textbox', { name: 'What needs to be done?' })).toBeVisible();" - } - ] - }, - "The todo appears in the list": { - "actions": [ - { - "method": "expectVisible", - "selector": "internal:text=\"Task to delete\"i", - "code": "await expect(page.getByText('Task to delete')).toBeVisible();" - } - ] - }, - "The todo is removed from the list": { - "actions": [ - { - "method": "expectVisible", - "selector": "internal:text=\"Task to delete\"i", - "isNot": true, - "code": "await expect(page.getByText('Task to delete')).not.toBeVisible();" - } - ] - } -} \ No newline at end of file diff --git a/examples/todomvc/tests/perform/deleting-todos/should-delete-specific-todo-from-multiple.spec.ts b/examples/todomvc/tests/perform/deleting-todos/should-delete-specific-todo-from-multiple.spec.ts deleted file mode 100644 index 03020597d17ab..0000000000000 --- a/examples/todomvc/tests/perform/deleting-todos/should-delete-specific-todo-from-multiple.spec.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { test } from '../../fixtures'; - -test('should delete specific todo from multiple', async ({ agent }) => { - await agent.expect(`The page loads with an empty todo list`); - - await agent.perform(`Add three todos: 'Task 1', 'Task 2', 'Task 3'`); - await agent.expect(`All three todos appear in the list`); - await agent.expect(`Counter shows '3 items left'`); - - await agent.perform(`Hover over 'Task 2' and click its delete button`); - await agent.expect(`'Task 2' is removed from the list`); - await agent.expect(`'Task 1' and 'Task 3' remain visible`); - await agent.expect(`Counter shows '2 items left'`); -}); diff --git a/examples/todomvc/tests/perform/deleting-todos/should-delete-specific-todo-from-multiple.spec.ts-cache.json b/examples/todomvc/tests/perform/deleting-todos/should-delete-specific-todo-from-multiple.spec.ts-cache.json deleted file mode 100644 index ae699824bb140..0000000000000 --- a/examples/todomvc/tests/perform/deleting-todos/should-delete-specific-todo-from-multiple.spec.ts-cache.json +++ /dev/null @@ -1,98 +0,0 @@ -{ - "'Task 1' and 'Task 3' remain visible": { - "actions": [ - { - "method": "expectAria", - "template": "- list:\n - listitem: Task 1\n - listitem: Task 3", - "code": "await expect(page.locator('body')).toMatchAria(`\n- list:\n - listitem: Task 1\n - listitem: Task 3\n`);" - } - ] - }, - "'Task 2' is removed from the list": { - "actions": [ - { - "method": "expectVisible", - "selector": "internal:text=\"Task 2\"i", - "isNot": true, - "code": "await expect(page.getByText('Task 2')).not.toBeVisible();" - } - ] - }, - "Add three todos: 'Task 1', 'Task 2', 'Task 3'": { - "actions": [ - { - "method": "fill", - "selector": "internal:role=textbox[name=\"What needs to be done?\"i]", - "text": "Task 1", - "submit": true, - "code": "await page.getByRole('textbox', { name: 'What needs to be done?' }).fill('Task 1');\nawait page.keyboard.press('Enter');" - }, - { - "method": "fill", - "selector": "internal:role=textbox[name=\"What needs to be done?\"i]", - "text": "Task 2", - "submit": true, - "code": "await page.getByRole('textbox', { name: 'What needs to be done?' }).fill('Task 2');\nawait page.keyboard.press('Enter');" - }, - { - "method": "fill", - "selector": "internal:role=textbox[name=\"What needs to be done?\"i]", - "text": "Task 3", - "submit": true, - "code": "await page.getByRole('textbox', { name: 'What needs to be done?' }).fill('Task 3');\nawait page.keyboard.press('Enter');" - } - ] - }, - "All three todos appear in the list": { - "actions": [ - { - "method": "expectAria", - "template": "- list:\n - listitem: Task 1\n - listitem: Task 2\n - listitem: Task 3", - "code": "await expect(page.locator('body')).toMatchAria(`\n- list:\n - listitem: Task 1\n - listitem: Task 2\n - listitem: Task 3\n`);" - } - ] - }, - "Counter shows '2 items left'": { - "actions": [ - { - "method": "expectVisible", - "selector": "internal:text=\"2 items left\"i", - "code": "await expect(page.getByText('2 items left')).toBeVisible();" - } - ] - }, - "Counter shows '3 items left'": { - "actions": [ - { - "method": "expectVisible", - "selector": "internal:text=\"3 items left\"i", - "code": "await expect(page.getByText('3 items left')).toBeVisible();" - } - ] - }, - "Hover over 'Task 2' and click its delete button": { - "actions": [ - { - "method": "hover", - "selector": "internal:role=listitem >> internal:has-text=\"Task 2\"i", - "code": "await page.getByRole('listitem').filter({ hasText: 'Task 2' }).hover();" - }, - { - "method": "click", - "selector": "internal:role=button[name=\"Delete\"i]", - "code": "await page.getByRole('button', { name: 'Delete' }).click();" - } - ] - }, - "The page loads with an empty todo list": { - "actions": [ - { - "method": "expectValue", - "selector": "internal:role=textbox[name=\"What needs to be done?\"i]", - "type": "textbox", - "value": "", - "code": "await expect(page.getByRole('textbox', { name: 'What needs to be done?' })).toHaveValue('');" - } - ] - } -} \ No newline at end of file diff --git a/examples/todomvc/tests/perform/editing-todos/should-cancel-edit-on-escape.spec.ts b/examples/todomvc/tests/perform/editing-todos/should-cancel-edit-on-escape.spec.ts deleted file mode 100644 index be4b2ef713647..0000000000000 --- a/examples/todomvc/tests/perform/editing-todos/should-cancel-edit-on-escape.spec.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { test } from '../../fixtures'; - -test('should cancel edit on escape', async ({ agent }) => { - await agent.expect(`The page loads with an empty todo list`); - - await agent.perform(`Add a todo 'Original text'`); - await agent.expect(`The todo appears in the list`); - - await agent.perform(`Double-click on the todo to enter edit mode`); - await agent.expect(`Edit textbox appears with 'Original text'`); - - await agent.perform(`Change the text to 'Modified text' but press Escape instead of Enter`); - await agent.expect(`Edit mode is cancelled`); - await agent.expect(`The todo text reverts to 'Original text'`); - await agent.expect(`Changes are not saved`); -}); diff --git a/examples/todomvc/tests/perform/editing-todos/should-cancel-edit-on-escape.spec.ts-cache.json b/examples/todomvc/tests/perform/editing-todos/should-cancel-edit-on-escape.spec.ts-cache.json deleted file mode 100644 index e2e3a57affc36..0000000000000 --- a/examples/todomvc/tests/perform/editing-todos/should-cancel-edit-on-escape.spec.ts-cache.json +++ /dev/null @@ -1,94 +0,0 @@ -{ - "Add a todo 'Original text'": { - "actions": [ - { - "method": "fill", - "selector": "internal:role=textbox[name=\"What needs to be done?\"i]", - "text": "Original text", - "submit": true, - "code": "await page.getByRole('textbox', { name: 'What needs to be done?' }).fill('Original text');\nawait page.keyboard.press('Enter');" - } - ] - }, - "Change the text to 'Modified text' but press Escape instead of Enter": { - "actions": [ - { - "method": "fill", - "selector": "internal:role=textbox[name=\"Edit\"i]", - "text": "Modified text", - "code": "await page.getByRole('textbox', { name: 'Edit' }).fill('Modified text');" - }, - { - "method": "pressKey", - "key": "Escape", - "code": "await page.keyboard.press('Escape');" - } - ] - }, - "Changes are not saved": { - "actions": [ - { - "method": "expectVisible", - "selector": "internal:text=\"Original text\"i", - "code": "await expect(page.getByText('Original text')).toBeVisible();" - } - ] - }, - "Double-click on the todo to enter edit mode": { - "actions": [ - { - "method": "click", - "selector": "internal:testid=[data-testid=\"todo-title\"s]", - "clickCount": 2, - "code": "await page.getByTestId('todo-title').click({\nclickCount: 2\n});" - } - ] - }, - "Edit mode is cancelled": { - "actions": [ - { - "method": "expectVisible", - "selector": "internal:role=generic[name=\"Original text\"i]", - "code": "await expect(page.getByRole('generic', { name: 'Original text' })).toBeVisible();" - } - ] - }, - "Edit textbox appears with 'Original text'": { - "actions": [ - { - "method": "expectValue", - "selector": "internal:role=textbox[name=\"Edit\"i]", - "type": "textbox", - "value": "Original text", - "code": "await expect(page.getByRole('textbox', { name: 'Edit' })).toHaveValue('Original text');" - } - ] - }, - "The page loads with an empty todo list": { - "actions": [ - { - "method": "expectVisible", - "selector": "internal:role=textbox[name=\"What needs to be done?\"i]", - "code": "await expect(page.getByRole('textbox', { name: 'What needs to be done?' })).toBeVisible();" - } - ] - }, - "The todo appears in the list": { - "actions": [ - { - "method": "expectVisible", - "selector": "internal:text=\"Original text\"i", - "code": "await expect(page.getByText('Original text')).toBeVisible();" - } - ] - }, - "The todo text reverts to 'Original text'": { - "actions": [ - { - "method": "expectVisible", - "selector": "internal:text=\"Original text\"i", - "code": "await expect(page.getByText('Original text')).toBeVisible();" - } - ] - } -} \ No newline at end of file diff --git a/examples/todomvc/tests/perform/editing-todos/should-delete-todo-when-edited-to-empty.spec.ts b/examples/todomvc/tests/perform/editing-todos/should-delete-todo-when-edited-to-empty.spec.ts deleted file mode 100644 index ffaa7fbb5fe0c..0000000000000 --- a/examples/todomvc/tests/perform/editing-todos/should-delete-todo-when-edited-to-empty.spec.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { test } from '../../fixtures'; - -test('should delete todo when edited to empty', async ({ agent }) => { - await agent.expect(`The page loads with an empty todo list`); - - await agent.perform(`Add a todo 'Temporary task'`); - await agent.expect(`The todo appears in the list`); - await agent.expect(`Counter shows '1 item left'`); - - await agent.perform(`Double-click on the todo to enter edit mode`); - await agent.expect(`Edit textbox appears`); - - await agent.perform(`Clear all the text and press Enter`); - await agent.expect(`The todo is deleted from the list`); - await agent.expect(`The list is empty`); - await agent.expect(`Counter shows '0 items left' or the footer is hidden`); -}); diff --git a/examples/todomvc/tests/perform/editing-todos/should-delete-todo-when-edited-to-empty.spec.ts-cache.json b/examples/todomvc/tests/perform/editing-todos/should-delete-todo-when-edited-to-empty.spec.ts-cache.json deleted file mode 100644 index b1a568c528aeb..0000000000000 --- a/examples/todomvc/tests/perform/editing-todos/should-delete-todo-when-edited-to-empty.spec.ts-cache.json +++ /dev/null @@ -1,99 +0,0 @@ -{ - "Add a todo 'Temporary task'": { - "actions": [ - { - "method": "fill", - "selector": "internal:role=textbox[name=\"What needs to be done?\"i]", - "text": "Temporary task", - "submit": true, - "code": "await page.getByRole('textbox', { name: 'What needs to be done?' }).fill('Temporary task');\nawait page.keyboard.press('Enter');" - } - ] - }, - "Clear all the text and press Enter": { - "actions": [ - { - "method": "fill", - "selector": "internal:role=textbox[name=\"Edit\"i]", - "text": "", - "submit": true, - "code": "await page.getByRole('textbox', { name: 'Edit' }).fill('');\nawait page.keyboard.press('Enter');" - } - ] - }, - "Counter shows '0 items left' or the footer is hidden": { - "actions": [ - { - "method": "expectVisible", - "selector": "internal:text=\"0 items left\"i", - "isNot": true, - "code": "await expect(page.getByText('0 items left')).not.toBeVisible();" - } - ] - }, - "Counter shows '1 item left'": { - "actions": [ - { - "method": "expectVisible", - "selector": "internal:text=\"1 item left\"i", - "code": "await expect(page.getByText('1 item left')).toBeVisible();" - } - ] - }, - "Double-click on the todo to enter edit mode": { - "actions": [ - { - "method": "click", - "selector": "internal:testid=[data-testid=\"todo-title\"s]", - "clickCount": 2, - "code": "await page.getByTestId('todo-title').click({\nclickCount: 2\n});" - } - ] - }, - "Edit textbox appears": { - "actions": [ - { - "method": "expectVisible", - "selector": "internal:role=textbox[name=\"Edit\"i]", - "code": "await expect(page.getByRole('textbox', { name: 'Edit' })).toBeVisible();" - } - ] - }, - "The list is empty": { - "actions": [ - { - "method": "expectAria", - "template": "- list:\n", - "code": "await expect(page.locator('body')).toMatchAria(`\n- list:\n\n`);" - } - ] - }, - "The page loads with an empty todo list": { - "actions": [ - { - "method": "expectVisible", - "selector": "internal:role=textbox[name=\"What needs to be done?\"i]", - "code": "await expect(page.getByRole('textbox', { name: 'What needs to be done?' })).toBeVisible();" - } - ] - }, - "The todo appears in the list": { - "actions": [ - { - "method": "expectVisible", - "selector": "internal:text=\"Temporary task\"i", - "code": "await expect(page.getByText('Temporary task')).toBeVisible();" - } - ] - }, - "The todo is deleted from the list": { - "actions": [ - { - "method": "expectVisible", - "selector": "internal:role=textbox[name=\"What needs to be done?\"i]", - "isNot": false, - "code": "await expect(page.getByRole('textbox', { name: 'What needs to be done?' })).toBeVisible();" - } - ] - } -} \ No newline at end of file diff --git a/examples/todomvc/tests/perform/editing-todos/should-edit-todo-by-double-clicking.spec.ts b/examples/todomvc/tests/perform/editing-todos/should-edit-todo-by-double-clicking.spec.ts deleted file mode 100644 index a11d582c43171..0000000000000 --- a/examples/todomvc/tests/perform/editing-todos/should-edit-todo-by-double-clicking.spec.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { test } from '../../fixtures'; - -test('should edit todo by double clicking', async ({ agent }) => { - await agent.expect(`The page loads with an empty todo list`); - - await agent.perform(`Add a todo 'Buy milk'`); - await agent.expect(`The todo appears in the list`); - - await agent.perform(`Double-click on the todo text`); - await agent.expect(`The todo enters edit mode`); - await agent.expect(`An edit textbox appears with the current text 'Buy milk'`); - await agent.expect(`The textbox is focused`); - - await agent.perform(`Change the text to 'Buy organic milk' and press Enter`); - await agent.expect(`The todo is updated to 'Buy organic milk'`); - await agent.expect(`Edit mode is exited`); - await agent.expect(`The updated text is displayed in the list`); -}); diff --git a/examples/todomvc/tests/perform/editing-todos/should-edit-todo-by-double-clicking.spec.ts-cache.json b/examples/todomvc/tests/perform/editing-todos/should-edit-todo-by-double-clicking.spec.ts-cache.json deleted file mode 100644 index 99e8380ae71a0..0000000000000 --- a/examples/todomvc/tests/perform/editing-todos/should-edit-todo-by-double-clicking.spec.ts-cache.json +++ /dev/null @@ -1,108 +0,0 @@ -{ - "Add a todo 'Buy milk'": { - "actions": [ - { - "method": "fill", - "selector": "internal:role=textbox[name=\"What needs to be done?\"i]", - "text": "Buy milk", - "submit": true, - "code": "await page.getByRole('textbox', { name: 'What needs to be done?' }).fill('Buy milk');\nawait page.keyboard.press('Enter');" - } - ] - }, - "An edit textbox appears with the current text 'Buy milk'": { - "actions": [ - { - "method": "expectValue", - "selector": "internal:role=textbox[name=\"Edit\"i]", - "type": "textbox", - "value": "Buy milk", - "code": "await expect(page.getByRole('textbox', { name: 'Edit' })).toHaveValue('Buy milk');" - } - ] - }, - "Change the text to 'Buy organic milk' and press Enter": { - "actions": [ - { - "method": "fill", - "selector": "internal:role=textbox[name=\"Edit\"i]", - "text": "Buy organic milk", - "submit": true, - "code": "await page.getByRole('textbox', { name: 'Edit' }).fill('Buy organic milk');\nawait page.keyboard.press('Enter');" - } - ] - }, - "Double-click on the todo text": { - "actions": [ - { - "method": "click", - "selector": "internal:testid=[data-testid=\"todo-title\"s]", - "clickCount": 2, - "code": "await page.getByTestId('todo-title').click({\nclickCount: 2\n});" - } - ] - }, - "Edit mode is exited": { - "actions": [ - { - "method": "expectVisible", - "selector": "internal:text=\"Buy organic milk\"i", - "code": "await expect(page.getByText('Buy organic milk')).toBeVisible();" - } - ] - }, - "The page loads with an empty todo list": { - "actions": [ - { - "method": "expectVisible", - "selector": "internal:role=textbox[name=\"What needs to be done?\"i]", - "code": "await expect(page.getByRole('textbox', { name: 'What needs to be done?' })).toBeVisible();" - } - ] - }, - "The textbox is focused": { - "actions": [ - { - "method": "expectVisible", - "selector": "internal:role=textbox[name=\"Edit\"i]", - "code": "await expect(page.getByRole('textbox', { name: 'Edit' })).toBeVisible();" - } - ] - }, - "The todo appears in the list": { - "actions": [ - { - "method": "expectVisible", - "selector": "internal:text=\"Buy milk\"i", - "code": "await expect(page.getByText('Buy milk')).toBeVisible();" - } - ] - }, - "The todo enters edit mode": { - "actions": [ - { - "method": "expectVisible", - "selector": "internal:role=textbox[name=\"Edit\"i]", - "code": "await expect(page.getByRole('textbox', { name: 'Edit' })).toBeVisible();" - } - ] - }, - "The todo is updated to 'Buy organic milk'": { - "actions": [ - { - "method": "expectVisible", - "selector": "internal:text=\"Buy organic milk\"i", - "code": "await expect(page.getByText('Buy organic milk')).toBeVisible();" - } - ] - }, - "The updated text is displayed in the list": { - "actions": [ - { - "method": "expectVisible", - "selector": "internal:text=\"Buy organic milk\"i", - "code": "await expect(page.getByText('Buy organic milk')).toBeVisible();" - } - ] - } -} \ No newline at end of file diff --git a/examples/todomvc/tests/perform/editing-todos/should-save-edit-on-blur.spec.ts b/examples/todomvc/tests/perform/editing-todos/should-save-edit-on-blur.spec.ts deleted file mode 100644 index cf429fb85b235..0000000000000 --- a/examples/todomvc/tests/perform/editing-todos/should-save-edit-on-blur.spec.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { test } from '../../fixtures'; - -test('should save edit on blur', async ({ agent }) => { - await agent.expect(`The page loads with an empty todo list`); - - await agent.perform(`Add a todo 'Call dentist'`); - await agent.expect(`The todo appears in the list`); - - await agent.perform(`Double-click on the todo to enter edit mode`); - await agent.expect(`Edit textbox appears`); - - await agent.perform(`Change the text to 'Schedule dentist appointment' and click elsewhere to blur the input`); - await agent.expect(`The changes are saved`); - await agent.expect(`The todo text is updated to 'Schedule dentist appointment'`); - await agent.expect(`Edit mode is exited`); -}); diff --git a/examples/todomvc/tests/perform/editing-todos/should-save-edit-on-blur.spec.ts-cache.json b/examples/todomvc/tests/perform/editing-todos/should-save-edit-on-blur.spec.ts-cache.json deleted file mode 100644 index 9387c52cb1998..0000000000000 --- a/examples/todomvc/tests/perform/editing-todos/should-save-edit-on-blur.spec.ts-cache.json +++ /dev/null @@ -1,93 +0,0 @@ -{ - "Add a todo 'Call dentist'": { - "actions": [ - { - "method": "fill", - "selector": "internal:role=textbox[name=\"What needs to be done?\"i]", - "text": "Call dentist", - "submit": true, - "code": "await page.getByRole('textbox', { name: 'What needs to be done?' }).fill('Call dentist');\nawait page.keyboard.press('Enter');" - } - ] - }, - "Change the text to 'Schedule dentist appointment' and click elsewhere to blur the input": { - "actions": [ - { - "method": "fill", - "selector": "internal:role=textbox[name=\"Edit\"i]", - "text": "Schedule dentist appointment", - "code": "await page.getByRole('textbox', { name: 'Edit' }).fill('Schedule dentist appointment');" - }, - { - "method": "click", - "selector": "internal:role=heading[name=\"todos\"i]", - "code": "await page.getByRole('heading', { name: 'todos' }).click();" - } - ] - }, - "Double-click on the todo to enter edit mode": { - "actions": [ - { - "method": "click", - "selector": "internal:testid=[data-testid=\"todo-title\"s]", - "clickCount": 2, - "code": "await page.getByTestId('todo-title').click({\nclickCount: 2\n});" - } - ] - }, - "Edit mode is exited": { - "actions": [ - { - "method": "expectVisible", - "selector": "internal:text=\"Schedule dentist appointment\"i", - "isNot": false, - "code": "await expect(page.getByText('Schedule dentist appointment')).toBeVisible();" - } - ] - }, - "Edit textbox appears": { - "actions": [ - { - "method": "expectVisible", - "selector": "internal:role=textbox[name=\"Edit\"i]", - "code": "await expect(page.getByRole('textbox', { name: 'Edit' })).toBeVisible();" - } - ] - }, - "The changes are saved": { - "actions": [ - { - "method": "expectVisible", - "selector": "internal:text=\"Schedule dentist appointment\"i", - "code": "await expect(page.getByText('Schedule dentist appointment')).toBeVisible();" - } - ] - }, - "The page loads with an empty todo list": { - "actions": [ - { - "method": "expectVisible", - "selector": "internal:role=textbox[name=\"What needs to be done?\"i]", - "code": "await expect(page.getByRole('textbox', { name: 'What needs to be done?' })).toBeVisible();" - } - ] - }, - "The todo appears in the list": { - "actions": [ - { - "method": "expectVisible", - "selector": "internal:text=\"Call dentist\"i", - "code": "await expect(page.getByText('Call dentist')).toBeVisible();" - } - ] - }, - "The todo text is updated to 'Schedule dentist appointment'": { - "actions": [ - { - "method": "expectVisible", - "selector": "internal:text=\"Schedule dentist appointment\"i", - "code": "await expect(page.getByText('Schedule dentist appointment')).toBeVisible();" - } - ] - } -} \ No newline at end of file diff --git a/examples/todomvc/tests/perform/editing-todos/should-trim-whitespace-when-editing.spec.ts b/examples/todomvc/tests/perform/editing-todos/should-trim-whitespace-when-editing.spec.ts deleted file mode 100644 index 4370afa23a292..0000000000000 --- a/examples/todomvc/tests/perform/editing-todos/should-trim-whitespace-when-editing.spec.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { test } from '../../fixtures'; - -test('should trim whitespace when editing', async ({ agent }) => { - await agent.expect(`The page loads with an empty todo list`); - - await agent.perform(`Add a todo 'Original task'`); - await agent.expect(`The todo appears in the list`); - - await agent.perform(`Double-click to edit and change text to ' Edited task ' (with leading and trailing spaces)`); - await agent.expect(`Edit textbox shows the text with spaces`); - - await agent.perform(`Press Enter to save`); - await agent.expect(`The todo is saved as 'Edited task' without leading or trailing whitespace`); -}); diff --git a/examples/todomvc/tests/perform/editing-todos/should-trim-whitespace-when-editing.spec.ts-cache.json b/examples/todomvc/tests/perform/editing-todos/should-trim-whitespace-when-editing.spec.ts-cache.json deleted file mode 100644 index aab34f22d570b..0000000000000 --- a/examples/todomvc/tests/perform/editing-todos/should-trim-whitespace-when-editing.spec.ts-cache.json +++ /dev/null @@ -1,77 +0,0 @@ -{ - "Add a todo 'Original task'": { - "actions": [ - { - "method": "fill", - "selector": "internal:role=textbox[name=\"What needs to be done?\"i]", - "text": "Original task", - "submit": true, - "code": "await page.getByRole('textbox', { name: 'What needs to be done?' }).fill('Original task');\nawait page.keyboard.press('Enter');" - } - ] - }, - "Double-click to edit and change text to ' Edited task ' (with leading and trailing spaces)": { - "actions": [ - { - "method": "click", - "selector": "internal:testid=[data-testid=\"todo-title\"s]", - "clickCount": 2, - "code": "await page.getByTestId('todo-title').click({\nclickCount: 2\n});" - }, - { - "method": "fill", - "selector": "internal:role=textbox[name=\"Edit\"i]", - "text": " Edited task ", - "submit": true, - "code": "await page.getByRole('textbox', { name: 'Edit' }).fill(' Edited task ');\nawait page.keyboard.press('Enter');" - } - ] - }, - "Edit textbox shows the text with spaces": { - "actions": [ - { - "method": "expectVisible", - "selector": "internal:text=\" Edited task \"i", - "code": "await expect(page.getByText(' Edited task ')).toBeVisible();" - } - ] - }, - "Press Enter to save": { - "actions": [ - { - "method": "pressKey", - "key": "Enter", - "code": "await page.keyboard.press('Enter');" - } - ] - }, - "The page loads with an empty todo list": { - "actions": [ - { - "method": "expectValue", - "selector": "internal:role=textbox[name=\"What needs to be done?\"i]", - "type": "textbox", - "value": "", - "code": "await expect(page.getByRole('textbox', { name: 'What needs to be done?' })).toHaveValue('');" - } - ] - }, - "The todo appears in the list": { - "actions": [ - { - "method": "expectVisible", - "selector": "internal:text=\"Original task\"i", - "code": "await expect(page.getByText('Original task')).toBeVisible();" - } - ] - }, - "The todo is saved as 'Edited task' without leading or trailing whitespace": { - "actions": [ - { - "method": "expectVisible", - "selector": "internal:text=\"Edited task\"i", - "code": "await expect(page.getByText('Edited task')).toBeVisible();" - } - ] - } -} \ No newline at end of file diff --git a/examples/todomvc/tests/perform/filtering-todos/should-filter-active-todos.spec.ts b/examples/todomvc/tests/perform/filtering-todos/should-filter-active-todos.spec.ts deleted file mode 100644 index e836bd141334d..0000000000000 --- a/examples/todomvc/tests/perform/filtering-todos/should-filter-active-todos.spec.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { test } from '../../fixtures'; - -test('should filter active todos', async ({ agent }) => { - await agent.expect(`The page loads with an empty todo list`); - - await agent.perform(`Add three todos: 'Active 1', 'Active 2', 'Will complete'`); - await agent.expect(`All three todos are visible`); - - await agent.perform(`Mark 'Will complete' as completed by clicking its checkbox`); - await agent.expect(`One todo is marked as complete`); - await agent.expect(`Counter shows '2 items left'`); - - await agent.perform(`Click on the 'Active' filter link`); - await agent.expect(`The URL changes to #/active`); - await agent.expect(`Only 'Active 1' and 'Active 2' are displayed`); - await agent.expect(`'Will complete' is not visible`); - await agent.expect(`The 'Active' filter link is highlighted`); -}); diff --git a/examples/todomvc/tests/perform/filtering-todos/should-filter-active-todos.spec.ts-cache.json b/examples/todomvc/tests/perform/filtering-todos/should-filter-active-todos.spec.ts-cache.json deleted file mode 100644 index 266698b8dcc98..0000000000000 --- a/examples/todomvc/tests/perform/filtering-todos/should-filter-active-todos.spec.ts-cache.json +++ /dev/null @@ -1,120 +0,0 @@ -{ - "'Will complete' is not visible": { - "actions": [ - { - "method": "expectVisible", - "selector": "internal:text=\"Will complete\"i", - "isNot": true, - "code": "await expect(page.getByText('Will complete')).not.toBeVisible();" - } - ] - }, - "Add three todos: 'Active 1', 'Active 2', 'Will complete'": { - "actions": [ - { - "method": "fill", - "selector": "internal:role=textbox[name=\"What needs to be done?\"i]", - "text": "Active 1", - "submit": true, - "code": "await page.getByRole('textbox', { name: 'What needs to be done?' }).fill('Active 1');\nawait page.keyboard.press('Enter');" - }, - { - "method": "fill", - "selector": "internal:role=textbox[name=\"What needs to be done?\"i]", - "text": "Active 2", - "submit": true, - "code": "await page.getByRole('textbox', { name: 'What needs to be done?' }).fill('Active 2');\nawait page.keyboard.press('Enter');" - }, - { - "method": "fill", - "selector": "internal:role=textbox[name=\"What needs to be done?\"i]", - "text": "Will complete", - "submit": true, - "code": "await page.getByRole('textbox', { name: 'What needs to be done?' }).fill('Will complete');\nawait page.keyboard.press('Enter');" - } - ] - }, - "All three todos are visible": { - "actions": [ - { - "method": "expectAria", - "template": "- list:\n - listitem: Active 1\n - listitem: Active 2\n - listitem: Will complete", - "code": "await expect(page.locator('body')).toMatchAria(`\n- list:\n - listitem: Active 1\n - listitem: Active 2\n - listitem: Will complete\n`);" - } - ] - }, - "Click on the 'Active' filter link": { - "actions": [ - { - "method": "click", - "selector": "internal:role=link[name=\"Active\"i]", - "code": "await page.getByRole('link', { name: 'Active' }).click();" - } - ] - }, - "Counter shows '2 items left'": { - "actions": [ - { - "method": "expectVisible", - "selector": "internal:text=\"2 items left\"i", - "code": "await expect(page.getByText('2 items left')).toBeVisible();" - } - ] - }, - "Mark 'Will complete' as completed by clicking its checkbox": { - "actions": [ - { - "method": "click", - "selector": "internal:role=listitem >> internal:has-text=\"Will complete\"i >> internal:label=\"Toggle Todo\"i", - "code": "await page.getByRole('listitem').filter({ hasText: 'Will complete' }).getByLabel('Toggle Todo').click();" - } - ] - }, - "One todo is marked as complete": { - "actions": [ - { - "method": "expectValue", - "selector": "internal:role=listitem >> internal:has-text=\"Will complete\"i >> internal:label=\"Toggle Todo\"i", - "type": "checkbox", - "value": "true", - "code": "await expect(page.getByRole('listitem').filter({ hasText: 'Will complete' }).getByLabel('Toggle Todo')).toBeChecked({ checked: true });" - } - ] - }, - "Only 'Active 1' and 'Active 2' are displayed": { - "actions": [ - { - "method": "expectAria", - "template": "- list:\n - listitem: Active 1\n - listitem: Active 2", - "code": "await expect(page.locator('body')).toMatchAria(`\n- list:\n - listitem: Active 1\n - listitem: Active 2\n`);" - } - ] - }, - "The 'Active' filter link is highlighted": { - "actions": [ - { - "method": "expectVisible", - "selector": "internal:role=link[name=\"Active\"i]", - "code": "await expect(page.getByRole('link', { name: 'Active' })).toBeVisible();" - } - ] - }, - "The page loads with an empty todo list": { - "actions": [ - { - "method": "expectVisible", - "selector": "internal:role=textbox[name=\"What needs to be done?\"i]", - "code": "await expect(page.getByRole('textbox', { name: 'What needs to be done?' })).toBeVisible();" - } - ] - }, - "The URL changes to #/active": { - "actions": [ - { - "method": "expectURL", - "regex": "/.*#/active$/", - "code": "await expect(page).toHaveURL(/.*#\\/active$/);" - } - ] - } -} \ No newline at end of file diff --git a/examples/todomvc/tests/perform/filtering-todos/should-filter-completed-todos.spec.ts b/examples/todomvc/tests/perform/filtering-todos/should-filter-completed-todos.spec.ts deleted file mode 100644 index 28891f1b11fd2..0000000000000 --- a/examples/todomvc/tests/perform/filtering-todos/should-filter-completed-todos.spec.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { test } from '../../fixtures'; - -test('should filter completed todos', async ({ agent }) => { - await agent.expect(`The page loads with an empty todo list`); - - await agent.perform(`Add three todos: 'Active task', 'Completed 1', 'Completed 2'`); - await agent.expect(`All three todos are visible`); - - await agent.perform(`Mark 'Completed 1' and 'Completed 2' as completed by clicking their checkboxes`); - await agent.expect(`Two todos are marked as complete`); - - await agent.perform(`Click on the 'Completed' filter link`); - await agent.expect(`The URL changes to #/completed`); - await agent.expect(`Only 'Completed 1' and 'Completed 2' are displayed`); - await agent.expect(`'Active task' is not visible`); - await agent.expect(`The 'Completed' filter link is highlighted`); -}); diff --git a/examples/todomvc/tests/perform/filtering-todos/should-filter-completed-todos.spec.ts-cache.json b/examples/todomvc/tests/perform/filtering-todos/should-filter-completed-todos.spec.ts-cache.json deleted file mode 100644 index c8fd9fa7b5e8d..0000000000000 --- a/examples/todomvc/tests/perform/filtering-todos/should-filter-completed-todos.spec.ts-cache.json +++ /dev/null @@ -1,114 +0,0 @@ -{ - "'Active task' is not visible": { - "actions": [ - { - "method": "expectVisible", - "selector": "internal:text=\"Active task\"i", - "isNot": true, - "code": "await expect(page.getByText('Active task')).not.toBeVisible();" - } - ] - }, - "Add three todos: 'Active task', 'Completed 1', 'Completed 2'": { - "actions": [ - { - "method": "fill", - "selector": "internal:role=textbox[name=\"What needs to be done?\"i]", - "text": "Active task", - "submit": true, - "code": "await page.getByRole('textbox', { name: 'What needs to be done?' }).fill('Active task');\nawait page.keyboard.press('Enter');" - }, - { - "method": "fill", - "selector": "internal:role=textbox[name=\"What needs to be done?\"i]", - "text": "Completed 1", - "submit": true, - "code": "await page.getByRole('textbox', { name: 'What needs to be done?' }).fill('Completed 1');\nawait page.keyboard.press('Enter');" - }, - { - "method": "fill", - "selector": "internal:role=textbox[name=\"What needs to be done?\"i]", - "text": "Completed 2", - "submit": true, - "code": "await page.getByRole('textbox', { name: 'What needs to be done?' }).fill('Completed 2');\nawait page.keyboard.press('Enter');" - } - ] - }, - "All three todos are visible": { - "actions": [ - { - "method": "expectAria", - "template": "- list:\n - listitem: Active task\n - listitem: Completed 1\n - listitem: Completed 2", - "code": "await expect(page.locator('body')).toMatchAria(`\n- list:\n - listitem: Active task\n - listitem: Completed 1\n - listitem: Completed 2\n`);" - } - ] - }, - "Click on the 'Completed' filter link": { - "actions": [ - { - "method": "click", - "selector": "internal:role=link[name=\"Completed\"i]", - "code": "await page.getByRole('link', { name: 'Completed' }).click();" - } - ] - }, - "Mark 'Completed 1' and 'Completed 2' as completed by clicking their checkboxes": { - "actions": [ - { - "method": "click", - "selector": "internal:role=listitem >> internal:has-text=\"Completed 1\"i >> internal:label=\"Toggle Todo\"i", - "code": "await page.getByRole('listitem').filter({ hasText: 'Completed 1' }).getByLabel('Toggle Todo').click();" - }, - { - "method": "click", - "selector": "internal:role=listitem >> internal:has-text=\"Completed 2\"i >> internal:label=\"Toggle Todo\"i", - "code": "await page.getByRole('listitem').filter({ hasText: 'Completed 2' }).getByLabel('Toggle Todo').click();" - } - ] - }, - "Only 'Completed 1' and 'Completed 2' are displayed": { - "actions": [ - { - "method": "expectAria", - "template": "- list:\n - listitem: Completed 1\n - listitem: Completed 2", - "code": "await expect(page.locator('body')).toMatchAria(`\n- list:\n - listitem: Completed 1\n - listitem: Completed 2\n`);" - } - ] - }, - "The 'Completed' filter link is highlighted": { - "actions": [ - { - "method": "expectVisible", - "selector": "internal:role=link[name=\"Completed\"i]", - "code": "await expect(page.getByRole('link', { name: 'Completed' })).toBeVisible();" - } - ] - }, - "The page loads with an empty todo list": { - "actions": [ - { - "method": "expectVisible", - "selector": "internal:role=textbox[name=\"What needs to be done?\"i]", - "code": "await expect(page.getByRole('textbox', { name: 'What needs to be done?' })).toBeVisible();" - } - ] - }, - "The URL changes to #/completed": { - "actions": [ - { - "method": "expectURL", - "regex": "/.*#/completed$/", - "code": "await expect(page).toHaveURL(/.*#\\/completed$/);" - } - ] - }, - "Two todos are marked as complete": { - "actions": [ - { - "method": "expectVisible", - "selector": "internal:role=button[name=\"Clear completed\"i]", - "code": "await expect(page.getByRole('button', { name: 'Clear completed' })).toBeVisible();" - } - ] - } -} \ No newline at end of file diff --git a/examples/todomvc/tests/perform/filtering-todos/should-show-all-todos-with-all-filter.spec.ts b/examples/todomvc/tests/perform/filtering-todos/should-show-all-todos-with-all-filter.spec.ts deleted file mode 100644 index ec31f5d73491f..0000000000000 --- a/examples/todomvc/tests/perform/filtering-todos/should-show-all-todos-with-all-filter.spec.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { test } from '../../fixtures'; - -test('should show all todos with all filter', async ({ agent }) => { - await agent.expect(`The page loads with an empty todo list`); - - await agent.perform(`Add three todos and mark one as complete`); - await agent.expect(`Three todos exist, one completed and two active`); - - await agent.perform(`Navigate to the 'Active' filter by clicking on it`); - await agent.expect(`Only active todos are visible`); - - await agent.perform(`Click on the 'All' filter link`); - await agent.expect(`The URL changes to #/`); - await agent.expect(`All todos (both completed and active) are displayed`); - await agent.expect(`The 'All' filter link is highlighted`); -}); diff --git a/examples/todomvc/tests/perform/filtering-todos/should-show-all-todos-with-all-filter.spec.ts-cache.json b/examples/todomvc/tests/perform/filtering-todos/should-show-all-todos-with-all-filter.spec.ts-cache.json deleted file mode 100644 index e31b7d2d24b82..0000000000000 --- a/examples/todomvc/tests/perform/filtering-todos/should-show-all-todos-with-all-filter.spec.ts-cache.json +++ /dev/null @@ -1,104 +0,0 @@ -{ - "Add three todos and mark one as complete": { - "actions": [ - { - "method": "fill", - "selector": "internal:role=textbox[name=\"What needs to be done?\"i]", - "text": "Buy groceries", - "submit": true, - "code": "await page.getByRole('textbox', { name: 'What needs to be done?' }).fill('Buy groceries');\nawait page.keyboard.press('Enter');" - }, - { - "method": "fill", - "selector": "internal:role=textbox[name=\"What needs to be done?\"i]", - "text": "Walk the dog", - "submit": true, - "code": "await page.getByRole('textbox', { name: 'What needs to be done?' }).fill('Walk the dog');\nawait page.keyboard.press('Enter');" - }, - { - "method": "fill", - "selector": "internal:role=textbox[name=\"What needs to be done?\"i]", - "text": "Read a book", - "submit": true, - "code": "await page.getByRole('textbox', { name: 'What needs to be done?' }).fill('Read a book');\nawait page.keyboard.press('Enter');" - }, - { - "method": "click", - "selector": "internal:role=listitem >> internal:has-text=\"Buy groceries\"i >> internal:label=\"Toggle Todo\"i", - "code": "await page.getByRole('listitem').filter({ hasText: 'Buy groceries' }).getByLabel('Toggle Todo').click();" - } - ] - }, - "All todos (both completed and active) are displayed": { - "actions": [ - { - "method": "expectAria", - "template": "- list:\n - listitem: Buy groceries\n - listitem: Walk the dog\n - listitem: Read a book", - "code": "await expect(page.locator('body')).toMatchAria(`\n- list:\n - listitem: Buy groceries\n - listitem: Walk the dog\n - listitem: Read a book\n`);" - } - ] - }, - "Click on the 'All' filter link": { - "actions": [ - { - "method": "click", - "selector": "internal:role=link[name=\"All\"i]", - "code": "await page.getByRole('link', { name: 'All' }).click();" - } - ] - }, - "Navigate to the 'Active' filter by clicking on it": { - "actions": [ - { - "method": "click", - "selector": "internal:role=link[name=\"Active\"i]", - "code": "await page.getByRole('link', { name: 'Active' }).click();" - } - ] - }, - "Only active todos are visible": { - "actions": [ - { - "method": "expectAria", - "template": "- list:\n - listitem: Walk the dog\n - listitem: Read a book", - "code": "await expect(page.locator('body')).toMatchAria(`\n- list:\n - listitem: Walk the dog\n - listitem: Read a book\n`);" - } - ] - }, - "The 'All' filter link is highlighted": { - "actions": [ - { - "method": "expectVisible", - "selector": "internal:role=link[name=\"All\"i]", - "code": "await expect(page.getByRole('link', { name: 'All' })).toBeVisible();" - } - ] - }, - "The page loads with an empty todo list": { - "actions": [ - { - "method": "expectVisible", - "selector": "internal:role=textbox[name=\"What needs to be done?\"i]", - "code": "await expect(page.getByRole('textbox', { name: 'What needs to be done?' })).toBeVisible();" - } - ] - }, - "The URL changes to #/": { - "actions": [ - { - "method": "expectURL", - "regex": "/.*#/$/", - "code": "await expect(page).toHaveURL(/.*#\\/$/);" - } - ] - }, - "Three todos exist, one completed and two active": { - "actions": [ - { - "method": "expectAria", - "template": "- list:\n - listitem: Buy groceries\n - listitem: Walk the dog\n - listitem: Read a book", - "code": "await expect(page.locator('body')).toMatchAria(`\n- list:\n - listitem: Buy groceries\n - listitem: Walk the dog\n - listitem: Read a book\n`);" - } - ] - } -} \ No newline at end of file diff --git a/examples/todomvc/tests/perform/persistence/should-persist-todos-after-page-reload.spec.ts b/examples/todomvc/tests/perform/persistence/should-persist-todos-after-page-reload.spec.ts deleted file mode 100644 index 2acc94427d32d..0000000000000 --- a/examples/todomvc/tests/perform/persistence/should-persist-todos-after-page-reload.spec.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { test } from '../../fixtures'; - -test('should persist todos after page reload', async ({ agent }) => { - await agent.expect(`The page loads with an empty todo list`); - - await agent.perform(`Add three todos: 'Persistent 1', 'Persistent 2', 'Persistent 3'`); - await agent.expect(`All three todos appear in the list`); - - await agent.perform(`Mark 'Persistent 2' as completed by clicking its checkbox`); - await agent.expect(`'Persistent 2' is marked as complete`); - - await agent.perform(`Reload the page`); - await agent.expect(`All three todos are still present after reload`); - await agent.expect(`'Persistent 2' is still marked as complete`); - await agent.expect(`The counter shows '2 items left'`); -}); diff --git a/examples/todomvc/tests/perform/persistence/should-persist-todos-after-page-reload.spec.ts-cache.json b/examples/todomvc/tests/perform/persistence/should-persist-todos-after-page-reload.spec.ts-cache.json deleted file mode 100644 index 0dc39f0bf9c60..0000000000000 --- a/examples/todomvc/tests/perform/persistence/should-persist-todos-after-page-reload.spec.ts-cache.json +++ /dev/null @@ -1,103 +0,0 @@ -{ - "'Persistent 2' is marked as complete": { - "actions": [ - { - "method": "expectValue", - "selector": "internal:role=listitem >> internal:has-text=\"Persistent 2\"i >> internal:label=\"Toggle Todo\"i", - "type": "checkbox", - "value": "true", - "code": "await expect(page.getByRole('listitem').filter({ hasText: 'Persistent 2' }).getByLabel('Toggle Todo')).toBeChecked({ checked: true });" - } - ] - }, - "'Persistent 2' is still marked as complete": { - "actions": [ - { - "method": "expectValue", - "selector": "internal:role=listitem >> internal:has-text=\"Persistent 2\"i >> internal:label=\"Toggle Todo\"i", - "type": "checkbox", - "value": "true", - "code": "await expect(page.getByRole('listitem').filter({ hasText: 'Persistent 2' }).getByLabel('Toggle Todo')).toBeChecked({ checked: true });" - } - ] - }, - "Add three todos: 'Persistent 1', 'Persistent 2', 'Persistent 3'": { - "actions": [ - { - "method": "fill", - "selector": "internal:role=textbox[name=\"What needs to be done?\"i]", - "text": "Persistent 1", - "submit": true, - "code": "await page.getByRole('textbox', { name: 'What needs to be done?' }).fill('Persistent 1');\nawait page.keyboard.press('Enter');" - }, - { - "method": "fill", - "selector": "internal:role=textbox[name=\"What needs to be done?\"i]", - "text": "Persistent 2", - "submit": true, - "code": "await page.getByRole('textbox', { name: 'What needs to be done?' }).fill('Persistent 2');\nawait page.keyboard.press('Enter');" - }, - { - "method": "fill", - "selector": "internal:role=textbox[name=\"What needs to be done?\"i]", - "text": "Persistent 3", - "submit": true, - "code": "await page.getByRole('textbox', { name: 'What needs to be done?' }).fill('Persistent 3');\nawait page.keyboard.press('Enter');" - } - ] - }, - "All three todos appear in the list": { - "actions": [ - { - "method": "expectAria", - "template": "- list:\n - listitem: Persistent 1\n - listitem: Persistent 2\n - listitem: Persistent 3", - "code": "await expect(page.locator('body')).toMatchAria(`\n- list:\n - listitem: Persistent 1\n - listitem: Persistent 2\n - listitem: Persistent 3\n`);" - } - ] - }, - "All three todos are still present after reload": { - "actions": [ - { - "method": "expectAria", - "template": "- list:\n - listitem: Persistent 1\n - listitem: Persistent 2\n - listitem: Persistent 3", - "code": "await expect(page.locator('body')).toMatchAria(`\n- list:\n - listitem: Persistent 1\n - listitem: Persistent 2\n - listitem: Persistent 3\n`);" - } - ] - }, - "Mark 'Persistent 2' as completed by clicking its checkbox": { - "actions": [ - { - "method": "click", - "selector": "internal:role=listitem >> internal:has-text=\"Persistent 2\"i >> internal:label=\"Toggle Todo\"i", - "code": "await page.getByRole('listitem').filter({ hasText: 'Persistent 2' }).getByLabel('Toggle Todo').click();" - } - ] - }, - "Reload the page": { - "actions": [ - { - "method": "pressKey", - "key": "F5", - "code": "await page.keyboard.press('F5');" - } - ] - }, - "The counter shows '2 items left'": { - "actions": [ - { - "method": "expectVisible", - "selector": "internal:text=\"2 items left\"i", - "code": "await expect(page.getByText('2 items left')).toBeVisible();" - } - ] - }, - "The page loads with an empty todo list": { - "actions": [ - { - "method": "expectVisible", - "selector": "internal:role=textbox[name=\"What needs to be done?\"i]", - "code": "await expect(page.getByRole('textbox', { name: 'What needs to be done?' })).toBeVisible();" - } - ] - } -} \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index bf0748c69484b..205ad77364ebc 100644 --- a/package-lock.json +++ b/package-lock.json @@ -18,7 +18,6 @@ "@eslint/compat": "^1.3.2", "@eslint/eslintrc": "^3.3.1", "@eslint/js": "^9.34.0", - "@lowire/loop": "^0.0.25", "@modelcontextprotocol/sdk": "^1.26.0", "@octokit/graphql-schema": "^15.26.0", "@stylistic/eslint-plugin": "^5.2.3", @@ -1091,16 +1090,6 @@ "@jridgewell/sourcemap-codec": "^1.4.14" } }, - "node_modules/@lowire/loop": { - "version": "0.0.25", - "resolved": "https://registry.npmjs.org/@lowire/loop/-/loop-0.0.25.tgz", - "integrity": "sha512-tBfQ3+m+BelMSRqmdpOqLQPervF6PoS4IV0JE0ga1fJnOaPRnOfjkGecyaR45Hp7VuhY9oBQdVlDkAXoOkjk1w==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": ">=20" - } - }, "node_modules/@modelcontextprotocol/sdk": { "version": "1.26.0", "resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.26.0.tgz", diff --git a/package.json b/package.json index 92e3d55b82c5b..73bd6076d5568 100644 --- a/package.json +++ b/package.json @@ -58,7 +58,6 @@ "@eslint/compat": "^1.3.2", "@eslint/eslintrc": "^3.3.1", "@eslint/js": "^9.34.0", - "@lowire/loop": "^0.0.25", "@modelcontextprotocol/sdk": "^1.26.0", "@octokit/graphql-schema": "^15.26.0", "@stylistic/eslint-plugin": "^5.2.3", diff --git a/packages/playwright-client/types/types.d.ts b/packages/playwright-client/types/types.d.ts index 9d2fd9510631b..f391d1d0f4d8e 100644 --- a/packages/playwright-client/types/types.d.ts +++ b/packages/playwright-client/types/types.d.ts @@ -2099,89 +2099,6 @@ export interface Page { url?: string; }): Promise; - /** - * Initialize page agent with the llm provider and cache. - * @param options - */ - agent(options?: { - cache?: { - /** - * Cache file to use/generate code for performed actions into. Cache is not used if not specified (default). - */ - cacheFile?: string; - - /** - * When specified, generated entries are written into the `cacheOutFile` instead of updating the `cacheFile`. - */ - cacheOutFile?: string; - }; - - expect?: { - /** - * Default timeout for expect calls in milliseconds, defaults to 5000ms. - */ - timeout?: number; - }; - - /** - * Limits to use for the agentic loop. - */ - limits?: { - /** - * Maximum number of tokens to consume. The agentic loop will stop after input + output tokens exceed this value. - * Defaults to unlimited. - */ - maxTokens?: number; - - /** - * Maximum number of agentic actions to generate, defaults to 10. - */ - maxActions?: number; - - /** - * Maximum number retries per action, defaults to 3. - */ - maxActionRetries?: number; - }; - - provider?: { - /** - * API to use. - */ - api: "openai"|"openai-compatible"|"anthropic"|"google"; - - /** - * Endpoint to use if different from default. - */ - apiEndpoint?: string; - - /** - * API key for the LLM provider. - */ - apiKey: string; - - /** - * Amount of time to wait for the provider to respond to each request. - */ - apiTimeout?: number; - - /** - * Model identifier within the provider. Required in non-cache mode. - */ - model: string; - }; - - /** - * Secrets to hide from the LLM. - */ - secrets?: { [key: string]: string; }; - - /** - * System prompt for the agent's loop. - */ - systemPrompt?: string; - }): Promise; - /** * Brings page to front (activates tab). */ @@ -5356,243 +5273,6 @@ export interface Page { [Symbol.asyncDispose](): Promise; } -/** - * - */ -export interface PageAgent { - /** - * Extract information from the page using the agentic loop, return it in a given Zod format. - * - * **Usage** - * - * ```js - * await agent.extract('List of items in the cart', z.object({ - * title: z.string().describe('Item title to extract'), - * price: z.string().describe('Item price to extract'), - * }).array()); - * ``` - * - * @param query Task to perform using agentic loop. - * @param schema - * @param options - */ - extract(query: string, schema: Schema): Promise<{ result: InferZodSchema, usage: { turns: number, inputTokens: number, outputTokens: number } }>; - /** - * Emitted when the agent makes a turn. - */ - on(event: 'turn', listener: (data: { - role: string; - - message: string; - - usage?: { - inputTokens: number; - - outputTokens: number; - }; - }) => any): this; - - /** - * Adds an event listener that will be automatically removed after it is triggered once. See `addListener` for more information about this event. - */ - once(event: 'turn', listener: (data: { - role: string; - - message: string; - - usage?: { - inputTokens: number; - - outputTokens: number; - }; - }) => any): this; - - /** - * Emitted when the agent makes a turn. - */ - addListener(event: 'turn', listener: (data: { - role: string; - - message: string; - - usage?: { - inputTokens: number; - - outputTokens: number; - }; - }) => any): this; - - /** - * Removes an event listener added by `on` or `addListener`. - */ - removeListener(event: 'turn', listener: (data: { - role: string; - - message: string; - - usage?: { - inputTokens: number; - - outputTokens: number; - }; - }) => any): this; - - /** - * Removes an event listener added by `on` or `addListener`. - */ - off(event: 'turn', listener: (data: { - role: string; - - message: string; - - usage?: { - inputTokens: number; - - outputTokens: number; - }; - }) => any): this; - - /** - * Emitted when the agent makes a turn. - */ - prependListener(event: 'turn', listener: (data: { - role: string; - - message: string; - - usage?: { - inputTokens: number; - - outputTokens: number; - }; - }) => any): this; - - /** - * Dispose this agent. - */ - dispose(): Promise; - - /** - * Expect certain condition to be met. - * - * **Usage** - * - * ```js - * await agent.expect('"0 items" to be reported'); - * ``` - * - * @param expectation Expectation to assert. - * @param options - */ - expect(expectation: string, options?: { - /** - * All the agentic actions are converted to the Playwright calls and are cached. By default, they are cached globally - * with the `task` as a key. This option allows controlling the cache key explicitly. - */ - cacheKey?: string; - - /** - * Maximum number of retries when generating each action, defaults to context-wide value specified in `agent` - * property. - */ - maxActionRetries?: number; - - /** - * Maximum number of agentic actions to generate, defaults to context-wide value specified in `agent` property. - */ - maxActions?: number; - - /** - * Maximum number of tokens to consume. The agentic loop will stop after input + output tokens exceed this value. - * Defaults to context-wide value specified in `agent` property. - */ - maxTokens?: number; - - /** - * Expect timeout in milliseconds. Defaults to `5000`. The default value can be changed via `expect.timeout` option in - * the config, or by specifying the `expect` property of the - * [`expect`](https://playwright.dev/docs/api/class-page#page-agent-option-expect) option. Pass `0` to disable - * timeout. - */ - timeout?: number; - }): Promise; - - /** - * Perform action using agentic loop. - * - * **Usage** - * - * ```js - * await agent.perform('Click submit button'); - * ``` - * - * @param task Task to perform using agentic loop. - * @param options - */ - perform(task: string, options?: { - /** - * All the agentic actions are converted to the Playwright calls and are cached. By default, they are cached globally - * with the `task` as a key. This option allows controlling the cache key explicitly. - */ - cacheKey?: string; - - /** - * Maximum number of retries when generating each action, defaults to context-wide value specified in `agent` - * property. - */ - maxActionRetries?: number; - - /** - * Maximum number of agentic actions to generate, defaults to context-wide value specified in `agent` property. - */ - maxActions?: number; - - /** - * Maximum number of tokens to consume. The agentic loop will stop after input + output tokens exceed this value. - * Defaults to context-wide value specified in `agent` property. - */ - maxTokens?: number; - - /** - * Perform timeout in milliseconds. Defaults to `5000`. The default value can be changed via `actionTimeout` option in - * the config, or by using the - * [browserContext.setDefaultTimeout(timeout)](https://playwright.dev/docs/api/class-browsercontext#browser-context-set-default-timeout) - * or [page.setDefaultTimeout(timeout)](https://playwright.dev/docs/api/class-page#page-set-default-timeout) methods. - * Pass `0` to disable timeout. - */ - timeout?: number; - }): Promise<{ - usage: { - turns: number; - - inputTokens: number; - - outputTokens: number; - }; - }>; - - /** - * Returns the current token usage for this agent. - * - * **Usage** - * - * ```js - * const usage = await agent.usage(); - * console.log(`Tokens used: ${usage.inputTokens} in, ${usage.outputTokens} out`); - * ``` - * - */ - usage(): Promise<{ - turns: number; - - inputTokens: number; - - outputTokens: number; - }>; - - [Symbol.asyncDispose](): Promise; -} - /** * At every point of time, page exposes its current frame tree via the * [page.mainFrame()](https://playwright.dev/docs/api/class-page#page-main-frame) and diff --git a/packages/playwright-core/ThirdPartyNotices.txt b/packages/playwright-core/ThirdPartyNotices.txt index 339918d781ea2..47d3f0c980c88 100644 --- a/packages/playwright-core/ThirdPartyNotices.txt +++ b/packages/playwright-core/ThirdPartyNotices.txt @@ -5,7 +5,6 @@ THIRD-PARTY SOFTWARE NOTICES AND INFORMATION This project incorporates components from the projects listed below. The original copyright notices and the licenses under which Microsoft received such components are set forth below. Microsoft reserves all rights not expressly granted herein, whether by implication, estoppel or otherwise. - @hono/node-server@1.19.11 (https://github.com/honojs/node-server) -- @lowire/loop@0.0.25 (https://github.com/pavelfeldman/lowire) - @modelcontextprotocol/sdk@1.26.0 (https://github.com/modelcontextprotocol/typescript-sdk) - accepts@2.0.0 (https://github.com/jshttp/accepts) - agent-base@7.1.4 (https://github.com/TooTallNate/proxy-agents) @@ -165,212 +164,6 @@ SOFTWARE. ========================================= END OF @hono/node-server@1.19.11 AND INFORMATION -%% @lowire/loop@0.0.25 NOTICES AND INFORMATION BEGIN HERE -========================================= -Apache License - Version 2.0, January 2004 - http://www.apache.org/licenses/ - - TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION - - 1. Definitions. - - "License" shall mean the terms and conditions for use, reproduction, - and distribution as defined by Sections 1 through 9 of this document. - - "Licensor" shall mean the copyright owner or entity authorized by - the copyright owner that is granting the License. - - "Legal Entity" shall mean the union of the acting entity and all - other entities that control, are controlled by, or are under common - control with that entity. For the purposes of this definition, - "control" means (i) the power, direct or indirect, to cause the - direction or management of such entity, whether by contract or - otherwise, or (ii) ownership of fifty percent (50%) or more of the - outstanding shares, or (iii) beneficial ownership of such entity. - - "You" (or "Your") shall mean an individual or Legal Entity - exercising permissions granted by this License. - - "Source" form shall mean the preferred form for making modifications, - including but not limited to software source code, documentation - source, and configuration files. - - "Object" form shall mean any form resulting from mechanical - transformation or translation of a Source form, including but - not limited to compiled object code, generated documentation, - and conversions to other media types. - - "Work" shall mean the work of authorship, whether in Source or - Object form, made available under the License, as indicated by a - copyright notice that is included in or attached to the work - (an example is provided in the Appendix below). - - "Derivative Works" shall mean any work, whether in Source or Object - form, that is based on (or derived from) the Work and for which the - editorial revisions, annotations, elaborations, or other modifications - represent, as a whole, an original work of authorship. For the purposes - of this License, Derivative Works shall not include works that remain - separable from, or merely link (or bind by name) to the interfaces of, - the Work and Derivative Works thereof. - - "Contribution" shall mean any work of authorship, including - the original version of the Work and any modifications or additions - to that Work or Derivative Works thereof, that is intentionally - submitted to Licensor for inclusion in the Work by the copyright owner - or by an individual or Legal Entity authorized to submit on behalf of - the copyright owner. For the purposes of this definition, "submitted" - means any form of electronic, verbal, or written communication sent - to the Licensor or its representatives, including but not limited to - communication on electronic mailing lists, source code control systems, - and issue tracking systems that are managed by, or on behalf of, the - Licensor for the purpose of discussing and improving the Work, but - excluding communication that is conspicuously marked or otherwise - designated in writing by the copyright owner as "Not a Contribution." - - "Contributor" shall mean Licensor and any individual or Legal Entity - on behalf of whom a Contribution has been received by Licensor and - subsequently incorporated within the Work. - - 2. Grant of Copyright License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - copyright license to reproduce, prepare Derivative Works of, - publicly display, publicly perform, sublicense, and distribute the - Work and such Derivative Works in Source or Object form. - - 3. Grant of Patent License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - (except as stated in this section) patent license to make, have made, - use, offer to sell, sell, import, and otherwise transfer the Work, - where such license applies only to those patent claims licensable - by such Contributor that are necessarily infringed by their - Contribution(s) alone or by combination of their Contribution(s) - with the Work to which such Contribution(s) was submitted. If You - institute patent litigation against any entity (including a - cross-claim or counterclaim in a lawsuit) alleging that the Work - or a Contribution incorporated within the Work constitutes direct - or contributory patent infringement, then any patent licenses - granted to You under this License for that Work shall terminate - as of the date such litigation is filed. - - 4. Redistribution. You may reproduce and distribute copies of the - Work or Derivative Works thereof in any medium, with or without - modifications, and in Source or Object form, provided that You - meet the following conditions: - - (a) You must give any other recipients of the Work or - Derivative Works a copy of this License; and - - (b) You must cause any modified files to carry prominent notices - stating that You changed the files; and - - (c) You must retain, in the Source form of any Derivative Works - that You distribute, all copyright, patent, trademark, and - attribution notices from the Source form of the Work, - excluding those notices that do not pertain to any part of - the Derivative Works; and - - (d) If the Work includes a "NOTICE" text file as part of its - distribution, then any Derivative Works that You distribute must - include a readable copy of the attribution notices contained - within such NOTICE file, excluding those notices that do not - pertain to any part of the Derivative Works, in at least one - of the following places: within a NOTICE text file distributed - as part of the Derivative Works; within the Source form or - documentation, if provided along with the Derivative Works; or, - within a display generated by the Derivative Works, if and - wherever such third-party notices normally appear. The contents - of the NOTICE file are for informational purposes only and - do not modify the License. You may add Your own attribution - notices within Derivative Works that You distribute, alongside - or as an addendum to the NOTICE text from the Work, provided - that such additional attribution notices cannot be construed - as modifying the License. - - You may add Your own copyright statement to Your modifications and - may provide additional or different license terms and conditions - for use, reproduction, or distribution of Your modifications, or - for any such Derivative Works as a whole, provided Your use, - reproduction, and distribution of the Work otherwise complies with - the conditions stated in this License. - - 5. Submission of Contributions. Unless You explicitly state otherwise, - any Contribution intentionally submitted for inclusion in the Work - by You to the Licensor shall be under the terms and conditions of - this License, without any additional terms or conditions. - Notwithstanding the above, nothing herein shall supersede or modify - the terms of any separate license agreement you may have executed - with Licensor regarding such Contributions. - - 6. Trademarks. This License does not grant permission to use the trade - names, trademarks, service marks, or product names of the Licensor, - except as required for reasonable and customary use in describing the - origin of the Work and reproducing the content of the NOTICE file. - - 7. Disclaimer of Warranty. Unless required by applicable law or - agreed to in writing, Licensor provides the Work (and each - Contributor provides its Contributions) on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or - implied, including, without limitation, any warranties or conditions - of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A - PARTICULAR PURPOSE. You are solely responsible for determining the - appropriateness of using or redistributing the Work and assume any - risks associated with Your exercise of permissions under this License. - - 8. Limitation of Liability. In no event and under no legal theory, - whether in tort (including negligence), contract, or otherwise, - unless required by applicable law (such as deliberate and grossly - negligent acts) or agreed to in writing, shall any Contributor be - liable to You for damages, including any direct, indirect, special, - incidental, or consequential damages of any character arising as a - result of this License or out of the use or inability to use the - Work (including but not limited to damages for loss of goodwill, - work stoppage, computer failure or malfunction, or any and all - other commercial damages or losses), even if such Contributor - has been advised of the possibility of such damages. - - 9. Accepting Warranty or Additional Liability. While redistributing - the Work or Derivative Works thereof, You may choose to offer, - and charge a fee for, acceptance of support, warranty, indemnity, - or other liability obligations and/or rights consistent with this - License. However, in accepting such obligations, You may act only - on Your own behalf and on Your sole responsibility, not on behalf - of any other Contributor, and only if You agree to indemnify, - defend, and hold each Contributor harmless for any liability - incurred by, or claims asserted against, such Contributor by reason - of your accepting any such warranty or additional liability. - - END OF TERMS AND CONDITIONS - - APPENDIX: How to apply the Apache License to your work. - - To apply the Apache License to your work, attach the following - boilerplate notice, with the fields enclosed by brackets "[]" - replaced with your own identifying information. (Don't include - the brackets!) The text should be enclosed in the appropriate - comment syntax for the file format. We also recommend that a - file or class name and description of purpose be included on the - same "printed page" as the copyright notice for easier - identification within third-party archives. - - Copyright (c) Microsoft Corporation. - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. -========================================= -END OF @lowire/loop@0.0.25 AND INFORMATION - %% @modelcontextprotocol/sdk@1.26.0 NOTICES AND INFORMATION BEGIN HERE ========================================= MIT License @@ -3754,6 +3547,6 @@ END OF zod@4.3.5 AND INFORMATION SUMMARY BEGIN HERE ========================================= -Total Packages: 134 +Total Packages: 133 ========================================= END OF SUMMARY \ No newline at end of file diff --git a/packages/playwright-core/bundles/mcp/package-lock.json b/packages/playwright-core/bundles/mcp/package-lock.json index bf9708185256f..04652af553b7a 100644 --- a/packages/playwright-core/bundles/mcp/package-lock.json +++ b/packages/playwright-core/bundles/mcp/package-lock.json @@ -8,7 +8,6 @@ "name": "mcp-bundle", "version": "0.0.1", "dependencies": { - "@lowire/loop": "^0.0.25", "@modelcontextprotocol/sdk": "^1.26.0", "zod": "^4.3.5", "zod-to-json-schema": "^3.25.1" @@ -26,15 +25,6 @@ "hono": "^4" } }, - "node_modules/@lowire/loop": { - "version": "0.0.25", - "resolved": "https://registry.npmjs.org/@lowire/loop/-/loop-0.0.25.tgz", - "integrity": "sha512-tBfQ3+m+BelMSRqmdpOqLQPervF6PoS4IV0JE0ga1fJnOaPRnOfjkGecyaR45Hp7VuhY9oBQdVlDkAXoOkjk1w==", - "license": "Apache-2.0", - "engines": { - "node": ">=20" - } - }, "node_modules/@modelcontextprotocol/sdk": { "version": "1.26.0", "resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.26.0.tgz", diff --git a/packages/playwright-core/bundles/mcp/package.json b/packages/playwright-core/bundles/mcp/package.json index 2536cca75f653..c22e7132ef04e 100644 --- a/packages/playwright-core/bundles/mcp/package.json +++ b/packages/playwright-core/bundles/mcp/package.json @@ -3,7 +3,6 @@ "version": "0.0.1", "private": true, "dependencies": { - "@lowire/loop": "^0.0.25", "@modelcontextprotocol/sdk": "^1.26.0", "zod": "^4.3.5", "zod-to-json-schema": "^3.25.1" diff --git a/packages/playwright-core/bundles/mcp/src/mcpBundleImpl.ts b/packages/playwright-core/bundles/mcp/src/mcpBundleImpl.ts index aa3b37ae1bf28..97a96a3c7fe0c 100644 --- a/packages/playwright-core/bundles/mcp/src/mcpBundleImpl.ts +++ b/packages/playwright-core/bundles/mcp/src/mcpBundleImpl.ts @@ -23,6 +23,5 @@ export { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js' export { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js'; export { StreamableHTTPClientTransport } from '@modelcontextprotocol/sdk/client/streamableHttp.js'; export { CallToolRequestSchema, ListRootsRequestSchema, ListToolsRequestSchema, PingRequestSchema, ProgressNotificationSchema } from '@modelcontextprotocol/sdk/types.js'; -export { Loop } from '@lowire/loop'; export * as z from 'zod'; export { zodToJsonSchema } from 'zod-to-json-schema'; diff --git a/packages/playwright-core/src/client/api.ts b/packages/playwright-core/src/client/api.ts index 9db785ed4f205..fba51a2dd1ac5 100644 --- a/packages/playwright-core/src/client/api.ts +++ b/packages/playwright-core/src/client/api.ts @@ -38,7 +38,6 @@ export { JSHandle } from './jsHandle'; export { Request, Response, Route, WebSocket, WebSocketRoute } from './network'; export { APIRequest, APIRequestContext, APIResponse } from './fetch'; export { Page } from './page'; -export { PageAgent } from './pageAgent'; export { Selectors } from './selectors'; export { Tracing } from './tracing'; export { Video } from './video'; diff --git a/packages/playwright-core/src/client/connection.ts b/packages/playwright-core/src/client/connection.ts index bc48d781595e2..d727fe3590255 100644 --- a/packages/playwright-core/src/client/connection.ts +++ b/packages/playwright-core/src/client/connection.ts @@ -42,8 +42,6 @@ import { Worker } from './worker'; import { WritableStream } from './writableStream'; import { ValidationError, findValidator } from '../protocol/validator'; import { rewriteErrorMessage } from '../utils/isomorphic/stackTrace'; -import { PageAgent } from './pageAgent'; - import type { ClientInstrumentation } from './clientInstrumentation'; import type { HeadersArray } from './types'; import type { ValidatorContext } from '../protocol/validator'; @@ -300,9 +298,6 @@ export class Connection extends EventEmitter { case 'Page': result = new Page(parent, type, guid, initializer); break; - case 'PageAgent': - result = new PageAgent(parent, type, guid, initializer); - break; case 'Playwright': result = new Playwright(parent, type, guid, initializer); break; diff --git a/packages/playwright-core/src/client/events.ts b/packages/playwright-core/src/client/events.ts index f56c52f3a01fd..cede73640cf3b 100644 --- a/packages/playwright-core/src/client/events.ts +++ b/packages/playwright-core/src/client/events.ts @@ -78,10 +78,6 @@ export const Events = { Worker: 'worker', }, - PageAgent: { - Turn: 'turn', - }, - WebSocket: { Close: 'close', Error: 'socketerror', diff --git a/packages/playwright-core/src/client/page.ts b/packages/playwright-core/src/client/page.ts index bb4a249b3b656..add96c395e781 100644 --- a/packages/playwright-core/src/client/page.ts +++ b/packages/playwright-core/src/client/page.ts @@ -43,8 +43,6 @@ import { urlMatches, urlMatchesEqual } from '../utils/isomorphic/urlMatch'; import { LongStandingScope } from '../utils/isomorphic/manualPromise'; import { isObject, isRegExp, isString } from '../utils/isomorphic/rtti'; import { ConsoleMessage } from './consoleMessage'; -import { PageAgent } from './pageAgent'; - import type { BrowserContext } from './browserContext'; import type { Clock } from './clock'; import type { APIRequestContext } from './fetch'; @@ -861,29 +859,6 @@ export class Page extends ChannelOwner implements api.Page return result.pdf; } - async agent(options: Parameters[0] = {}) { - const params: channels.PageAgentParams = { - api: options.provider?.api, - apiEndpoint: options.provider?.apiEndpoint, - apiKey: options.provider?.apiKey, - apiTimeout: options.provider?.apiTimeout, - apiCacheFile: (options.provider as any)?._apiCacheFile, - doNotRenderActive: (options as any)._doNotRenderActive, - model: options.provider?.model, - cacheFile: options.cache?.cacheFile, - cacheOutFile: options.cache?.cacheOutFile, - maxTokens: options.limits?.maxTokens, - maxActions: options.limits?.maxActions, - maxActionRetries: options.limits?.maxActionRetries, - secrets: options.secrets ? Object.entries(options.secrets).map(([name, value]) => ({ name, value })) : undefined, - systemPrompt: options.systemPrompt, - }; - const { agent } = await this._channel.agent(params); - const pageAgent = PageAgent.from(agent); - pageAgent._expectTimeout = options?.expect?.timeout; - return pageAgent; - } - async _snapshotForAI(options: TimeoutOptions & { track?: string } = {}): Promise<{ full: string, incremental?: string }> { return await this._channel.snapshotForAI({ timeout: this._timeoutSettings.timeout(options), track: options.track }); } diff --git a/packages/playwright-core/src/client/pageAgent.ts b/packages/playwright-core/src/client/pageAgent.ts deleted file mode 100644 index bcf0cdb839d98..0000000000000 --- a/packages/playwright-core/src/client/pageAgent.ts +++ /dev/null @@ -1,68 +0,0 @@ -/** - * Copyright 2017 Google Inc. All rights reserved. - * Modifications copyright (c) Microsoft Corporation. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { ChannelOwner } from './channelOwner'; -import { Events } from './events'; -import { Page } from './page'; - -import type * as api from '../../types/types'; -import type * as channels from '@protocol/channels'; - -export class PageAgent extends ChannelOwner implements api.PageAgent { - private _page: Page; - _expectTimeout?: number; - - static from(channel: channels.PageAgentChannel): PageAgent { - return (channel as any)._object; - } - - constructor(parent: ChannelOwner, type: string, guid: string, initializer: channels.PageAgentInitializer) { - super(parent, type, guid, initializer); - this._page = Page.from(initializer.page); - this._channel.on('turn', params => this.emit(Events.PageAgent.Turn, params)); - } - - async expect(expectation: string, options: channels.PageAgentExpectOptions = {}) { - const timeout = options.timeout ?? this._expectTimeout ?? 5000; - await this._channel.expect({ expectation, ...options, timeout }); - } - - async perform(task: string, options: channels.PageAgentPerformOptions = {}) { - const timeout = this._page._timeoutSettings.timeout(options); - const { usage } = await this._channel.perform({ task, ...options, timeout }); - return { usage }; - } - - async extract(query: string, schema: Schema, options: channels.PageAgentExtractOptions = {}): Promise<{ result: any, usage: channels.AgentUsage }> { - const timeout = this._page._timeoutSettings.timeout(options); - const { result, usage } = await this._channel.extract({ query, schema: this._page._platform.zodToJsonSchema(schema), ...options, timeout }); - return { result, usage }; - } - - async usage() { - const { usage } = await this._channel.usage({}); - return usage; - } - - async dispose() { - await this._channel.dispose(); - } - - async [Symbol.asyncDispose]() { - await this.dispose(); - } -} diff --git a/packages/playwright-core/src/mcpBundle.ts b/packages/playwright-core/src/mcpBundle.ts index fb233f02fcdcb..a9673ad6f02d8 100644 --- a/packages/playwright-core/src/mcpBundle.ts +++ b/packages/playwright-core/src/mcpBundle.ts @@ -31,7 +31,6 @@ const ListRootsRequestSchema: typeof import('@modelcontextprotocol/sdk/types.js' const ProgressNotificationSchema: typeof import('@modelcontextprotocol/sdk/types.js').ProgressNotificationSchema = bundle.ProgressNotificationSchema; const ListToolsRequestSchema: typeof import('@modelcontextprotocol/sdk/types.js').ListToolsRequestSchema = bundle.ListToolsRequestSchema; const PingRequestSchema: typeof import('@modelcontextprotocol/sdk/types.js').PingRequestSchema = bundle.PingRequestSchema; -const Loop: typeof import('@lowire/loop').Loop = bundle.Loop; const z: typeof import('zod') = bundle.z; export { @@ -49,6 +48,5 @@ export { ListToolsRequestSchema, PingRequestSchema, ProgressNotificationSchema, - Loop, z, }; diff --git a/packages/playwright-core/src/protocol/validator.ts b/packages/playwright-core/src/protocol/validator.ts index 52380c8b49b31..735ca9d329b33 100644 --- a/packages/playwright-core/src/protocol/validator.ts +++ b/packages/playwright-core/src/protocol/validator.ts @@ -849,7 +849,6 @@ scheme.WorkerWaitForEventInfoParams = tType('EventTargetWaitForEventInfoParams') scheme.WebSocketWaitForEventInfoParams = tType('EventTargetWaitForEventInfoParams'); scheme.ElectronApplicationWaitForEventInfoParams = tType('EventTargetWaitForEventInfoParams'); scheme.AndroidDeviceWaitForEventInfoParams = tType('EventTargetWaitForEventInfoParams'); -scheme.PageAgentWaitForEventInfoParams = tType('EventTargetWaitForEventInfoParams'); scheme.EventTargetWaitForEventInfoResult = tOptional(tObject({})); scheme.BrowserContextWaitForEventInfoResult = tType('EventTargetWaitForEventInfoResult'); scheme.PageWaitForEventInfoResult = tType('EventTargetWaitForEventInfoResult'); @@ -857,7 +856,6 @@ scheme.WorkerWaitForEventInfoResult = tType('EventTargetWaitForEventInfoResult') scheme.WebSocketWaitForEventInfoResult = tType('EventTargetWaitForEventInfoResult'); scheme.ElectronApplicationWaitForEventInfoResult = tType('EventTargetWaitForEventInfoResult'); scheme.AndroidDeviceWaitForEventInfoResult = tType('EventTargetWaitForEventInfoResult'); -scheme.PageAgentWaitForEventInfoResult = tType('EventTargetWaitForEventInfoResult'); scheme.BrowserContextInitializer = tObject({ requestContext: tChannel(['APIRequestContext']), tracing: tChannel(['Tracing']), @@ -1570,25 +1568,6 @@ scheme.PageUpdateSubscriptionParams = tObject({ enabled: tBoolean, }); scheme.PageUpdateSubscriptionResult = tOptional(tObject({})); -scheme.PageAgentParams = tObject({ - api: tOptional(tString), - apiKey: tOptional(tString), - apiEndpoint: tOptional(tString), - apiTimeout: tOptional(tInt), - apiCacheFile: tOptional(tString), - cacheFile: tOptional(tString), - cacheOutFile: tOptional(tString), - doNotRenderActive: tOptional(tBoolean), - maxActions: tOptional(tInt), - maxActionRetries: tOptional(tInt), - maxTokens: tOptional(tInt), - model: tOptional(tString), - secrets: tOptional(tArray(tType('NameValue'))), - systemPrompt: tOptional(tString), -}); -scheme.PageAgentResult = tObject({ - agent: tChannel(['PageAgent']), -}); scheme.PageSetDockTileParams = tObject({ image: tBinary, }); @@ -2992,60 +2971,3 @@ scheme.JsonPipeSendParams = tObject({ scheme.JsonPipeSendResult = tOptional(tObject({})); scheme.JsonPipeCloseParams = tOptional(tObject({})); scheme.JsonPipeCloseResult = tOptional(tObject({})); -scheme.PageAgentInitializer = tObject({ - page: tChannel(['Page']), -}); -scheme.PageAgentTurnEvent = tObject({ - role: tString, - message: tString, - usage: tOptional(tObject({ - inputTokens: tInt, - outputTokens: tInt, - })), -}); -scheme.PageAgentPerformParams = tObject({ - task: tString, - maxActions: tOptional(tInt), - maxActionRetries: tOptional(tInt), - maxTokens: tOptional(tInt), - cacheKey: tOptional(tString), - timeout: tOptional(tInt), -}); -scheme.PageAgentPerformResult = tObject({ - usage: tType('AgentUsage'), -}); -scheme.PageAgentExpectParams = tObject({ - expectation: tString, - maxActions: tOptional(tInt), - maxActionRetries: tOptional(tInt), - maxTokens: tOptional(tInt), - cacheKey: tOptional(tString), - timeout: tOptional(tInt), -}); -scheme.PageAgentExpectResult = tObject({ - usage: tType('AgentUsage'), -}); -scheme.PageAgentExtractParams = tObject({ - query: tString, - schema: tAny, - maxActions: tOptional(tInt), - maxActionRetries: tOptional(tInt), - maxTokens: tOptional(tInt), - cacheKey: tOptional(tString), - timeout: tOptional(tInt), -}); -scheme.PageAgentExtractResult = tObject({ - result: tAny, - usage: tType('AgentUsage'), -}); -scheme.PageAgentDisposeParams = tOptional(tObject({})); -scheme.PageAgentDisposeResult = tOptional(tObject({})); -scheme.PageAgentUsageParams = tOptional(tObject({})); -scheme.PageAgentUsageResult = tObject({ - usage: tType('AgentUsage'), -}); -scheme.AgentUsage = tObject({ - turns: tInt, - inputTokens: tInt, - outputTokens: tInt, -}); diff --git a/packages/playwright-core/src/server/agent/DEPS.list b/packages/playwright-core/src/server/agent/DEPS.list deleted file mode 100644 index f0fade51be83d..0000000000000 --- a/packages/playwright-core/src/server/agent/DEPS.list +++ /dev/null @@ -1,11 +0,0 @@ -[*] -../browserContext.ts -../errors.ts -../page.ts -../progress.ts -../utils/expectUtils.ts -../utils/crypto.ts -../../mcpBundle.ts -../../protocol/ -../../utilsBundle.ts -../../utils/isomorphic/ diff --git a/packages/playwright-core/src/server/agent/actionRunner.ts b/packages/playwright-core/src/server/agent/actionRunner.ts deleted file mode 100644 index a644dd69dfb1c..0000000000000 --- a/packages/playwright-core/src/server/agent/actionRunner.ts +++ /dev/null @@ -1,346 +0,0 @@ -/** - * Copyright (c) Microsoft Corporation. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { formatMatcherMessage, serializeExpectedTextValues, simpleMatcherUtils } from '../utils/expectUtils'; -import { constructURLBasedOnBaseURL } from '../../utils/isomorphic/urlMatch'; -import { parseRegex } from '../../utils/isomorphic/stringUtils'; -import { monotonicTime } from '../../utils/isomorphic/time'; -import { createGuid } from '../utils/crypto'; -import { parseAriaSnapshotUnsafe } from '../../utils/isomorphic/ariaSnapshot'; -import { asLocatorDescription } from '../../utils/isomorphic/locatorGenerators'; -import { yaml } from '../../utilsBundle'; -import { serializeError } from '../errors'; -import { rewriteErrorMessage } from '../../utils/isomorphic/stackTrace'; -import { applySecrets, redactSecrets } from './context'; - -import type * as actions from './actions'; -import type { Page } from '../page'; -import type { Progress } from '../progress'; -import type { NameValue } from '@protocol/channels'; -import type { Frame } from '../frames'; -import type { CallMetadata } from '../instrumentation'; -import type * as channels from '@protocol/channels'; -import type { FrameExpectParams } from '@injected/injectedScript'; - -export async function runAction(progress: Progress, mode: 'generate' | 'run', page: Page, action: actions.Action, secrets: NameValue[]) { - const parentMetadata = progress.metadata; - const frame = page.mainFrame(); - const callMetadata = callMetadataForAction(progress, frame, action, mode); - callMetadata.log = parentMetadata.log; - progress.metadata = callMetadata; - - await frame.instrumentation.onBeforeCall(frame, callMetadata, parentMetadata.id); - let error: Error | undefined; - const result = await innerRunAction(progress, mode, page, action, secrets).catch(e => error = e); - callMetadata.endTime = monotonicTime(); - callMetadata.error = error ? serializeError(error) : undefined; - callMetadata.result = error ? undefined : result; - await frame.instrumentation.onAfterCall(frame, callMetadata); - if (error) { - rewriteErrorMessage(error, redactSecrets(error.message, secrets)); - throw error; - } - return result; -} - -async function innerRunAction(progress: Progress, mode: 'generate' | 'run', page: Page, action: actions.Action, secrets: NameValue[]) { - const frame = page.mainFrame(); - // Disable auto-waiting to avoid timeouts, model has seen the snapshot anyway. - const commonOptions = { strict: true, noAutoWaiting: mode === 'generate' }; - switch (action.method) { - case 'navigate': - await frame.goto(progress, action.url); - break; - case 'click': - await frame.click(progress, action.selector, { - button: action.button, - clickCount: action.clickCount, - modifiers: action.modifiers, - ...commonOptions - }); - break; - case 'drag': - await frame.dragAndDrop(progress, action.sourceSelector, action.targetSelector, { ...commonOptions }); - break; - case 'hover': - await frame.hover(progress, action.selector, { - modifiers: action.modifiers, - ...commonOptions - }); - break; - case 'selectOption': - const labels = action.labels.map(label => applySecrets(label, secrets)); - await frame.selectOption(progress, action.selector, [], labels.map(a => ({ label: a })), { ...commonOptions }); - break; - case 'pressKey': - await page.keyboard.press(progress, action.key); - break; - case 'pressSequentially': { - await frame.type(progress, action.selector, applySecrets(action.text, secrets), { ...commonOptions }); - if (action.submit) - await page.keyboard.press(progress, 'Enter'); - break; - } - case 'fill': { - await frame.fill(progress, action.selector, applySecrets(action.text, secrets), { ...commonOptions }); - if (action.submit) - await page.keyboard.press(progress, 'Enter'); - break; - } - case 'setChecked': - if (action.checked) - await frame.check(progress, action.selector, { ...commonOptions }); - else - await frame.uncheck(progress, action.selector, { ...commonOptions }); - break; - case 'expectVisible': { - await runExpect(frame, progress, mode, action.selector, { expression: 'to.be.visible', isNot: !!action.isNot }, 'visible', 'toBeVisible', ''); - break; - } - case 'expectValue': { - if (action.type === 'textbox' || action.type === 'combobox' || action.type === 'slider') { - const value = applySecrets(action.value, secrets); - const expectedText = serializeExpectedTextValues([value]); - await runExpect(frame, progress, mode, action.selector, { expression: 'to.have.value', expectedText, isNot: !!action.isNot }, value, 'toHaveValue', 'expected'); - } else if (action.type === 'checkbox' || action.type === 'radio') { - const expectedValue = { checked: action.value === 'true' }; - await runExpect(frame, progress, mode, action.selector, { selector: action.selector, expression: 'to.be.checked', expectedValue, isNot: !!action.isNot }, action.value ? 'checked' : 'unchecked', 'toBeChecked', ''); - } else { - throw new Error(`Unsupported element type: ${action.type}`); - } - break; - } - case 'expectAria': { - const template = applySecrets(action.template, secrets); - const expectedValue = parseAriaSnapshotUnsafe(yaml, template); - await runExpect(frame, progress, mode, 'body', { expression: 'to.match.aria', expectedValue, isNot: !!action.isNot }, '\n' + template, 'toMatchAriaSnapshot', 'expected'); - break; - } - case 'expectURL': { - if (!action.regex && !action.value) - throw new Error('Either url or regex must be provided'); - if (action.regex && action.value) - throw new Error('Only one of url or regex can be provided'); - const expected = action.regex ? parseRegex(action.regex) : constructURLBasedOnBaseURL(page.browserContext._options.baseURL, action.value!); - const expectedText = serializeExpectedTextValues([expected]); - await runExpect(frame, progress, mode, undefined, { expression: 'to.have.url', expectedText, isNot: !!action.isNot }, expected, 'toHaveURL', 'expected'); - break; - } - case 'expectTitle': { - const value = applySecrets(action.value, secrets); - const expectedText = serializeExpectedTextValues([value], { normalizeWhiteSpace: true }); - await runExpect(frame, progress, mode, undefined, { expression: 'to.have.title', expectedText, isNot: !!action.isNot }, value, 'toHaveTitle', 'expected'); - break; - } - } -} - -async function runExpect(frame: Frame, progress: Progress, mode: 'generate' | 'run', selector: string | undefined, options: FrameExpectParams, expected: string | RegExp, matcherName: string, expectation: string) { - const result = await frame.expect(progress, selector, { - ...options, - // When generating, we want the expect to pass or fail immediately and give feedback to the model. - noAutoWaiting: mode === 'generate', - timeoutForLogs: mode === 'generate' ? undefined : progress.timeout, - }); - if (!result.matches === !options.isNot) { - const received = matcherName === 'toMatchAriaSnapshot' ? '\n' + result.received.raw : result.received; - const expectedSuffix = typeof expected === 'string' ? '' : ' pattern'; - const expectedDisplay = typeof expected === 'string' ? expected : expected.toString(); - throw new Error(formatMatcherMessage(simpleMatcherUtils, { - isNot: options.isNot, - matcherName, - expectation, - locator: selector ? asLocatorDescription('javascript', selector) : undefined, - timedOut: result.timedOut, - timeout: mode === 'generate' ? undefined : progress.timeout, - printedExpected: options.isNot ? `Expected${expectedSuffix}: not ${expectedDisplay}` : `Expected${expectedSuffix}: ${expectedDisplay}`, - printedReceived: result.errorMessage ? '' : `Received: ${received}`, - errorMessage: result.errorMessage, - // Note: we are not passing call log, because it will be automatically appended on the client side, - // as a part of the agent.{perform,expect} call. - })); - } -} - -export function traceParamsForAction(progress: Progress, action: actions.Action, mode: 'generate' | 'run'): { title?: string, type: string, method: string, params: any } { - const timeout = progress.timeout; - switch (action.method) { - case 'navigate': { - const params: channels.FrameGotoParams = { - url: action.url, - timeout, - }; - return { type: 'Frame', method: 'goto', params }; - } - case 'click': { - const params: channels.FrameClickParams = { - selector: action.selector, - strict: true, - modifiers: action.modifiers, - button: action.button, - clickCount: action.clickCount, - timeout, - }; - return { type: 'Frame', method: 'click', params }; - } - case 'drag': { - const params: channels.FrameDragAndDropParams = { - source: action.sourceSelector, - target: action.targetSelector, - timeout, - }; - return { type: 'Frame', method: 'dragAndDrop', params }; - } - case 'hover': { - const params: channels.FrameHoverParams = { - selector: action.selector, - modifiers: action.modifiers, - timeout, - }; - return { type: 'Frame', method: 'hover', params }; - } - case 'pressKey': { - const params: channels.PageKeyboardPressParams = { - key: action.key, - }; - return { type: 'Page', method: 'keyboardPress', params }; - } - case 'pressSequentially': { - const params: channels.FrameTypeParams = { - selector: action.selector, - text: action.text, - timeout, - }; - return { type: 'Frame', method: 'type', params }; - } - case 'fill': { - const params: channels.FrameFillParams = { - selector: action.selector, - strict: true, - value: action.text, - timeout, - }; - return { type: 'Frame', method: 'fill', params }; - } - case 'setChecked': { - if (action.checked) { - const params: channels.FrameCheckParams = { - selector: action.selector, - strict: true, - timeout, - }; - return { type: 'Frame', method: 'check', params }; - } else { - const params: channels.FrameUncheckParams = { - selector: action.selector, - strict: true, - timeout, - }; - return { type: 'Frame', method: 'uncheck', params }; - } - } - case 'selectOption': { - const params: channels.FrameSelectOptionParams = { - selector: action.selector, - strict: true, - options: action.labels.map(label => ({ label })), - timeout, - }; - return { type: 'Frame', method: 'selectOption', params }; - } - case 'expectValue': { - if (action.type === 'textbox' || action.type === 'combobox' || action.type === 'slider') { - const expectedText = serializeExpectedTextValues([action.value]); - const params: channels.FrameExpectParams = { - selector: action.selector, - expression: 'to.have.value', - expectedText, - isNot: !!action.isNot, - timeout, - }; - return { type: 'Frame', method: 'expect', title: 'Expect Value', params }; - } else if (action.type === 'checkbox' || action.type === 'radio') { - // TODO: provide serialized expected value - const params: channels.FrameExpectParams = { - selector: action.selector, - expression: 'to.be.checked', - isNot: !!action.isNot, - timeout, - }; - return { type: 'Frame', method: 'expect', title: 'Expect Checked', params }; - } else { - throw new Error(`Unsupported element type: ${action.type}`); - } - } - case 'expectVisible': { - const params: channels.FrameExpectParams = { - selector: action.selector, - expression: 'to.be.visible', - isNot: !!action.isNot, - timeout, - }; - return { type: 'Frame', method: 'expect', title: 'Expect Visible', params }; - } - case 'expectAria': { - // TODO: provide serialized expected value - const params: channels.FrameExpectParams = { - selector: 'body', - expression: 'to.match.snapshot', - expectedText: [], - isNot: !!action.isNot, - timeout, - }; - return { type: 'Frame', method: 'expect', title: 'Expect Aria Snapshot', params }; - } - case 'expectURL': { - const expected = action.regex ? parseRegex(action.regex) : action.value!; - const expectedText = serializeExpectedTextValues([expected]); - const params: channels.FrameExpectParams = { - selector: undefined, - expression: 'to.have.url', - expectedText, - isNot: !!action.isNot, - timeout, - }; - return { type: 'Frame', method: 'expect', title: 'Expect URL', params }; - } - case 'expectTitle': { - const expectedText = serializeExpectedTextValues([action.value], { normalizeWhiteSpace: true }); - const params: channels.FrameExpectParams = { - selector: undefined, - expression: 'to.have.title', - expectedText, - isNot: !!action.isNot, - timeout, - }; - return { type: 'Frame', method: 'expect', title: 'Expect Title', params }; - } - } -} - -function callMetadataForAction(progress: Progress, frame: Frame, action: actions.Action, mode: 'generate' | 'run'): CallMetadata { - const callMetadata: CallMetadata = { - id: `call@${createGuid()}`, - objectId: frame.guid, - pageId: frame._page.guid, - frameId: frame.guid, - startTime: monotonicTime(), - endTime: 0, - log: [], - ...traceParamsForAction(progress, action, mode), - }; - return callMetadata; -} diff --git a/packages/playwright-core/src/server/agent/actions.ts b/packages/playwright-core/src/server/agent/actions.ts deleted file mode 100644 index ae3600f355671..0000000000000 --- a/packages/playwright-core/src/server/agent/actions.ts +++ /dev/null @@ -1,153 +0,0 @@ -/** - * Copyright (c) Microsoft Corporation. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { z as zod } from '../../mcpBundle'; -import type * as z from 'zod'; - -const modifiersSchema = zod.array( - zod.enum(['Alt', 'Control', 'ControlOrMeta', 'Meta', 'Shift']) -); - -const navigateActionSchema = zod.object({ - method: zod.literal('navigate'), - url: zod.string(), -}); -export type NavigateAction = z.infer; - -const clickActionSchema = zod.object({ - method: zod.literal('click'), - selector: zod.string(), - button: zod.enum(['left', 'right', 'middle']).optional(), - clickCount: zod.number().optional(), - modifiers: modifiersSchema.optional(), -}); -export type ClickAction = z.infer; - -const dragActionSchema = zod.object({ - method: zod.literal('drag'), - sourceSelector: zod.string(), - targetSelector: zod.string(), -}); -export type DragAction = z.infer; - -const hoverActionSchema = zod.object({ - method: zod.literal('hover'), - selector: zod.string(), - modifiers: modifiersSchema.optional(), -}); -export type HoverAction = z.infer; - -const selectOptionActionSchema = zod.object({ - method: zod.literal('selectOption'), - selector: zod.string(), - labels: zod.array(zod.string()), -}); -export type SelectOptionAction = z.infer; - -const pressActionSchema = zod.object({ - method: zod.literal('pressKey'), - key: zod.string(), -}); -export type PressAction = z.infer; - -const pressSequentiallyActionSchema = zod.object({ - method: zod.literal('pressSequentially'), - selector: zod.string(), - text: zod.string(), - submit: zod.boolean().optional(), -}); -export type PressSequentiallyAction = z.infer; - -const fillActionSchema = zod.object({ - method: zod.literal('fill'), - selector: zod.string(), - text: zod.string(), - submit: zod.boolean().optional(), -}); -export type FillAction = z.infer; - -const setCheckedSchema = zod.object({ - method: zod.literal('setChecked'), - selector: zod.string(), - checked: zod.boolean(), -}); -export type SetChecked = z.infer; - -const expectVisibleSchema = zod.object({ - method: zod.literal('expectVisible'), - selector: zod.string(), - isNot: zod.boolean().optional(), -}); -export type ExpectVisible = z.infer; - -const expectValueSchema = zod.object({ - method: zod.literal('expectValue'), - selector: zod.string(), - type: zod.enum(['textbox', 'checkbox', 'radio', 'combobox', 'slider']), - value: zod.string(), - isNot: zod.boolean().optional(), -}); -export type ExpectValue = z.infer; - -const expectAriaSchema = zod.object({ - method: zod.literal('expectAria'), - template: zod.string(), - isNot: zod.boolean().optional(), -}); -export type ExpectAria = z.infer; - -const expectURLSchema = zod.object({ - method: zod.literal('expectURL'), - value: zod.string().optional(), - regex: zod.string().optional(), - isNot: zod.boolean().optional(), -}); -export type ExpectURL = z.infer; - -const expectTitleSchema = zod.object({ - method: zod.literal('expectTitle'), - value: zod.string(), - isNot: zod.boolean().optional(), -}); -export type ExpectTitle = z.infer; - -const actionSchema = zod.discriminatedUnion('method', [ - navigateActionSchema, - clickActionSchema, - dragActionSchema, - hoverActionSchema, - selectOptionActionSchema, - pressActionSchema, - pressSequentiallyActionSchema, - fillActionSchema, - setCheckedSchema, - expectVisibleSchema, - expectValueSchema, - expectAriaSchema, - expectURLSchema, - expectTitleSchema, -]); -export type Action = z.infer; - -const actionWithCodeSchema = actionSchema.and(zod.object({ - code: zod.string(), -})); -export type ActionWithCode = z.infer; - -export const cachedActionsSchema = zod.record(zod.string(), zod.object({ - actions: zod.array(actionWithCodeSchema), -})); -export type CachedActions = z.infer; diff --git a/packages/playwright-core/src/server/agent/codegen.ts b/packages/playwright-core/src/server/agent/codegen.ts deleted file mode 100644 index 03aa794d3d80a..0000000000000 --- a/packages/playwright-core/src/server/agent/codegen.ts +++ /dev/null @@ -1,103 +0,0 @@ -/** - * Copyright (c) Microsoft Corporation. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { asLocator } from '../../utils/isomorphic/locatorGenerators'; -import { escapeTemplateString, escapeWithQuotes, formatObjectOrVoid, parseRegex } from '../../utils/isomorphic/stringUtils'; - -import type * as actions from './actions'; -import type { Language } from '../../utils/isomorphic/locatorGenerators'; - -export async function generateCode(sdkLanguage: Language, action: actions.Action) { - switch (action.method) { - case 'navigate': { - return `await page.goto(${escapeWithQuotes(action.url)});`; - } - case 'click': { - const locator = asLocator(sdkLanguage, action.selector); - return `await page.${locator}.click(${formatObjectOrVoid({ - button: action.button, - clickCount: action.clickCount, - modifiers: action.modifiers, - })});`; - } - case 'drag': { - const sourceLocator = asLocator(sdkLanguage, action.sourceSelector); - const targetLocator = asLocator(sdkLanguage, action.targetSelector); - return `await page.${sourceLocator}.dragAndDrop(${targetLocator});`; - } - case 'hover': { - const locator = asLocator(sdkLanguage, action.selector); - return `await page.${locator}.hover(${formatObjectOrVoid({ - modifiers: action.modifiers, - })});`; - } - case 'pressKey': { - return `await page.keyboard.press(${escapeWithQuotes(action.key, '\'')});`; - } - case 'selectOption': { - const locator = asLocator(sdkLanguage, action.selector); - return `await page.${locator}.selectOption(${action.labels.length === 1 ? escapeWithQuotes(action.labels[0]) : '[' + action.labels.map(label => escapeWithQuotes(label)).join(', ') + ']'});`; - } - case 'pressSequentially': { - const locator = asLocator(sdkLanguage, action.selector); - const code = [`await page.${locator}.pressSequentially(${escapeWithQuotes(action.text)});`]; - if (action.submit) - code.push(`await page.keyboard.press('Enter');`); - return code.join('\n'); - } - case 'fill': { - const locator = asLocator(sdkLanguage, action.selector); - const code = [`await page.${locator}.fill(${escapeWithQuotes(action.text)});`]; - if (action.submit) - code.push(`await page.keyboard.press('Enter');`); - return code.join('\n'); - } - case 'setChecked': { - const locator = asLocator(sdkLanguage, action.selector); - if (action.checked) - return `await page.${locator}.check();`; - else - return `await page.${locator}.uncheck();`; - } - case 'expectVisible': { - const locator = asLocator(sdkLanguage, action.selector); - const notInfix = action.isNot ? 'not.' : ''; - return `await expect(page.${locator}).${notInfix}toBeVisible();`; - } - case 'expectValue': { - const notInfix = action.isNot ? 'not.' : ''; - const locator = asLocator(sdkLanguage, action.selector); - if (action.type === 'checkbox' || action.type === 'radio') - return `await expect(page.${locator}).${notInfix}toBeChecked({ checked: ${action.value === 'true'} });`; - return `await expect(page.${locator}).${notInfix}toHaveValue(${escapeWithQuotes(action.value)});`; - } - case 'expectAria': { - const notInfix = action.isNot ? 'not.' : ''; - return `await expect(page.locator('body')).${notInfix}toMatchAria(\`\n${escapeTemplateString(action.template)}\n\`);`; - } - case 'expectURL': { - const arg = action.regex ? parseRegex(action.regex).toString() : escapeWithQuotes(action.value!); - const notInfix = action.isNot ? 'not.' : ''; - return `await expect(page).${notInfix}toHaveURL(${arg});`; - } - case 'expectTitle': { - const notInfix = action.isNot ? 'not.' : ''; - return `await expect(page).${notInfix}toHaveTitle(${escapeWithQuotes(action.value)});`; - } - } - // @ts-expect-error - throw new Error('Unknown action ' + action.method); -} diff --git a/packages/playwright-core/src/server/agent/context.ts b/packages/playwright-core/src/server/agent/context.ts deleted file mode 100644 index 5861a00ec6d1e..0000000000000 --- a/packages/playwright-core/src/server/agent/context.ts +++ /dev/null @@ -1,187 +0,0 @@ -/** - * Copyright (c) Microsoft Corporation. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { BrowserContext } from '../browserContext'; -import { runAction } from './actionRunner'; -import { generateCode } from './codegen'; -import { stripAnsiEscapes } from '../../utils/isomorphic/stringUtils'; - -import type { Request } from '../network'; -import type * as loopTypes from '@lowire/loop'; -import type * as actions from './actions'; -import type { Page } from '../page'; -import type { Progress } from '../progress'; -import type { Language } from '../../utils/isomorphic/locatorGenerators.ts'; -import type * as channels from '@protocol/channels'; - - -type HistoryItem = { - type: 'expect' | 'perform'; - description: string; -}; -export class Context { - readonly page: Page; - readonly sdkLanguage: Language; - readonly agentParams: channels.PageAgentParams; - readonly events: loopTypes.LoopEvents; - private _actions: actions.ActionWithCode[] = []; - private _history: HistoryItem[] = []; - private _budget: { tokens: number | undefined; }; - - constructor(page: Page, agentParams: channels.PageAgentParams, events: loopTypes.LoopEvents) { - this.page = page; - this.agentParams = agentParams; - this.sdkLanguage = page.browserContext._browser.sdkLanguage(); - this.events = events; - this._budget = { tokens: agentParams.maxTokens }; - } - - async runActionAndWait(progress: Progress, action: actions.Action) { - return await this.runActionsAndWait(progress, [action]); - } - - async runActionsAndWait(progress: Progress, action: actions.Action[], options?: { noWait?: boolean }) { - const error = await this.waitForCompletion(progress, async () => { - for (const a of action) { - await runAction(progress, 'generate', this.page, a, this.agentParams?.secrets ?? []); - const code = await generateCode(this.sdkLanguage, a); - this._actions.push({ ...a, code }); - } - return undefined; - }, options).catch((error: Error) => error); - return await this.snapshotResult(progress, error); - } - - async runActionNoWait(progress: Progress, action: actions.Action) { - return await this.runActionsAndWait(progress, [action], { noWait: true }); - } - - actions() { - return this._actions.slice(); - } - - history(): HistoryItem[] { - return this._history; - } - - pushHistory(item: HistoryItem) { - this._history.push(item); - this._actions = []; - } - - consumeTokens(tokens: number) { - if (this._budget.tokens === undefined) - return; - this._budget.tokens = Math.max(0, this._budget.tokens - tokens); - } - - maxTokensRemaining(): number | undefined { - return this._budget.tokens; - } - - async waitForCompletion(progress: Progress, callback: () => Promise, options?: { noWait?: boolean }): Promise { - if (options?.noWait) - return await callback(); - - const requests: Request[] = []; - const requestListener = (request: Request) => requests.push(request); - const disposeListeners = () => { - this.page.browserContext.off(BrowserContext.Events.Request, requestListener); - }; - this.page.browserContext.on(BrowserContext.Events.Request, requestListener); - - let result: R; - try { - result = await callback(); - await progress.wait(500); - } finally { - disposeListeners(); - } - - const requestedNavigation = requests.some(request => request.isNavigationRequest()); - if (requestedNavigation) { - await this.page.mainFrame().waitForLoadState(progress, 'load'); - return result; - } - - const promises: Promise[] = []; - for (const request of requests) { - if (['document', 'stylesheet', 'script', 'xhr', 'fetch'].includes(request.resourceType())) - promises.push(request.response().then(r => r?.finished())); - else - promises.push(request.response()); - } - - if (promises.length) - await progress.race([...promises, progress.wait(5000)]); - else - await progress.wait(500); - - return result; - } - - async takeSnapshot(progress: Progress) { - const { full } = await this.page.snapshotForAI(progress, { doNotRenderActive: this.agentParams.doNotRenderActive }); - return redactSecrets(full, this.agentParams?.secrets); - } - - async snapshotResult(progress: Progress, error?: Error): Promise { - const snapshot = await this.takeSnapshot(progress); - - const text: string[] = []; - if (error) - text.push(`# Error\n${stripAnsiEscapes(error.message)}`); - else - text.push(`# Success`); - - text.push(`# Page snapshot\n${snapshot}`); - - return { - isError: !!error, - content: [{ type: 'text', text: text.join('\n\n') }], - }; - } - - async refSelectors(progress: Progress, params: { element: string, ref: string }[]): Promise { - return Promise.all(params.map(async param => { - try { - const { resolvedSelector } = await this.page.mainFrame().resolveSelector(progress, `aria-ref=${param.ref}`); - return resolvedSelector; - } catch (e) { - throw new Error(`Ref ${param.ref} not found in the current page snapshot. Try capturing new snapshot.`); - } - })); - } -} - -export function redactSecrets(text: string, secrets: channels.NameValue[] | undefined): string { - if (!secrets) - return text; - for (const { name, value } of secrets) - text = text.replaceAll(value, `${name}`); - return text; -} - -export function applySecrets(text: string, secrets: channels.NameValue[] | undefined): string { - if (!secrets) - return text; - const secret = secrets.find(s => s.name === text); - if (secret) - return secret.value; - for (const { name, value } of secrets) - text = text.replaceAll(`${name}`, value); - return text; -} diff --git a/packages/playwright-core/src/server/agent/expectTools.ts b/packages/playwright-core/src/server/agent/expectTools.ts deleted file mode 100644 index fe06f851184c6..0000000000000 --- a/packages/playwright-core/src/server/agent/expectTools.ts +++ /dev/null @@ -1,164 +0,0 @@ -/** - * Copyright (c) Microsoft Corporation. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { z } from '../../mcpBundle'; -import { getByRoleSelector, getByTextSelector } from '../../utils/isomorphic/locatorUtils'; -import { yamlEscapeValueIfNeeded } from '../../utils/isomorphic/yaml'; -import { defineTool } from './tool'; - -import type { ToolDefinition } from './tool'; - -const expectVisible = defineTool({ - schema: { - name: 'browser_expect_visible', - title: 'Expect element visible', - description: 'Expect element is visible on the page', - inputSchema: z.object({ - role: z.string().describe('ROLE of the element. Can be found in the snapshot like this: \`- {ROLE} "Accessible Name":\`'), - accessibleName: z.string().describe('ACCESSIBLE_NAME of the element. Can be found in the snapshot like this: \`- role "{ACCESSIBLE_NAME}"\`'), - isNot: z.boolean().optional().describe('Expect the opposite'), - }), - }, - - handle: async (progress, context, params) => { - return await context.runActionAndWait(progress, { - method: 'expectVisible', - selector: getByRoleSelector(params.role, { name: params.accessibleName }), - isNot: params.isNot, - }); - }, -}); - -const expectVisibleText = defineTool({ - schema: { - name: 'browser_expect_visible_text', - title: 'Expect text visible', - description: `Expect text is visible on the page. Prefer ${expectVisible.schema.name} if possible.`, - inputSchema: z.object({ - text: z.string().describe('TEXT to expect. Can be found in the snapshot like this: \`- role "Accessible Name": {TEXT}\` or like this: \`- text: {TEXT}\`'), - isNot: z.boolean().optional().describe('Expect the opposite'), - }), - }, - - handle: async (progress, context, params) => { - return await context.runActionAndWait(progress, { - method: 'expectVisible', - selector: getByTextSelector(params.text), - isNot: params.isNot, - }); - }, -}); - -const expectValue = defineTool({ - schema: { - name: 'browser_expect_value', - title: 'Expect value', - description: 'Expect element value', - inputSchema: z.object({ - type: z.enum(['textbox', 'checkbox', 'radio', 'combobox', 'slider']).describe('Type of the element'), - element: z.string().describe('Human-readable element description'), - ref: z.string().describe('Exact target element reference from the page snapshot'), - value: z.string().describe('Value to expect. For checkbox, use "true" or "false".'), - isNot: z.boolean().optional().describe('Expect the opposite'), - }), - }, - - handle: async (progress, context, params) => { - const [selector] = await context.refSelectors(progress, [{ ref: params.ref, element: params.element }]); - return await context.runActionAndWait(progress, { - method: 'expectValue', - selector, - type: params.type, - value: params.value, - isNot: params.isNot, - }); - }, -}); - -const expectList = defineTool({ - schema: { - name: 'browser_expect_list_visible', - title: 'Expect list visible', - description: 'Expect list is visible on the page, ensures items are present in the element in the exact order', - inputSchema: z.object({ - listRole: z.string().describe('Aria role of the list element as in the snapshot'), - listAccessibleName: z.string().optional().describe('Accessible name of the list element as in the snapshot'), - itemRole: z.string().describe('Aria role of the list items as in the snapshot, should all be the same'), - items: z.array(z.string().describe('Text to look for in the list item, can be either from accessible name of self / nested text content')), - isNot: z.boolean().optional().describe('Expect the opposite'), - }), - }, - - handle: async (progress, context, params) => { - const template = `- ${params.listRole}: -${params.items.map(item => ` - ${params.itemRole}: ${yamlEscapeValueIfNeeded(item)}`).join('\n')}`; - return await context.runActionAndWait(progress, { - method: 'expectAria', - template, - }); - }, -}); - -const expectURL = defineTool({ - schema: { - name: 'browser_expect_url', - title: 'Expect URL', - description: 'Expect the page URL to match the expected value. Either provide a url string or a regex pattern.', - inputSchema: z.object({ - url: z.string().optional().describe('Expected URL string. Relative URLs are resolved against the baseURL.'), - regex: z.string().optional().describe('Regular expression pattern to match the URL against, e.g. /foo.*/i.'), - isNot: z.boolean().optional().describe('Expect the opposite'), - }), - }, - - handle: async (progress, context, params) => { - return await context.runActionAndWait(progress, { - method: 'expectURL', - value: params.url, - regex: params.regex, - isNot: params.isNot, - }); - }, -}); - -const expectTitle = defineTool({ - schema: { - name: 'browser_expect_title', - title: 'Expect title', - description: 'Expect the page title to match the expected value.', - inputSchema: z.object({ - title: z.string().describe('Expected page title.'), - isNot: z.boolean().optional().describe('Expect the opposite'), - }), - }, - - handle: async (progress, context, params) => { - return await context.runActionAndWait(progress, { - method: 'expectTitle', - value: params.title, - isNot: params.isNot, - }); - }, -}); - -export default [ - expectVisible, - expectVisibleText, - expectValue, - expectList, - expectURL, - expectTitle, -] as ToolDefinition[]; diff --git a/packages/playwright-core/src/server/agent/pageAgent.ts b/packages/playwright-core/src/server/agent/pageAgent.ts deleted file mode 100644 index 5c1da041c8683..0000000000000 --- a/packages/playwright-core/src/server/agent/pageAgent.ts +++ /dev/null @@ -1,223 +0,0 @@ -/** - * Copyright (c) Microsoft Corporation. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import fs from 'fs'; -import path from 'path'; - -import { toolsForLoop } from './tool'; -import { debug } from '../../utilsBundle'; -import { Loop, z as zod } from '../../mcpBundle'; -import { runAction } from './actionRunner'; -import { Context } from './context'; -import performTools from './performTools'; -import expectTools from './expectTools'; - -import * as actions from './actions'; -import type { ToolDefinition } from './tool'; -import type * as loopTypes from '@lowire/loop'; -import type { Progress } from '../progress'; - -export type CallParams = { - cacheKey?: string; - maxTokens?: number; - maxActions?: number; - maxActionRetries?: number; -}; - -export async function pageAgentPerform(progress: Progress, context: Context, userTask: string, callParams: CallParams) { - const cacheKey = (callParams.cacheKey ?? userTask).trim(); - if (await cachedPerform(progress, context, cacheKey)) - return; - - const task = ` -### Instructions -- Perform the following task on the page. -- Your reply should be a tool call that performs action the page. -- If you see text surrounded by , it is a secret and you should preserve it as such. It will be replaced with the actual value before the tool call. - -### Task -${userTask} -`; - progress.disableTimeout(); - await runLoop(progress, context, performTools, task, undefined, callParams); - await updateCache(context, cacheKey); -} - -export async function pageAgentExpect(progress: Progress, context: Context, expectation: string, callParams: CallParams) { - const cacheKey = (callParams.cacheKey ?? expectation).trim(); - if (await cachedPerform(progress, context, cacheKey)) - return; - - const task = ` -### Instructions -- Call one of the "browser_expect_*" tools to verify / assert the condition. -- If you see text surrounded by , it is a secret and you should preserve it as such. It will be replaced with the actual value before the tool call. - -### Expectation -${expectation} -`; - progress.disableTimeout(); - await runLoop(progress, context, expectTools, task, undefined, callParams); - await updateCache(context, cacheKey); -} - -export async function pageAgentExtract(progress: Progress, context: Context, query: string, schema: loopTypes.Schema, callParams: CallParams): Promise { - - const task = ` -### Instructions -Extract the following information from the page. Do not perform any actions, just extract the information. -If you see text surrounded by , it is a secret and you should preserve it as such. It will be replaced with the actual value before the tool call. - -### Query -${query}`; - const { result } = await runLoop(progress, context, [], task, schema, callParams); - return result; -} - -async function runLoop(progress: Progress, context: Context, toolDefinitions: ToolDefinition[], userTask: string, resultSchema: loopTypes.Schema | undefined, params: CallParams): Promise<{ - result: any -}> { - if (!context.agentParams.api || !context.agentParams.model) - throw new Error(`This action requires the API and API key to be set on the page agent. Did you mean to --run-agents=missing?`); - if (!context.agentParams.apiKey) - throw new Error(`This action requires API key to be set on the page agent.`); - if (context.agentParams.apiEndpoint && !URL.canParse(context.agentParams.apiEndpoint)) - throw new Error(`Agent API endpoint "${context.agentParams.apiEndpoint}" is not a valid URL.`); - - const snapshot = await context.takeSnapshot(progress); - const { tools, callTool, reportedResult, refusedToPerformReason } = toolsForLoop(progress, context, toolDefinitions, { resultSchema, refuseToPerform: 'allow' }); - - const apiCacheTextBefore = context.agentParams.apiCacheFile ? - await fs.promises.readFile(context.agentParams.apiCacheFile, 'utf-8').catch(() => '{}') : '{}'; - const apiCacheBefore = JSON.parse(apiCacheTextBefore || '{}'); - - const loop = new Loop({ - api: context.agentParams.api as any, - apiEndpoint: context.agentParams.apiEndpoint, - apiKey: context.agentParams.apiKey, - apiTimeout: context.agentParams.apiTimeout ?? 0, - model: context.agentParams.model, - maxTokens: params.maxTokens ?? context.maxTokensRemaining(), - maxToolCalls: params.maxActions ?? context.agentParams.maxActions ?? 10, - maxToolCallRetries: params.maxActionRetries ?? context.agentParams.maxActionRetries ?? 3, - summarize: true, - debug, - callTool, - tools, - cache: apiCacheBefore, - ...context.events, - }); - - const task: string[] = []; - if (context.agentParams.systemPrompt) { - task.push('### System'); - task.push(context.agentParams.systemPrompt); - task.push(''); - } - - task.push('### Task'); - task.push(userTask); - - if (context.history().length) { - task.push('### Context history'); - task.push(context.history().map(h => `- ${h.type}: ${h.description}`).join('\n')); - task.push(''); - } - task.push('### Page snapshot'); - task.push(snapshot); - task.push(''); - - const { error, usage } = await loop.run(task.join('\n'), { signal: progress.signal }); - context.consumeTokens(usage.input + usage.output); - if (context.agentParams.apiCacheFile) { - const apiCacheAfter = { ...apiCacheBefore, ...loop.cache() }; - const sortedCache = Object.fromEntries(Object.entries(apiCacheAfter).sort(([a], [b]) => a.localeCompare(b))); - const apiCacheTextAfter = JSON.stringify(sortedCache, undefined, 2); - if (apiCacheTextAfter !== apiCacheTextBefore) { - await fs.promises.mkdir(path.dirname(context.agentParams.apiCacheFile), { recursive: true }); - await fs.promises.writeFile(context.agentParams.apiCacheFile, apiCacheTextAfter); - } - } - - if (refusedToPerformReason()) - throw new Error(`Agent refused to perform action: ${refusedToPerformReason()}`); - - if (error) - throw new Error(`Agentic loop failed: ${error}`); - - return { result: reportedResult ? reportedResult() : undefined }; -} - -async function cachedPerform(progress: Progress, context: Context, cacheKey: string): Promise { - if (!context.agentParams?.cacheFile) - return; - - const cache = await cachedActions(context.agentParams?.cacheFile); - const entry = cache.actions[cacheKey]; - if (!entry) - return; - - for (const action of entry.actions) - await runAction(progress, 'run', context.page, action, context.agentParams.secrets ?? []); - return entry.actions; -} - -async function updateCache(context: Context, cacheKey: string) { - const cacheFile = context.agentParams?.cacheFile; - const cacheOutFile = context.agentParams?.cacheOutFile; - const cacheFileKey = cacheFile ?? cacheOutFile; - - const cache = cacheFileKey ? await cachedActions(cacheFileKey) : { actions: {}, newActions: {} }; - const newEntry = { actions: context.actions() }; - cache.actions[cacheKey] = newEntry; - cache.newActions[cacheKey] = newEntry; - - if (cacheOutFile) { - const entries = Object.entries(cache.newActions); - entries.sort((e1, e2) => e1[0].localeCompare(e2[0])); - await fs.promises.writeFile(cacheOutFile, JSON.stringify(Object.fromEntries(entries), undefined, 2)); - } else if (cacheFile) { - const entries = Object.entries(cache.actions); - entries.sort((e1, e2) => e1[0].localeCompare(e2[0])); - await fs.promises.writeFile(cacheFile, JSON.stringify(Object.fromEntries(entries), undefined, 2)); - } -} - -type Cache = { - actions: actions.CachedActions; - newActions: actions.CachedActions; -}; - -const allCaches = new Map(); - -async function cachedActions(cacheFile: string): Promise { - let cache = allCaches.get(cacheFile); - if (!cache) { - const content = await fs.promises.readFile(cacheFile, 'utf-8').catch(() => ''); - let json: any; - try { - json = JSON.parse(content.trim() || '{}'); - } catch (error) { - throw new Error(`Failed to parse cache file ${cacheFile}:\n${error.message}`); - } - const parsed = actions.cachedActionsSchema.safeParse(json); - if (parsed.error) - throw new Error(`Failed to parse cache file ${cacheFile}:\n${zod.prettifyError(parsed.error)}`); - cache = { actions: parsed.data, newActions: {} }; - allCaches.set(cacheFile, cache); - } - return cache; -} diff --git a/packages/playwright-core/src/server/agent/performTools.ts b/packages/playwright-core/src/server/agent/performTools.ts deleted file mode 100644 index 125af978d8a3a..0000000000000 --- a/packages/playwright-core/src/server/agent/performTools.ts +++ /dev/null @@ -1,287 +0,0 @@ -/** - * Copyright (c) Microsoft Corporation. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { z } from '../../mcpBundle'; -import { defineTool } from './tool'; - -import type * as actions from './actions'; -import type { ToolDefinition } from './tool'; - -const navigateSchema = z.object({ - url: z.string().describe('URL to navigate to'), -}); - -const navigate = defineTool({ - schema: { - name: 'browser_navigate', - title: 'Navigate to URL', - description: 'Navigate to a URL', - inputSchema: navigateSchema, - }, - - handle: async (progress, context, params) => { - return await context.runActionNoWait(progress, { - method: 'navigate', - url: params.url, - }); - }, -}); - -const snapshot = defineTool({ - schema: { - name: 'browser_snapshot', - title: 'Page snapshot', - description: 'Capture accessibility snapshot of the current page, this is better than screenshot', - inputSchema: z.object({}), - }, - - handle: async (progress, context, params) => { - return await context.snapshotResult(progress); - }, -}); - -const elementSchema = z.object({ - element: z.string().describe('Human-readable element description used to obtain permission to interact with the element'), - ref: z.string().describe('Exact target element reference from the page snapshot'), -}); - -const clickSchema = elementSchema.extend({ - doubleClick: z.boolean().optional().describe('Whether to perform a double click instead of a single click'), - button: z.enum(['left', 'right', 'middle']).optional().describe('Button to click, defaults to left'), - modifiers: z.array(z.enum(['Alt', 'Control', 'ControlOrMeta', 'Meta', 'Shift'])).optional().describe('Modifier keys to press'), -}); - -const click = defineTool({ - schema: { - name: 'browser_click', - title: 'Click', - description: 'Perform click on a web page', - inputSchema: clickSchema, - }, - - handle: async (progress, context, params) => { - const [selector] = await context.refSelectors(progress, [params]); - return await context.runActionAndWait(progress, { - method: 'click', - selector, - button: params.button, - modifiers: params.modifiers, - clickCount: params.doubleClick ? 2 : undefined, - }); - }, -}); - -const drag = defineTool({ - schema: { - name: 'browser_drag', - title: 'Drag mouse', - description: 'Perform drag and drop between two elements', - inputSchema: z.object({ - startElement: z.string().describe('Human-readable source element description used to obtain the permission to interact with the element'), - startRef: z.string().describe('Exact source element reference from the page snapshot'), - endElement: z.string().describe('Human-readable target element description used to obtain the permission to interact with the element'), - endRef: z.string().describe('Exact target element reference from the page snapshot'), - }), - }, - - handle: async (progress, context, params) => { - const [sourceSelector, targetSelector] = await context.refSelectors(progress, [ - { ref: params.startRef, element: params.startElement }, - { ref: params.endRef, element: params.endElement }, - ]); - - return await context.runActionAndWait(progress, { - method: 'drag', - sourceSelector, - targetSelector - }); - }, -}); - -const hoverSchema = elementSchema.extend({ - modifiers: z.array(z.enum(['Alt', 'Control', 'ControlOrMeta', 'Meta', 'Shift'])).optional().describe('Modifier keys to press'), -}); - -const hover = defineTool({ - schema: { - name: 'browser_hover', - title: 'Hover mouse', - description: 'Hover over element on page', - inputSchema: hoverSchema, - }, - - handle: async (progress, context, params) => { - const [selector] = await context.refSelectors(progress, [params]); - return await context.runActionAndWait(progress, { - method: 'hover', - selector, - modifiers: params.modifiers, - }); - }, -}); - -const selectOptionSchema = elementSchema.extend({ - values: z.array(z.string()).describe('Array of values to select in the dropdown. This can be a single value or multiple values.'), -}); - -const selectOption = defineTool({ - schema: { - name: 'browser_select_option', - title: 'Select option', - description: 'Select an option in a dropdown', - inputSchema: selectOptionSchema, - }, - - handle: async (progress, context, params) => { - const [selector] = await context.refSelectors(progress, [params]); - return await context.runActionAndWait(progress, { - method: 'selectOption', - selector, - labels: params.values - }); - }, -}); - -const pressKey = defineTool({ - schema: { - name: 'browser_press_key', - title: 'Press a key', - description: 'Press a key on the keyboard', - inputSchema: z.object({ - key: z.string().describe('Name of the key to press or a character to generate, such as `ArrowLeft` or `a`'), - modifiers: z.array(z.enum(['Alt', 'Control', 'ControlOrMeta', 'Meta', 'Shift'])).optional().describe('Modifier keys to press'), - }), - }, - - handle: async (progress, context, params) => { - return await context.runActionAndWait(progress, { - method: 'pressKey', - key: params.modifiers ? [...params.modifiers, params.key].join('+') : params.key, - }); - }, -}); - -const typeSchema = elementSchema.extend({ - text: z.string().describe('Text to type into the element'), - submit: z.boolean().optional().describe('Whether to submit entered text (press Enter after)'), - slowly: z.boolean().optional().describe('Whether to type one character at a time. Useful for triggering key handlers in the page. By default entire text is filled in at once.'), -}); - -const type = defineTool({ - schema: { - name: 'browser_type', - title: 'Type text', - description: 'Type text into editable element', - inputSchema: typeSchema, - }, - - handle: async (progress, context, params) => { - const [selector] = await context.refSelectors(progress, [params]); - if (params.slowly) { - return await context.runActionAndWait(progress, { - method: 'pressSequentially', - selector, - text: params.text, - submit: params.submit, - }); - } else { - return await context.runActionAndWait(progress, { - method: 'fill', - selector, - text: params.text, - submit: params.submit, - }); - } - }, -}); - -const fillForm = defineTool({ - schema: { - name: 'browser_fill_form', - title: 'Fill form', - description: 'Fill multiple form fields. Always use this tool when you can fill more than one field at a time.', - inputSchema: z.object({ - fields: z.array(z.object({ - name: z.string().describe('Human-readable field name'), - type: z.enum(['textbox', 'checkbox', 'radio', 'combobox', 'slider']).describe('Type of the field'), - ref: z.string().describe('Exact target field reference from the page snapshot'), - value: z.string().describe('Value to fill in the field. If the field is a checkbox, the value should be `true` or `false`. If the field is a combobox, the value should be the text of the option.'), - })).describe('Fields to fill in'), - }), - }, - - handle: async (progress, context, params) => { - const actions: actions.Action[] = []; - for (const field of params.fields) { - const [selector] = await context.refSelectors(progress, [{ ref: field.ref, element: field.name }]); - if (field.type === 'textbox' || field.type === 'slider') { - actions.push({ - method: 'fill', - selector, - text: field.value, - }); - } else if (field.type === 'checkbox' || field.type === 'radio') { - actions.push({ - method: 'setChecked', - selector, - checked: field.value === 'true', - }); - } else if (field.type === 'combobox') { - actions.push({ - method: 'selectOption', - selector, - labels: [field.value], - }); - } - } - return await context.runActionsAndWait(progress, actions); - }, -}); - -const setCheckedSchema = elementSchema.extend({ - checked: z.boolean().describe('Whether to check the checkbox'), -}); - -const setChecked = defineTool({ - schema: { - name: 'browser_set_checked', - title: 'Set checked', - description: 'Set the checked state of a checkbox', - inputSchema: setCheckedSchema, - }, - - handle: async (progress, context, params) => { - const [selector] = await context.refSelectors(progress, [params]); - return await context.runActionAndWait(progress, { - method: 'setChecked', - selector, - checked: params.checked, - }); - }, -}); - -export default [ - navigate, - snapshot, - click, - drag, - hover, - selectOption, - pressKey, - type, - fillForm, - setChecked, -] as ToolDefinition[]; diff --git a/packages/playwright-core/src/server/agent/tool.ts b/packages/playwright-core/src/server/agent/tool.ts deleted file mode 100644 index 34e36d8d6924c..0000000000000 --- a/packages/playwright-core/src/server/agent/tool.ts +++ /dev/null @@ -1,126 +0,0 @@ -/** - * Copyright (c) Microsoft Corporation. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { z } from '../../mcpBundle'; -import { stripAnsiEscapes } from '../../utils/isomorphic/stringUtils'; -import type zod from 'zod'; -import type * as loopTypes from '@lowire/loop'; -import type { Context } from './context'; -import type { Progress } from '../progress'; - -export type ToolSchema = Omit & { - title: string; - inputSchema: Input; -}; - -export type ToolDefinition = { - schema: ToolSchema; - handle: (progress: Progress, context: Context, params: zod.output) => Promise; -}; - -export function defineTool(tool: ToolDefinition): ToolDefinition { - return tool; -} - -type ToolsForLoop = { - tools: loopTypes.Tool[]; - callTool: loopTypes.ToolCallback; - reportedResult?: () => any; - refusedToPerformReason: () => string | undefined; -}; - -export function toolsForLoop(progress: Progress, context: Context, toolDefinitions: ToolDefinition[], options: { resultSchema?: loopTypes.Schema, refuseToPerform?: 'allow' | 'deny' } = {}): ToolsForLoop { - const tools = toolDefinitions.map(tool => { - const result: loopTypes.Tool = { - name: tool.schema.name, - description: tool.schema.description, - inputSchema: z.toJSONSchema(tool.schema.inputSchema) as loopTypes.Schema, - }; - return result; - }); - - if (options.resultSchema) { - tools.push({ - name: 'report_result', - description: 'Report the result of the task.', - inputSchema: options.resultSchema, - }); - } - - if (options.refuseToPerform === 'allow') { - tools.push({ - name: 'refuse_to_perform', - description: 'Refuse to perform action.', - inputSchema: { - type: 'object', - properties: { - reason: { - type: 'string', - description: `Call this when you believe that you can't perform the action because something is wrong with the page. The reason will be reported to the user.`, - }, - }, - required: ['reason'], - }, - }); - } - - let reportedResult: any; - let refusedToPerformReason: string | undefined; - - const callTool: loopTypes.ToolCallback = async params => { - if (params.name === 'report_result') { - reportedResult = params.arguments; - return { - content: [{ type: 'text', text: 'Done' }], - isError: false, - }; - } - - if (params.name === 'refuse_to_perform') { - refusedToPerformReason = params.arguments.reason; - return { - content: [{ type: 'text', text: 'Done' }], - isError: false, - }; - } - - const tool = toolDefinitions.find(t => t.schema.name === params.name); - if (!tool) { - return { - content: [{ type: 'text', - text: `Tool ${params.name} not found. Available tools: ${toolDefinitions.map(t => t.schema.name)}` - }], - isError: true, - }; - } - - try { - return await tool.handle(progress, context, params.arguments); - } catch (error) { - return { - content: [{ type: 'text', text: stripAnsiEscapes(error.message) }], - isError: true, - }; - } - }; - - return { - tools, - callTool, - reportedResult: options.resultSchema ? () => reportedResult : undefined, - refusedToPerformReason: () => refusedToPerformReason, - }; -} diff --git a/packages/playwright-core/src/server/dispatchers/pageAgentDispatcher.ts b/packages/playwright-core/src/server/dispatchers/pageAgentDispatcher.ts deleted file mode 100644 index 88846382502bb..0000000000000 --- a/packages/playwright-core/src/server/dispatchers/pageAgentDispatcher.ts +++ /dev/null @@ -1,122 +0,0 @@ -/** - * Copyright (c) Microsoft Corporation. - * - * Licensed under the Apache License, Version 2.0 (the 'License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { Dispatcher } from './dispatcher'; -import { pageAgentExpect, pageAgentExtract, pageAgentPerform } from '../agent/pageAgent'; -import { SdkObject } from '../instrumentation'; -import { Context } from '../agent/context'; - -import type { PageDispatcher } from './pageDispatcher'; -import type { DispatcherScope } from './dispatcher'; -import type * as channels from '@protocol/channels'; -import type { Progress } from '@protocol/progress'; -import type { Page } from '../page'; -import type * as loopTypes from '@lowire/loop'; - -export class PageAgentDispatcher extends Dispatcher implements channels.PageAgentChannel { - _type_PageAgent = true; - _type_EventTarget = true; - private _page: Page; - private _usage: Usage = { turns: 0, inputTokens: 0, outputTokens: 0 }; - private _context: Context; - - constructor(scope: PageDispatcher, options: channels.PageAgentParams) { - super(scope, new SdkObject(scope._object, 'pageAgent'), 'PageAgent', { page: scope }); - this._page = scope._object; - this._context = new Context(this._page, options, this._eventSupport()); - } - - async perform(params: channels.PageAgentPerformParams, progress: Progress): Promise { - try { - await pageAgentPerform(progress, this._context, params.task, params); - } finally { - this._context.pushHistory({ type: 'perform', description: params.task }); - } - return { usage: this._usage }; - } - - async expect(params: channels.PageAgentExpectParams, progress: Progress): Promise { - try { - await pageAgentExpect(progress, this._context, params.expectation, params); - } finally { - this._context.pushHistory({ type: 'expect', description: params.expectation }); - } - return { usage: this._usage }; - } - - async extract(params: channels.PageAgentExtractParams, progress: Progress): Promise { - const result = await pageAgentExtract(progress, this._context, params.query, params.schema, params); - return { result, usage: this._usage }; - } - - async usage(params: channels.PageAgentUsageParams, progress: Progress): Promise { - return { usage: this._usage }; - } - - async dispose(params: channels.PageAgentDisposeParams, progress: Progress): Promise { - progress.metadata.potentiallyClosesScope = true; - void this.stopPendingOperations(new Error('The agent is disposed')); - this._dispose(); - } - - private _eventSupport(): loopTypes.LoopEvents { - const self = this; - return { - onBeforeTurn(params: { conversation: loopTypes.Conversation }) { - if (self._disposed) - return; - const userMessage = params.conversation.messages.find(m => m.role === 'user'); - self._dispatchEvent('turn', { role: 'user', message: userMessage?.content ?? '' }); - }, - - onAfterTurn(params: { assistantMessage: loopTypes.AssistantMessage, totalUsage: loopTypes.Usage }) { - if (self._disposed) - return; - const usage = { inputTokens: params.totalUsage.input, outputTokens: params.totalUsage.output }; - const intent = params.assistantMessage.content.filter(c => c.type === 'text').map(c => c.text).join('\n'); - self._dispatchEvent('turn', { role: 'assistant', message: intent, usage }); - if (!params.assistantMessage.content.filter(c => c.type === 'tool_call').length) - self._dispatchEvent('turn', { role: 'assistant', message: `no tool calls`, usage }); - self._usage = { turns: self._usage.turns + 1, inputTokens: self._usage.inputTokens + usage.inputTokens, outputTokens: self._usage.outputTokens + usage.outputTokens }; - }, - - onBeforeToolCall(params: { toolCall: loopTypes.ToolCallContentPart }) { - if (self._disposed) - return; - self._dispatchEvent('turn', { role: 'assistant', message: `call tool "${params.toolCall.name}"` }); - }, - - onAfterToolCall(params: { toolCall: loopTypes.ToolCallContentPart, result: loopTypes.ToolResult }) { - if (self._disposed) - return; - const suffix = params.toolCall.result?.isError ? 'failed' : 'succeeded'; - self._dispatchEvent('turn', { role: 'user', message: `tool "${params.toolCall.name}" ${suffix}` }); - }, - - onToolCallError(params: { toolCall: loopTypes.ToolCallContentPart, error: Error }) { - if (self._disposed) - return; - self._dispatchEvent('turn', { role: 'user', message: `tool "${params.toolCall.name}" failed: ${params.error.message}` }); - } - }; - } -} - -type Usage = { - turns: number, - inputTokens: number, - outputTokens: number, -}; diff --git a/packages/playwright-core/src/server/dispatchers/pageDispatcher.ts b/packages/playwright-core/src/server/dispatchers/pageDispatcher.ts index 14bd1b0aa0412..84b5e44b6d4be 100644 --- a/packages/playwright-core/src/server/dispatchers/pageDispatcher.ts +++ b/packages/playwright-core/src/server/dispatchers/pageDispatcher.ts @@ -28,7 +28,6 @@ import { WebSocketRouteDispatcher } from './webSocketRouteDispatcher'; import { DisposableDispatcher } from './disposableDispatcher'; import { SdkObject } from '../instrumentation'; import { deserializeURLMatch, urlMatches } from '../../utils/isomorphic/urlMatch'; -import { PageAgentDispatcher } from './pageAgentDispatcher'; import { Recorder } from '../recorder'; import { disposeAll } from '../disposable'; @@ -418,10 +417,6 @@ export class PageDispatcher extends Dispatcher { - return { agent: new PageAgentDispatcher(this, params) }; - } - _onFrameAttached(frame: Frame) { this._dispatchEvent('frameAttached', { frame: FrameDispatcher.from(this.parentScope(), frame) }); } diff --git a/packages/playwright-core/src/utils/isomorphic/protocolMetainfo.ts b/packages/playwright-core/src/utils/isomorphic/protocolMetainfo.ts index fb35b02047e26..3104b4c4c740a 100644 --- a/packages/playwright-core/src/utils/isomorphic/protocolMetainfo.ts +++ b/packages/playwright-core/src/utils/isomorphic/protocolMetainfo.ts @@ -69,7 +69,6 @@ export const methodMetainfo = new Map; - /** - * Initialize page agent with the llm provider and cache. - * @param options - */ - agent(options?: { - cache?: { - /** - * Cache file to use/generate code for performed actions into. Cache is not used if not specified (default). - */ - cacheFile?: string; - - /** - * When specified, generated entries are written into the `cacheOutFile` instead of updating the `cacheFile`. - */ - cacheOutFile?: string; - }; - - expect?: { - /** - * Default timeout for expect calls in milliseconds, defaults to 5000ms. - */ - timeout?: number; - }; - - /** - * Limits to use for the agentic loop. - */ - limits?: { - /** - * Maximum number of tokens to consume. The agentic loop will stop after input + output tokens exceed this value. - * Defaults to unlimited. - */ - maxTokens?: number; - - /** - * Maximum number of agentic actions to generate, defaults to 10. - */ - maxActions?: number; - - /** - * Maximum number retries per action, defaults to 3. - */ - maxActionRetries?: number; - }; - - provider?: { - /** - * API to use. - */ - api: "openai"|"openai-compatible"|"anthropic"|"google"; - - /** - * Endpoint to use if different from default. - */ - apiEndpoint?: string; - - /** - * API key for the LLM provider. - */ - apiKey: string; - - /** - * Amount of time to wait for the provider to respond to each request. - */ - apiTimeout?: number; - - /** - * Model identifier within the provider. Required in non-cache mode. - */ - model: string; - }; - - /** - * Secrets to hide from the LLM. - */ - secrets?: { [key: string]: string; }; - - /** - * System prompt for the agent's loop. - */ - systemPrompt?: string; - }): Promise; - /** * Brings page to front (activates tab). */ @@ -5356,243 +5273,6 @@ export interface Page { [Symbol.asyncDispose](): Promise; } -/** - * - */ -export interface PageAgent { - /** - * Extract information from the page using the agentic loop, return it in a given Zod format. - * - * **Usage** - * - * ```js - * await agent.extract('List of items in the cart', z.object({ - * title: z.string().describe('Item title to extract'), - * price: z.string().describe('Item price to extract'), - * }).array()); - * ``` - * - * @param query Task to perform using agentic loop. - * @param schema - * @param options - */ - extract(query: string, schema: Schema): Promise<{ result: InferZodSchema, usage: { turns: number, inputTokens: number, outputTokens: number } }>; - /** - * Emitted when the agent makes a turn. - */ - on(event: 'turn', listener: (data: { - role: string; - - message: string; - - usage?: { - inputTokens: number; - - outputTokens: number; - }; - }) => any): this; - - /** - * Adds an event listener that will be automatically removed after it is triggered once. See `addListener` for more information about this event. - */ - once(event: 'turn', listener: (data: { - role: string; - - message: string; - - usage?: { - inputTokens: number; - - outputTokens: number; - }; - }) => any): this; - - /** - * Emitted when the agent makes a turn. - */ - addListener(event: 'turn', listener: (data: { - role: string; - - message: string; - - usage?: { - inputTokens: number; - - outputTokens: number; - }; - }) => any): this; - - /** - * Removes an event listener added by `on` or `addListener`. - */ - removeListener(event: 'turn', listener: (data: { - role: string; - - message: string; - - usage?: { - inputTokens: number; - - outputTokens: number; - }; - }) => any): this; - - /** - * Removes an event listener added by `on` or `addListener`. - */ - off(event: 'turn', listener: (data: { - role: string; - - message: string; - - usage?: { - inputTokens: number; - - outputTokens: number; - }; - }) => any): this; - - /** - * Emitted when the agent makes a turn. - */ - prependListener(event: 'turn', listener: (data: { - role: string; - - message: string; - - usage?: { - inputTokens: number; - - outputTokens: number; - }; - }) => any): this; - - /** - * Dispose this agent. - */ - dispose(): Promise; - - /** - * Expect certain condition to be met. - * - * **Usage** - * - * ```js - * await agent.expect('"0 items" to be reported'); - * ``` - * - * @param expectation Expectation to assert. - * @param options - */ - expect(expectation: string, options?: { - /** - * All the agentic actions are converted to the Playwright calls and are cached. By default, they are cached globally - * with the `task` as a key. This option allows controlling the cache key explicitly. - */ - cacheKey?: string; - - /** - * Maximum number of retries when generating each action, defaults to context-wide value specified in `agent` - * property. - */ - maxActionRetries?: number; - - /** - * Maximum number of agentic actions to generate, defaults to context-wide value specified in `agent` property. - */ - maxActions?: number; - - /** - * Maximum number of tokens to consume. The agentic loop will stop after input + output tokens exceed this value. - * Defaults to context-wide value specified in `agent` property. - */ - maxTokens?: number; - - /** - * Expect timeout in milliseconds. Defaults to `5000`. The default value can be changed via `expect.timeout` option in - * the config, or by specifying the `expect` property of the - * [`expect`](https://playwright.dev/docs/api/class-page#page-agent-option-expect) option. Pass `0` to disable - * timeout. - */ - timeout?: number; - }): Promise; - - /** - * Perform action using agentic loop. - * - * **Usage** - * - * ```js - * await agent.perform('Click submit button'); - * ``` - * - * @param task Task to perform using agentic loop. - * @param options - */ - perform(task: string, options?: { - /** - * All the agentic actions are converted to the Playwright calls and are cached. By default, they are cached globally - * with the `task` as a key. This option allows controlling the cache key explicitly. - */ - cacheKey?: string; - - /** - * Maximum number of retries when generating each action, defaults to context-wide value specified in `agent` - * property. - */ - maxActionRetries?: number; - - /** - * Maximum number of agentic actions to generate, defaults to context-wide value specified in `agent` property. - */ - maxActions?: number; - - /** - * Maximum number of tokens to consume. The agentic loop will stop after input + output tokens exceed this value. - * Defaults to context-wide value specified in `agent` property. - */ - maxTokens?: number; - - /** - * Perform timeout in milliseconds. Defaults to `5000`. The default value can be changed via `actionTimeout` option in - * the config, or by using the - * [browserContext.setDefaultTimeout(timeout)](https://playwright.dev/docs/api/class-browsercontext#browser-context-set-default-timeout) - * or [page.setDefaultTimeout(timeout)](https://playwright.dev/docs/api/class-page#page-set-default-timeout) methods. - * Pass `0` to disable timeout. - */ - timeout?: number; - }): Promise<{ - usage: { - turns: number; - - inputTokens: number; - - outputTokens: number; - }; - }>; - - /** - * Returns the current token usage for this agent. - * - * **Usage** - * - * ```js - * const usage = await agent.usage(); - * console.log(`Tokens used: ${usage.inputTokens} in, ${usage.outputTokens} out`); - * ``` - * - */ - usage(): Promise<{ - turns: number; - - inputTokens: number; - - outputTokens: number; - }>; - - [Symbol.asyncDispose](): Promise; -} - /** * At every point of time, page exposes its current frame tree via the * [page.mainFrame()](https://playwright.dev/docs/api/class-page#page-main-frame) and diff --git a/packages/playwright/src/DEPS.list b/packages/playwright/src/DEPS.list index 39705a210a7db..63df33ec4f297 100644 --- a/packages/playwright/src/DEPS.list +++ b/packages/playwright/src/DEPS.list @@ -13,7 +13,6 @@ common/ ./mcp/sdk/ ./mcp/test/ ./transform/babelBundle.ts -./agents/ [internalsForTest.ts] ** diff --git a/packages/playwright/src/common/config.ts b/packages/playwright/src/common/config.ts index 27ea2061716a5..5447f2b2f5cb2 100644 --- a/packages/playwright/src/common/config.ts +++ b/packages/playwright/src/common/config.ts @@ -112,7 +112,6 @@ export class FullConfigInternal { quiet: takeFirst(configCLIOverrides.quiet, userConfig.quiet, false), reporter: takeFirst(configCLIOverrides.reporter, resolveReporters(userConfig.reporter, configDir), [[defaultReporter]]), reportSlowTests: takeFirst(userConfig.reportSlowTests, { max: 5, threshold: 300_000 /* 5 minutes */ }), - runAgents: takeFirst(configCLIOverrides.runAgents, userConfig.runAgents, 'none'), shard: takeFirst(configCLIOverrides.shard, userConfig.shard, null), tags: globalTags, updateSnapshots: takeFirst(configCLIOverrides.updateSnapshots, userConfig.updateSnapshots, 'missing'), diff --git a/packages/playwright/src/common/ipc.ts b/packages/playwright/src/common/ipc.ts index c6aa1b0a71f26..21e465b9d7779 100644 --- a/packages/playwright/src/common/ipc.ts +++ b/packages/playwright/src/common/ipc.ts @@ -44,7 +44,6 @@ export type ConfigCLIOverrides = { ignoreSnapshots?: boolean; updateSnapshots?: 'all' | 'changed' | 'missing' | 'none'; updateSourceMethod?: 'overwrite' | 'patch' | '3way'; - runAgents?: 'all' | 'missing' | 'none'; workers?: number | string; projects?: { name: string, use?: any }[], use?: any; @@ -97,15 +96,6 @@ export type TestPausedPayload = { export type ResumePayload = {}; -export type CloneStoragePayload = { - storageFile: string; -}; - -export type UpstreamStoragePayload = { - storageFile: string; - storageOutFile: string; -}; - export type CustomMessageRequestPayload = { testId: string; request: any; diff --git a/packages/playwright/src/index.ts b/packages/playwright/src/index.ts index 72582d06fb96c..add90f6df1ca4 100644 --- a/packages/playwright/src/index.ts +++ b/packages/playwright/src/index.ts @@ -153,8 +153,6 @@ const playwrightFixtures: Fixtures = ({ }, { option: true, box: true }], serviceWorkers: [({ contextOptions }, use) => use(contextOptions.serviceWorkers ?? 'allow'), { option: true, box: true }], contextOptions: [{}, { option: true, box: true }], - agentOptions: [({}, use) => use(undefined), { option: true, box: true }], - _combinedContextOptions: [async ({ acceptDownloads, bypassCSP, @@ -461,47 +459,6 @@ const playwrightFixtures: Fixtures = ({ await use(page); }, - agent: async ({ page, agentOptions }, use, testInfo) => { - const testInfoImpl = testInfo as TestInfoImpl; - const cachePathTemplate = agentOptions?.cachePathTemplate ?? '{testDir}/{testFilePath}-cache.json'; - const resolvedCacheFile = testInfoImpl._applyPathTemplate(cachePathTemplate, '', '.json'); - const cacheFile = testInfoImpl.config.runAgents === 'all' ? undefined : await testInfoImpl._cloneStorage(resolvedCacheFile); - const cacheOutFile = path.join(testInfoImpl.artifactsDir(), 'agent-cache-' + createGuid() + '.json'); - - const provider = agentOptions?.provider && testInfo.config.runAgents !== 'none' ? agentOptions.provider : undefined; - if (provider) - testInfo.setTimeout(0); - - const cache = { - cacheFile, - cacheOutFile, - }; - - const agent = await page.agent({ - provider, - cache, - limits: agentOptions?.limits, - secrets: agentOptions?.secrets, - systemPrompt: agentOptions?.systemPrompt, - expect: { - timeout: testInfoImpl._projectInternal.expect?.timeout, - }, - }); - - await use(agent); - - const usage = await agent.usage(); - if (usage.turns > 0) - await testInfoImpl.attach('agent-usage', { contentType: 'application/json', body: Buffer.from(JSON.stringify(usage, null, 2)) }); - - if (!resolvedCacheFile || !cacheOutFile) - return; - if (testInfo.status !== 'passed') - return; - - await testInfoImpl._upstreamStorage(resolvedCacheFile, cacheOutFile); - }, - request: async ({ playwright }, use) => { const request = await playwright.request.newContext(); await use(request); diff --git a/packages/playwright/src/isomorphic/teleReceiver.ts b/packages/playwright/src/isomorphic/teleReceiver.ts index 6eba6073b0484..a86cf25791c78 100644 --- a/packages/playwright/src/isomorphic/teleReceiver.ts +++ b/packages/playwright/src/isomorphic/teleReceiver.ts @@ -786,7 +786,6 @@ export const baseFullConfig: reporterTypes.FullConfig = { tags: [], updateSnapshots: 'missing', updateSourceMethod: 'patch', - runAgents: 'none', version: '', workers: 0, webServer: null, diff --git a/packages/playwright/src/isomorphic/testServerInterface.ts b/packages/playwright/src/isomorphic/testServerInterface.ts index 9fb411c467bbf..c0c91ca9b4d97 100644 --- a/packages/playwright/src/isomorphic/testServerInterface.ts +++ b/packages/playwright/src/isomorphic/testServerInterface.ts @@ -98,7 +98,6 @@ export interface TestServerInterface { workers?: number | string; updateSnapshots?: 'all' | 'changed' | 'missing' | 'none'; updateSourceMethod?: 'overwrite' | 'patch' | '3way'; - runAgents?: 'all' | 'missing' | 'none'; reporters?: string[], trace?: 'on' | 'off'; video?: 'on' | 'off'; diff --git a/packages/playwright/src/program.ts b/packages/playwright/src/program.ts index 3c3f1e94063cc..e885a235dd5bb 100644 --- a/packages/playwright/src/program.ts +++ b/packages/playwright/src/program.ts @@ -301,7 +301,6 @@ function overridesFromOptions(options: { [key: string]: any }): ConfigCLIOverrid ignoreSnapshots: options.ignoreSnapshots ? !!options.ignoreSnapshots : undefined, updateSnapshots: options.updateSnapshots, updateSourceMethod: options.updateSourceMethod, - runAgents: options.runAgents, workers: options.workers, }; diff --git a/packages/playwright/src/runner/dispatcher.ts b/packages/playwright/src/runner/dispatcher.ts index 8824d38d9f0de..ff4660213f943 100644 --- a/packages/playwright/src/runner/dispatcher.ts +++ b/packages/playwright/src/runner/dispatcher.ts @@ -22,13 +22,11 @@ import { WorkerHost } from './workerHost'; import { serializeConfig } from '../common/ipc'; import { addLocationAndSnippetToError } from '../reporters/internalReporter'; import { serializeError } from '../util'; -import { Storage } from './storage'; import type { FailureTracker } from './failureTracker'; import type { ProcessExitData } from './processHost'; import type { TestGroup } from './testGroups'; import type { TestError, TestResult, TestStep } from '../../types/testReporter'; -import type * as ipc from '../common/ipc'; import type { FullConfigInternal } from '../common/config'; import type { AttachmentPayload, DonePayload, RunPayload, SerializedConfig, StepBeginPayload, StepEndPayload, TeardownErrorsPayload, TestBeginPayload, TestEndPayload, TestOutputPayload, TestPausedPayload } from '../common/ipc'; import type { Suite } from '../common/test'; @@ -261,12 +259,6 @@ export class Dispatcher { const producedEnv = this._producedEnvByProjectId.get(testGroup.projectId) || {}; this._producedEnvByProjectId.set(testGroup.projectId, { ...producedEnv, ...worker.producedEnv() }); }); - worker.onRequest('cloneStorage', async (params: ipc.CloneStoragePayload) => { - return await Storage.clone(params.storageFile, outputDir); - }); - worker.onRequest('upstreamStorage', async (params: ipc.UpstreamStoragePayload) => { - await Storage.upstream(params.storageFile, params.storageOutFile); - }); return worker; } diff --git a/packages/playwright/src/runner/storage.ts b/packages/playwright/src/runner/storage.ts deleted file mode 100644 index 8ac4e8412566d..0000000000000 --- a/packages/playwright/src/runner/storage.ts +++ /dev/null @@ -1,80 +0,0 @@ -/** - * Copyright Microsoft Corporation. All rights reserved. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import fs from 'fs'; -import path from 'path'; - -import { createGuid } from 'playwright-core/lib/utils'; - -type StorageEnties = Record; - -export class Storage { - private static _storages = new Map(); - private static _serializeQueue: Promise = Promise.resolve(); - - private _fileName: string; - private _lastSnapshotFileName: string | undefined; - private _entriesPromise: Promise | undefined; - - static clone(storageFile: string, outputDir: string): Promise { - return Storage._withStorage(storageFile, storage => storage._clone(outputDir)); - } - - static upstream(storageFile: string, storageOutFile: string) { - return Storage._withStorage(storageFile, storage => storage._upstream(storageOutFile)); - } - - private static _withStorage(fileName: string, runnable: (storage: Storage) => Promise) { - this._serializeQueue = this._serializeQueue.then(() => { - let storage = Storage._storages.get(fileName); - if (!storage) { - storage = new Storage(fileName); - Storage._storages.set(fileName, storage); - } - return runnable(storage); - }); - return this._serializeQueue; - } - - private constructor(fileName: string) { - this._fileName = fileName; - } - - async _clone(outputDir: string): Promise { - const entries = await this._load(); - if (this._lastSnapshotFileName) - return this._lastSnapshotFileName; - const snapshotFile = path.join(outputDir, `pw-storage-${createGuid()}.json`); - await fs.promises.writeFile(snapshotFile, JSON.stringify(entries, null, 2)).catch(() => {}); - this._lastSnapshotFileName = snapshotFile; - return snapshotFile; - } - - async _upstream(storageOutFile: string) { - const entries = await this._load(); - const newEntries = await fs.promises.readFile(storageOutFile, 'utf8').then(JSON.parse).catch(() => ({})) as StorageEnties; - for (const [key, newValue] of Object.entries(newEntries)) - entries[key] = newValue; - this._lastSnapshotFileName = undefined; - await fs.promises.writeFile(this._fileName, JSON.stringify(entries, null, 2)); - } - - private async _load(): Promise { - if (!this._entriesPromise) - this._entriesPromise = fs.promises.readFile(this._fileName, 'utf8').then(JSON.parse).catch(() => ({})); - return this._entriesPromise; - } -} diff --git a/packages/playwright/src/runner/testRunner.ts b/packages/playwright/src/runner/testRunner.ts index db6edbead170d..05bfbdaf9bbb1 100644 --- a/packages/playwright/src/runner/testRunner.ts +++ b/packages/playwright/src/runner/testRunner.ts @@ -69,7 +69,6 @@ export type RunTestsParams = { workers?: number | string; updateSnapshots?: 'all' | 'changed' | 'missing' | 'none'; updateSourceMethod?: 'overwrite' | 'patch' | '3way'; - runAgents?: 'all' | 'missing' | 'none'; reporters?: string[], trace?: 'on' | 'off'; video?: 'on' | 'off'; @@ -334,7 +333,6 @@ export class TestRunner extends EventEmitter { }, ...(params.updateSnapshots ? { updateSnapshots: params.updateSnapshots } : {}), ...(params.updateSourceMethod ? { updateSourceMethod: params.updateSourceMethod } : {}), - ...(params.runAgents ? { runAgents: params.runAgents } : {}), ...(params.workers ? { workers: params.workers } : {}), }; diff --git a/packages/playwright/src/runner/testServer.ts b/packages/playwright/src/runner/testServer.ts index 657d6700c11f1..627e4f7a25ec1 100644 --- a/packages/playwright/src/runner/testServer.ts +++ b/packages/playwright/src/runner/testServer.ts @@ -78,7 +78,6 @@ export type RunTestsParams = { workers?: number | string; updateSnapshots?: 'all' | 'changed' | 'missing' | 'none'; updateSourceMethod?: 'overwrite' | 'patch' | '3way'; - runAgents?: boolean; reporters?: string[], trace?: 'on' | 'off'; video?: 'on' | 'off'; diff --git a/packages/playwright/src/worker/testInfo.ts b/packages/playwright/src/worker/testInfo.ts index 30e044bded66b..1a5a6f3091055 100644 --- a/packages/playwright/src/worker/testInfo.ts +++ b/packages/playwright/src/worker/testInfo.ts @@ -70,8 +70,6 @@ type TestInfoCallbacks = { onStepEnd: (payload: ipc.StepEndPayload) => void; onAttach: (payload: ipc.AttachmentPayload) => void; onTestPaused: (payload: ipc.TestPausedPayload) => Promise; - onCloneStorage: (payload: ipc.CloneStoragePayload) => Promise; - onUpstreamStorage: (payload: ipc.UpstreamStoragePayload) => Promise; }; export const emtpyTestInfoCallbacks: TestInfoCallbacks = { @@ -79,8 +77,6 @@ export const emtpyTestInfoCallbacks: TestInfoCallbacks = { onStepEnd: () => {}, onAttach: () => {}, onTestPaused: () => Promise.reject(new Error('TestInfoImpl not initialized')), - onCloneStorage: () => Promise.reject(new Error('TestInfoImpl not initialized')), - onUpstreamStorage: () => Promise.resolve(), }; export class TestInfoImpl implements TestInfo { @@ -647,14 +643,6 @@ export class TestInfoImpl implements TestInfo { this._timeoutManager.setTimeout(timeout); } - async _cloneStorage(storageFile: string): Promise { - return await this._callbacks.onCloneStorage!({ storageFile }); - } - - async _upstreamStorage(storageFile: string, storageOutFile: string) { - await this._callbacks.onUpstreamStorage!({ storageFile, storageOutFile }); - } - artifactsDir(): string { return this._workerParams.artifactsDir; } diff --git a/packages/playwright/src/worker/workerMain.ts b/packages/playwright/src/worker/workerMain.ts index bdd46abaf00ad..4fcd1e57e6a4a 100644 --- a/packages/playwright/src/worker/workerMain.ts +++ b/packages/playwright/src/worker/workerMain.ts @@ -301,8 +301,6 @@ export class WorkerMain extends ProcessRunner { this.dispatchEvent('testPaused', payload); return this._resumePromise; }, - onCloneStorage: async payload => this.sendRequest('cloneStorage', payload), - onUpstreamStorage: payload => this.sendRequest('upstreamStorage', payload), }); const processAnnotation = (annotation: TestAnnotation) => { testInfo.annotations.push(annotation); diff --git a/packages/playwright/types/test.d.ts b/packages/playwright/types/test.d.ts index 031c71d63deb9..6ced6b68d5cdf 100644 --- a/packages/playwright/types/test.d.ts +++ b/packages/playwright/types/test.d.ts @@ -15,7 +15,7 @@ * limitations under the License. */ -import type { APIRequestContext, Browser, BrowserContext, BrowserContextOptions, Page, PageAgent, LaunchOptions, ViewportSize, Geolocation, HTTPCredentials, Locator, APIResponse, PageScreenshotOptions } from 'playwright-core'; +import type { APIRequestContext, Browser, BrowserContext, BrowserContextOptions, Page, LaunchOptions, ViewportSize, Geolocation, HTTPCredentials, Locator, APIResponse, PageScreenshotOptions } from 'playwright-core'; export * from 'playwright-core'; export type BlobReporterOptions = { outputDir?: string, fileName?: string }; @@ -1630,14 +1630,6 @@ interface TestConfig { */ retries?: number; - /** - * Whether to run LLM agent for [PageAgent](https://playwright.dev/docs/api/class-pageagent): - * - "all" disregards existing cache and performs all actions via LLM - * - "missing" only performs actions that don't have generated cache actions - * - "none" does not talk to LLM at all, relies on the cached actions (default) - */ - runAgents?: "all"|"missing"|"none"; - /** * Shard tests and execute only the selected shard. Specify in the one-based form like `{ total: 5, current: 2 }`. * @@ -2094,14 +2086,6 @@ export interface FullConfig { */ rootDir: string; - /** - * Whether to run LLM agent for [PageAgent](https://playwright.dev/docs/api/class-pageagent): - * - "all" disregards existing cache and performs all actions via LLM - * - "missing" only performs actions that don't have generated cache actions - * - "none" does not talk to LLM at all, relies on the cached actions (default) - */ - runAgents: "all"|"missing"|"none"; - /** * See [testConfig.shard](https://playwright.dev/docs/api/class-testconfig#test-config-shard). */ @@ -6970,25 +6954,6 @@ export interface PlaywrightWorkerOptions { export type ScreenshotMode = 'off' | 'on' | 'only-on-failure' | 'on-first-failure'; export type TraceMode = 'off' | 'on' | 'retain-on-failure' | 'on-first-retry' | 'on-all-retries' | 'retain-on-first-failure' | 'retain-on-failure-and-retries'; export type VideoMode = 'off' | 'on' | 'retain-on-failure' | 'on-first-retry'; -export type AgentOptions = { - provider?: { - api: 'openai' | 'openai-compatible' | 'anthropic' | 'google'; - apiEndpoint?: string; - apiKey: string; - apiTimeout?: number; - model: string; - }, - limits?: { - maxTokens?: number; - maxActions?: number; - maxActionRetries?: number; - }; - cachePathTemplate?: string; - runAgents?: 'all' | 'missing' | 'none'; - secrets?: { [key: string]: string }; - systemPrompt?: string; -}; - /** * Playwright Test provides many options to configure test environment, * [Browser](https://playwright.dev/docs/api/class-browser), @@ -7028,7 +6993,6 @@ export type AgentOptions = { * */ export interface PlaywrightTestOptions { - agentOptions: AgentOptions | undefined; /** * Whether to automatically download all the attachments. Defaults to `true` where all the downloads are accepted. * @@ -7738,7 +7702,6 @@ export interface PlaywrightTestArgs { * */ request: APIRequestContext; - agent: PageAgent; } type ExcludeProps = { diff --git a/packages/protocol/src/channels.d.ts b/packages/protocol/src/channels.d.ts index ee6b3a99e6907..65916f58ded99 100644 --- a/packages/protocol/src/channels.d.ts +++ b/packages/protocol/src/channels.d.ts @@ -26,7 +26,6 @@ export interface Channel { // ----------- Initializer Traits ----------- export type InitializerTraits = - T extends PageAgentChannel ? PageAgentInitializer : T extends JsonPipeChannel ? JsonPipeInitializer : T extends AndroidDeviceChannel ? AndroidDeviceInitializer : T extends AndroidSocketChannel ? AndroidSocketInitializer : @@ -65,7 +64,6 @@ export type InitializerTraits = // ----------- Event Traits ----------- export type EventsTraits = - T extends PageAgentChannel ? PageAgentEvents : T extends JsonPipeChannel ? JsonPipeEvents : T extends AndroidDeviceChannel ? AndroidDeviceEvents : T extends AndroidSocketChannel ? AndroidSocketEvents : @@ -104,7 +102,6 @@ export type EventsTraits = // ----------- EventTarget Traits ----------- export type EventTargetTraits = - T extends PageAgentChannel ? PageAgentEventTarget : T extends JsonPipeChannel ? JsonPipeEventTarget : T extends AndroidDeviceChannel ? AndroidDeviceEventTarget : T extends AndroidSocketChannel ? AndroidSocketEventTarget : @@ -2158,7 +2155,6 @@ export interface PageChannel extends PageEventTarget, EventTargetChannel { videoStart(params: PageVideoStartParams, progress?: Progress): Promise; videoStop(params?: PageVideoStopParams, progress?: Progress): Promise; updateSubscription(params: PageUpdateSubscriptionParams, progress?: Progress): Promise; - agent(params: PageAgentParams, progress?: Progress): Promise; setDockTile(params: PageSetDockTileParams, progress?: Progress): Promise; } export type PageBindingCallEvent = { @@ -2718,41 +2714,6 @@ export type PageUpdateSubscriptionOptions = { }; export type PageUpdateSubscriptionResult = void; -export type PageAgentParams = { - api?: string, - apiKey?: string, - apiEndpoint?: string, - apiTimeout?: number, - apiCacheFile?: string, - cacheFile?: string, - cacheOutFile?: string, - doNotRenderActive?: boolean, - maxActions?: number, - maxActionRetries?: number, - maxTokens?: number, - model?: string, - secrets?: NameValue[], - systemPrompt?: string, -}; -export type PageAgentOptions = { - api?: string, - apiKey?: string, - apiEndpoint?: string, - apiTimeout?: number, - apiCacheFile?: string, - cacheFile?: string, - cacheOutFile?: string, - doNotRenderActive?: boolean, - maxActions?: number, - maxActionRetries?: number, - maxTokens?: number, - model?: string, - secrets?: NameValue[], - systemPrompt?: string, -}; -export type PageAgentResult = { - agent: PageAgentChannel, -}; export type PageSetDockTileParams = { image: Binary, }; @@ -5262,101 +5223,3 @@ export interface JsonPipeEvents { 'closed': JsonPipeClosedEvent; } -// ----------- PageAgent ----------- -export type PageAgentInitializer = { - page: PageChannel, -}; -export interface PageAgentEventTarget { - on(event: 'turn', callback: (params: PageAgentTurnEvent) => void): this; -} -export interface PageAgentChannel extends PageAgentEventTarget, EventTargetChannel { - _type_PageAgent: boolean; - perform(params: PageAgentPerformParams, progress?: Progress): Promise; - expect(params: PageAgentExpectParams, progress?: Progress): Promise; - extract(params: PageAgentExtractParams, progress?: Progress): Promise; - dispose(params?: PageAgentDisposeParams, progress?: Progress): Promise; - usage(params?: PageAgentUsageParams, progress?: Progress): Promise; -} -export type PageAgentTurnEvent = { - role: string, - message: string, - usage?: { - inputTokens: number, - outputTokens: number, - }, -}; -export type PageAgentPerformParams = { - task: string, - maxActions?: number, - maxActionRetries?: number, - maxTokens?: number, - cacheKey?: string, - timeout?: number, -}; -export type PageAgentPerformOptions = { - maxActions?: number, - maxActionRetries?: number, - maxTokens?: number, - cacheKey?: string, - timeout?: number, -}; -export type PageAgentPerformResult = { - usage: AgentUsage, -}; -export type PageAgentExpectParams = { - expectation: string, - maxActions?: number, - maxActionRetries?: number, - maxTokens?: number, - cacheKey?: string, - timeout?: number, -}; -export type PageAgentExpectOptions = { - maxActions?: number, - maxActionRetries?: number, - maxTokens?: number, - cacheKey?: string, - timeout?: number, -}; -export type PageAgentExpectResult = { - usage: AgentUsage, -}; -export type PageAgentExtractParams = { - query: string, - schema: any, - maxActions?: number, - maxActionRetries?: number, - maxTokens?: number, - cacheKey?: string, - timeout?: number, -}; -export type PageAgentExtractOptions = { - maxActions?: number, - maxActionRetries?: number, - maxTokens?: number, - cacheKey?: string, - timeout?: number, -}; -export type PageAgentExtractResult = { - result: any, - usage: AgentUsage, -}; -export type PageAgentDisposeParams = {}; -export type PageAgentDisposeOptions = {}; -export type PageAgentDisposeResult = void; -export type PageAgentUsageParams = {}; -export type PageAgentUsageOptions = {}; -export type PageAgentUsageResult = { - usage: AgentUsage, -}; - -export interface PageAgentEvents { - 'turn': PageAgentTurnEvent; -} - -export type AgentUsage = { - turns: number, - inputTokens: number, - outputTokens: number, -}; - diff --git a/packages/protocol/src/protocol.yml b/packages/protocol/src/protocol.yml index a3b7a4a63007c..9b75b56b5c3eb 100644 --- a/packages/protocol/src/protocol.yml +++ b/packages/protocol/src/protocol.yml @@ -2129,28 +2129,6 @@ Page: - requestFailed enabled: boolean - agent: - internal: true - parameters: - api: string? - apiKey: string? - apiEndpoint: string? - apiTimeout: int? - apiCacheFile: string? - cacheFile: string? - cacheOutFile: string? - doNotRenderActive: boolean? - maxActions: int? - maxActionRetries: int? - maxTokens: int? - model: string? - secrets: - type: array? - items: NameValue - systemPrompt: string? - returns: - agent: PageAgent - setDockTile: internal: true parameters: @@ -4449,77 +4427,3 @@ JsonPipe: reason: string? -PageAgent: - type: interface - extends: EventTarget - - initializer: - page: Page - - commands: - perform: - title: 'Perform "{task}"' - parameters: - task: string - $mixin: PageAgentOptions - - returns: - usage: AgentUsage - - expect: - title: 'Expect "{expectation}"' - parameters: - expectation: string - $mixin: PageAgentOptions - - returns: - usage: AgentUsage - - extract: - title: 'Extract "{query}"' - parameters: - query: string - schema: json - $mixin: PageAgentOptions - - returns: - result: json - usage: AgentUsage - - dispose: - internal: true - - usage: - title: 'Get agent usage' - group: configuration - returns: - usage: AgentUsage - - events: - - turn: - parameters: - role: string - message: string - usage: - type: object? - properties: - inputTokens: int - outputTokens: int - - -PageAgentOptions: - type: mixin - properties: - maxActions: int? - maxActionRetries: int? - maxTokens: int? - cacheKey: string? - timeout: int? - -AgentUsage: - type: object - properties: - turns: int - inputTokens: int - outputTokens: int diff --git a/tests/library/agent-expect.spec.ts b/tests/library/agent-expect.spec.ts deleted file mode 100644 index 988780dcb2157..0000000000000 --- a/tests/library/agent-expect.spec.ts +++ /dev/null @@ -1,367 +0,0 @@ -/** - * Copyright (c) Microsoft Corporation. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { stripAnsi } from '../config/utils'; -import { browserTest as test, expect } from '../config/browserTest'; -import { setCacheObject, runAgent, generateAgent, cacheObject } from './agent-helpers'; - -// LOWIRE_NO_CACHE=1 to generate api caches -// LOWIRE_FORCE_CACHE=1 to force api caches - -test('expectVisible not found error', async ({ context }) => { - await setCacheObject({ - 'submit button is visible': { - actions: [{ - code: '', - method: 'expectVisible', - selector: `internal:role=button`, - }], - }, - }); - const { page, agent } = await runAgent(context); - await page.setContent(``); - const error = await agent.expect('submit button is visible').catch(e => e); - expect(stripAnsi(error.message)).toContain(`pageAgent.expect: expect(locator).toBeVisible() failed - -Locator: getByRole('button') -Expected: visible -Timeout: 5000ms -Error: element(s) not found - -Call log: - - Expect Visible with timeout 5000ms - - waiting for getByRole('button')`); -}); - -test('expectVisible not visible error', async ({ context }) => { - await setCacheObject({ - 'submit button is visible': { - actions: [{ - code: '', - method: 'expectVisible', - selector: `button`, - }], - }, - }); - const { page, agent } = await runAgent(context); - await page.setContent(``); - const error = await agent.expect('submit button is visible').catch(e => e); - expect(stripAnsi(error.message)).toContain(`pageAgent.expect: expect(locator).toBeVisible() failed - -Locator: locator('button') -Expected: visible -Received: hidden -Timeout: 5000ms - -Call log: - - Expect Visible with timeout 5000ms - - waiting for locator('button')`); -}); - -test('not expectVisible visible error', async ({ context }) => { - await setCacheObject({ - 'submit button is not visible': { - actions: [{ - code: '', - method: 'expectVisible', - selector: `button`, - isNot: true, - }], - }, - }); - const { page, agent } = await runAgent(context); - await page.setContent(``); - const error = await agent.expect('submit button is not visible').catch(e => e); - expect(stripAnsi(error.message)).toContain(`pageAgent.expect: expect(locator).not.toBeVisible() failed - -Locator: locator('button') -Expected: not visible -Received: visible -Timeout: 5000ms - -Call log: - - Expect Visible with timeout 5000ms - - waiting for locator('button')`); -}); - -test('expectChecked not checked error', async ({ context }) => { - await setCacheObject({ - 'checkbox is checked': { - actions: [{ - code: '', - method: 'expectValue', - type: 'checkbox', - value: 'true', - selector: `input`, - }], - }, - }); - const { page, agent } = await runAgent(context); - await page.setContent(``); - const error = await agent.expect('checkbox is checked').catch(e => e); - expect(stripAnsi(error.message)).toContain(`pageAgent.expect: expect(locator).toBeChecked() failed - -Locator: locator('input') -Expected: checked -Received: unchecked -Timeout: 5000ms - -Call log: - - Expect Checked with timeout 5000ms - - waiting for locator('input')`); -}); - -test('expectValue wrong value error', async ({ context }) => { - await setCacheObject({ - 'input has value "hello"': { - actions: [{ - code: '', - method: 'expectValue', - type: 'textbox', - value: 'hello', - selector: `input`, - }], - }, - }); - const { page, agent } = await runAgent(context); - await page.setContent(``); - const error = await agent.expect('input has value "hello"').catch(e => e); - expect(stripAnsi(error.message)).toContain(`pageAgent.expect: expect(locator).toHaveValue(expected) failed - -Locator: locator('input') -Expected: hello -Received: world -Timeout: 5000ms - -Call log: - - Expect Value with timeout 5000ms - - waiting for locator('input')`); -}); - -test('expectAria wrong snapshot error', async ({ context }) => { - await setCacheObject({ - 'two items are visible': { - actions: [{ - code: '', - method: 'expectAria', - template: '- list:\n - listitem: one\n - listitem: two', - }], - }, - }); - const { page, agent } = await runAgent(context); - await page.setContent(`
  • one
  • two
`); - const error = await agent.expect('two items are visible').catch(e => e); - const errorMessage = `pageAgent.expect: expect(locator).toMatchAriaSnapshot(expected) failed - -Locator: locator('body') -Expected: -- list: - - listitem: one - - listitem: two -Received: -- list: - - listitem: one -Timeout: 5000ms - -Call log: - - Expect Aria Snapshot with timeout 5000ms - - waiting for locator('body')`.replace('Expected:', 'Expected: ').replace('Received:', 'Received: '); - expect(stripAnsi(error.message)).toContain(errorMessage); -}); - -test('expect timeout during run', async ({ context }) => { - { - const { page, agent } = await generateAgent(context); - await page.setContent(``); - await agent.expect('submit button is visible'); - } - expect(await cacheObject()).toEqual({ - 'submit button is visible': { - actions: [expect.objectContaining({ method: 'expectVisible' })], - }, - }); - { - const { page, agent } = await runAgent(context); - await page.setContent(``); - const error = await agent.expect('submit button is visible', { timeout: 3000 }).catch(e => e); - expect(stripAnsi(error.message)).toContain(`pageAgent.expect: expect(locator).toBeVisible() failed - -Locator: getByRole('button', { name: 'Submit' }) -Expected: visible -Timeout: 3000ms -Error: element(s) not found - -Call log: - - Expect Visible with timeout 3000ms`); - } -}); - -test('expect timeout during run from agent options', async ({ context }) => { - { - const { page, agent } = await generateAgent(context); - await page.setContent(``); - await agent.expect('submit button is visible'); - } - expect(await cacheObject()).toEqual({ - 'submit button is visible': { - actions: [expect.objectContaining({ method: 'expectVisible' })], - }, - }); - { - const { page, agent } = await runAgent(context, { expect: { timeout: 3000 } }); - await page.setContent(``); - const error = await agent.expect('submit button is visible').catch(e => e); - expect(stripAnsi(error.message)).toContain(`pageAgent.expect: expect(locator).toBeVisible() failed`); - expect(stripAnsi(error.message)).toContain(`Expect Visible with timeout 3000ms`); - } -}); - -test('expect timeout during generate', async ({ context }) => { - const { page, agent } = await generateAgent(context, { limits: { maxActionRetries: 0 } }); - await page.setContent(``); - const error = await agent.expect('input has value "hello"').catch(e => e); - expect(stripAnsi(error.message)).toContain(`pageAgent.expect: Agentic loop failed: Failed to perform action after 0 tool call retries -Call log: - - Expect Value - - waiting for getByRole('textbox')`); - expect(stripAnsi(error.message)).toContain(`- unexpected value "bye"`); -}); - -test('expectURL success', async ({ context, server }) => { - const secrets = { - SERVER: server.PREFIX - }; - { - const { page, agent } = await generateAgent(context, { secrets }); - await page.goto(server.PREFIX + '/counter.html'); - await agent.expect('page URL is /counter.html'); - } - expect(await cacheObject()).toEqual({ - 'page URL is /counter.html': { - actions: [expect.objectContaining({ method: 'expectURL' })], - }, - }); - { - const { page, agent } = await runAgent(context, { secrets }); - await page.goto(server.PREFIX + '/counter.html'); - await agent.expect('page URL is /counter.html'); - } -}); - -test('expectURL wrong URL error', async ({ context, server }) => { - const secrets = { - SERVER: server.PREFIX - }; - { - const { page, agent } = await generateAgent(context, { secrets }); - await page.goto(server.PREFIX + '/counter.html'); - await agent.expect('page URL is /counter.html'); - } - expect(await cacheObject()).toEqual({ - 'page URL is /counter.html': { - actions: [expect.objectContaining({ method: 'expectURL' })], - }, - }); - { - const { page, agent } = await runAgent(context, { secrets }); - await page.goto(server.PREFIX + '/empty.html'); - const error = await agent.expect('page URL is /counter.html').catch(e => e); - expect(stripAnsi(error.message)).toContain(`pageAgent.expect: expect(page).toHaveURL(expected) failed`); - expect(stripAnsi(error.message)).toContain(`Received: SERVER/empty.html`); - } -}); - -test('expectURL with regex', async ({ context, server }) => { - const secrets = { - SERVER: server.PREFIX - }; - { - const { page, agent } = await generateAgent(context, { secrets }); - await page.goto(server.PREFIX + '/counter.html'); - await agent.expect('page URL matches /counter pattern'); - } - expect(await cacheObject()).toEqual({ - 'page URL matches /counter pattern': { - actions: [expect.objectContaining({ method: 'expectURL', regex: expect.any(String) })], - }, - }); - { - const { page, agent } = await runAgent(context, { secrets }); - await page.goto(server.PREFIX + '/counter.html'); - await agent.expect('page URL matches /counter pattern'); - } -}); - -test('expectURL with regex error', async ({ context, server }) => { - const secrets = { - SERVER: server.PREFIX - }; - { - const { page, agent } = await generateAgent(context, { secrets }); - await page.goto(server.PREFIX + '/counter.html'); - await agent.expect('page URL matches /counter pattern'); - } - expect(await cacheObject()).toEqual({ - 'page URL matches /counter pattern': { - actions: [expect.objectContaining({ method: 'expectURL', regex: expect.any(String) })], - }, - }); - { - const { page, agent } = await runAgent(context, { secrets }); - await page.goto(server.PREFIX + '/empty.html'); - const error = await agent.expect('page URL matches /counter pattern').catch(e => e); - expect(stripAnsi(error.message)).toContain(`pageAgent.expect: expect(page).toHaveURL(expected) failed`); - expect(stripAnsi(error.message)).toContain(`Received: SERVER/empty.html`); - } -}); - -test('expectTitle success', async ({ context }) => { - { - const { page, agent } = await generateAgent(context); - await page.setContent(`My Page Title`); - await agent.expect('page title is "My Page Title"'); - } - expect(await cacheObject()).toEqual({ - 'page title is "My Page Title"': { - actions: [expect.objectContaining({ method: 'expectTitle' })], - }, - }); - { - const { page, agent } = await runAgent(context); - await page.setContent(`My Page Title`); - await agent.expect('page title is "My Page Title"'); - } -}); - -test('expectTitle wrong title error', async ({ context }) => { - { - const { page, agent } = await generateAgent(context); - await page.setContent(`Other Title`); - await agent.expect('page title is "Other Title"'); - } - expect(await cacheObject()).toEqual({ - 'page title is "Other Title"': { - actions: [expect.objectContaining({ method: 'expectTitle' })], - }, - }); - { - const { page, agent } = await runAgent(context); - await page.setContent(`My Page Title`); - const error = await agent.expect('page title is "Other Title"').catch(e => e); - expect(stripAnsi(error.message)).toContain(`pageAgent.expect: expect(page).toHaveTitle(expected) failed`); - expect(stripAnsi(error.message)).toContain(`Received: My Page Title`); - } -}); diff --git a/tests/library/agent-helpers.ts b/tests/library/agent-helpers.ts deleted file mode 100644 index 5a718082df48d..0000000000000 --- a/tests/library/agent-helpers.ts +++ /dev/null @@ -1,81 +0,0 @@ -/** - * Copyright (c) Microsoft Corporation. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import fs from 'fs'; -import path from 'path'; - -import { browserTest as test } from '../config/browserTest'; -import type { BrowserContext, Page, PageAgent } from '@playwright/test'; - -export function cacheFile() { - return test.info().outputPath('agent-cache.json'); -} - -export async function cacheObject() { - return JSON.parse(await fs.promises.readFile(cacheFile(), 'utf8')); -} - -export async function setCacheObject(object: any) { - await fs.promises.writeFile(cacheFile(), JSON.stringify(object, null, 2), 'utf8'); -} - -type AgentOptions = Parameters[0]; - -export async function generateAgent(context: BrowserContext, options: AgentOptions = {}) { - const apiCacheFile = path.join(__dirname, '__llm_cache__', sanitizeFileName(test.info().titlePath.join(' ')) + '.json'); - - const page = await context.newPage(); - const agent = await page.agent({ - provider: { - api: 'anthropic' as const, - apiKey: process.env.AZURE_SONNET_API_KEY ?? 'dummy', - apiEndpoint: process.env.AZURE_SONNET_ENDPOINT, - model: 'claude-sonnet-4-5', - ...{ _apiCacheFile: apiCacheFile } - }, - ...options, - cache: { - cacheFile: cacheFile(), - }, - ...{ _doNotRenderActive: true }, - }); - return { page, agent }; -} - -export async function runAgent(context: BrowserContext, options: AgentOptions = {}) { - const page = await context.newPage(); - const agent = await page.agent({ - ...options, - cache: { cacheFile: cacheFile() }, - ...{ _doNotRenderActive: true }, - }); - return { page, agent }; -} - -export async function run(context: BrowserContext, callback: (page: Page, agent: PageAgent) => Promise, options: { secrets?: Record } = {}) { - { - const { page, agent } = await generateAgent(context, options); - await callback(page, agent); - } - { - const { page, agent } = await runAgent(context, options); - await callback(page, agent); - } -} - -function sanitizeFileName(name: string): string { - return name.replace('.spec.ts', '').replace(/[^a-zA-Z0-9_]+/g, '-'); -} diff --git a/tests/library/agent-limits.spec.ts b/tests/library/agent-limits.spec.ts deleted file mode 100644 index 07d3f3014964c..0000000000000 --- a/tests/library/agent-limits.spec.ts +++ /dev/null @@ -1,54 +0,0 @@ -/** - * Copyright (c) Microsoft Corporation. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { browserTest as test, expect } from '../config/browserTest'; -import { generateAgent } from './agent-helpers'; - -test('should respect total max tokens limit', async ({ context }) => { - const { page, agent } = await generateAgent(context, { - limits: { - maxTokens: 123, - }, - }); - await page.setContent(` - - `); - const e = await agent.perform('Click submit button').catch(e => e); - expect(e.message.toLowerCase()).toContain('budget'); - expect(e.message).toContain('123'); -}); - -test('should respect call max tokens limit', async ({ context }) => { - const { page, agent } = await generateAgent(context); - await page.setContent(` - - `); - const e = await agent.perform('Click submit button', { maxTokens: 123 }).catch(e => e); - expect(e.message.toLowerCase()).toContain('budget'); - expect(e.message).toContain('123'); -}); - -test('should respect max actions limit', async ({ context }) => { - const { page, agent } = await generateAgent(context); - let clicked = 0; - await page.exposeFunction('clicked', () => ++clicked); - await page.setContent(` - - `); - const e = await agent.perform('Click the submit button 5 times', { maxActions: 3 }).catch(e => e); - expect(e.message).toContain('Failed to perform step, max tool calls (3) reached'); - expect(clicked).toBe(3); -}); diff --git a/tests/library/agent-perform.spec.ts b/tests/library/agent-perform.spec.ts deleted file mode 100644 index 7bca002b8b734..0000000000000 --- a/tests/library/agent-perform.spec.ts +++ /dev/null @@ -1,328 +0,0 @@ -/** - * Copyright (c) Microsoft Corporation. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { z as zod3 } from 'zod/v3'; -import * as zod4 from 'zod'; -import fs from 'fs'; - -import { browserTest as test, expect } from '../config/browserTest'; -import { run, generateAgent, cacheObject, runAgent, setCacheObject, cacheFile } from './agent-helpers'; - -// LOWIRE_NO_CACHE=1 to generate api caches -// LOWIRE_FORCE_CACHE=1 to force api caches - -test('click a button', async ({ context }) => { - await run(context, async (page, agent) => { - let clicked = 0; - await page.exposeFunction('clicked', () => ++clicked); - await page.setContent(``); - await agent.perform('click the Submit button'); - expect(clicked).toBe(1); - }); - - expect(await cacheObject()).toEqual({ - 'click the Submit button': { - actions: [{ - code: `await page.getByRole('button', { name: 'Submit' }).click();`, - method: 'click', - selector: `internal:role=button[name=\"Submit\"i]`, - }], - }, - }); -}); - -test('retrieve a secret', async ({ context }) => { - await run(context, async (page, agent) => { - await page.setContent(` - -
    - - `); - await agent.perform('Enter x-secret-email into the email field'); - await agent.expect('Check that email fields contains x-secret-email'); - await agent.perform('Press enter to add the email to the list'); - await agent.perform('Enter x-secret-email into the email field and press enter'); - await agent.expect('Check that list of emails contains two entries with x-secret-email'); - await expect(page.locator('body')).toMatchAriaSnapshot(` - - textbox "Email Address" - - list "list of emails": - - listitem: secret-email@at-microsoft.com - - listitem: secret-email@at-microsoft.com - `); - }, { secrets: { 'x-secret-email': 'secret-email@at-microsoft.com' } }); - - expect(await cacheObject()).toEqual(expect.objectContaining({ - 'Enter x-secret-email into the email field': { - actions: [{ - code: `await page.getByRole('textbox', { name: 'Email Address' }).fill('x-secret-email');`, - method: 'fill', - selector: `internal:role=textbox[name=\"Email Address\"i]`, - text: 'x-secret-email', - }], - }, - 'Check that email fields contains x-secret-email': { - 'actions': [{ - 'code': `await expect(page.getByRole('textbox', { name: 'Email Address' })).toHaveValue('x-secret-email');`, - 'method': 'expectValue', - 'selector': 'internal:role=textbox[name="Email Address"i]', - 'type': 'textbox', - 'value': 'x-secret-email', - }], - }, - })); -}); - -test('extract task', async ({ context }) => { - const { page, agent } = await generateAgent(context); - await page.setContent(` -
      -
    • Buy groceries [DONE]
    • -
    • Buy milk [PENDING]
    • -
    - `); - - await test.step('zod 3', async () => { - const { result } = await agent.extract('List todos with their statuses', zod3.object({ - items: zod3.object({ - title: zod3.string(), - completed: zod3.boolean(), - }).array(), - })); - - expect(result.items).toEqual([ - { title: 'Buy groceries', completed: true }, - { title: 'Buy milk', completed: false } - ]); - }); - - await test.step('zod 4', async () => { - const { result } = await agent.extract('List todos with their statuses', zod4.object({ - items: zod4.object({ - title: zod4.string(), - completed: zod4.boolean(), - }).array(), - })); - - expect(result.items).toEqual([ - { title: 'Buy groceries', completed: true }, - { title: 'Buy milk', completed: false } - ]); - }); -}); - -test('expect value', async ({ context }) => { - const task = ` - - Enter "bogus" into the email field - - Check that the value is in fact "bogus" - - Check that the error message is displayed -`; - - await run(context, async (page, agent) => { - await page.setContent(` - - - - `); - await agent.perform(task); - }); - - const expectations = {}; - expectations[task.trim()] = { - actions: [{ - code: `await page.getByRole('textbox', { name: 'Email Address' }).fill('bogus');`, - method: 'fill', - selector: `internal:role=textbox[name=\"Email Address\"i]`, - text: 'bogus', - }], - }; - expect(await cacheObject()).toEqual(expectations); -}); - -test('perform history', async ({ context }) => { - await run(context, async (page, agent) => { - let clicked = 0; - await page.exposeFunction('clicked', () => clicked++); - await page.setContent(` - - - - `); - await agent.perform('click the Fox button'); - await agent.perform('click the Fox button again'); - expect(clicked).toBe(2); - }); -}); - -test('perform run timeout', async ({ context }) => { - { - const { page, agent } = await generateAgent(context); - await page.setContent(` - - - `); - await agent.perform('click the Fox button'); - } - { - const { page, agent } = await runAgent(context); - await page.setContent(` - - - `); - const error = await agent.perform('click the Fox button', { timeout: 3000 }).catch(e => e); - expect(error.message).toContain('Timeout 3000ms exceeded.'); - expect(error.message).toContain(`waiting for getByRole('button', { name: 'Fox' })`); - } -}); - -test('perform run timeout inherited from page', async ({ context }) => { - { - const { page, agent } = await generateAgent(context); - await page.setContent(` - - - `); - await agent.perform('click the Fox button'); - } - { - const { page, agent } = await runAgent(context); - await page.setContent(` - - - `); - page.setDefaultTimeout(3000); - const error = await agent.perform('click the Fox button').catch(e => e); - expect(error.message).toContain('Timeout 3000ms exceeded.'); - expect(error.message).toContain(`waiting for getByRole('button', { name: 'Fox' })`); - } -}); - -test('invalid cache file throws error', async ({ context }) => { - await setCacheObject({ - 'some key': { - actions: [{ - method: 'invalid-method', - }], - }, - }); - const { agent } = await runAgent(context); - await expect(() => agent.perform('click the Test button')).rejects.toThrowError(` -Failed to parse cache file ${test.info().outputPath('agent-cache.json')}: -✖ Invalid input - → at [\"some key\"].actions[0].method -✖ Invalid input: expected string, received undefined - → at [\"some key\"].actions[0].code - `.trim()); -}); - -test('non-json cache file throws a nice error', async ({ context }) => { - await fs.promises.writeFile(cacheFile(), 'bogus', 'utf8'); - const { agent } = await runAgent(context); - const error = await agent.perform('click the Test button').catch(e => e); - expect(error.message).toContain(`Failed to parse cache file ${test.info().outputPath('agent-cache.json')}:`); - expect(error.message.toLowerCase()).toContain(`valid json`); -}); - -test('empty cache file works', async ({ context }) => { - await fs.promises.writeFile(cacheFile(), '', 'utf8'); - const { page, agent } = await generateAgent(context); - await page.setContent(``); - await agent.perform('click the Test button'); -}); - -test('missing apiKey throws a nice error', async ({ page }) => { - const agent = await page.agent({ provider: { api: 'anthropic', model: 'some model' } as any }); - const error = await agent.perform('click the Test button').catch(e => e); - expect(error.message).toContain(`This action requires API key to be set on the page agent`); -}); - -test('malformed apiEndpoint throws a nice error', async ({ page }) => { - const agent = await page.agent({ provider: { api: 'anthropic', model: 'some model', apiKey: 'some key', apiEndpoint: 'foobar' } }); - const error = await agent.perform('click the Test button').catch(e => e); - expect(error.message).toContain(`Agent API endpoint "foobar" is not a valid URL`); -}); - -test('perform reports error', async ({ context }) => { - const { page, agent } = await generateAgent(context); - await page.setContent(` - - - `); - const e = await agent.perform('click the Rabbit button').catch(e => e); - expect(e.message).toContain('Agent refused to perform action:'); -}); - -test('should dispatch event and respect dispose()', async ({ context, server, mode }) => { - test.skip(mode !== 'default', 'Different errors due to timing'); - - let apiResponse; - server.setRoute('/api', (req, res) => { - apiResponse = res; - // stall - }); - - const apiRequestPromise = server.waitForRequest('/api'); - const { page, agent } = await generateAgent(context, { - provider: { - api: 'anthropic', - apiKey: 'not a real key', - apiEndpoint: server.PREFIX + '/api', - model: 'no such model', - }, - }); - await page.setContent(``); - - const promiseCanceledByDispose = agent.perform('click the Wolf button').catch(e => e); - let promiseAfterDispose; - let eventCounter = 0; - - agent.on('turn', async () => { - ++eventCounter; - if (eventCounter > 1) - return; - - await apiRequestPromise; - void agent.dispose(); - promiseAfterDispose = agent.perform('click the Wolf button again').catch(e => e); - apiResponse.end(); - }); - - const errorCanceledByDispose = await promiseCanceledByDispose; - expect(errorCanceledByDispose.message).toContain('The agent is disposed'); - expect(errorCanceledByDispose.message).not.toContain('after being disposed'); - - const errorAfterDispose = await promiseAfterDispose; - expect(errorAfterDispose.message).toContain('Target page, context or browser has been closed'); - - // no more events after dispose - await page.waitForTimeout(1000); - expect(eventCounter).toBe(1); -}); diff --git a/tests/mcp/fixtures.ts b/tests/mcp/fixtures.ts index ddd1daeae2bf0..06d8b06b163d9 100644 --- a/tests/mcp/fixtures.ts +++ b/tests/mcp/fixtures.ts @@ -17,7 +17,6 @@ import fs from 'fs'; import path from 'path'; import { chromium } from 'playwright'; -import { Loop } from '@lowire/loop'; import { test as baseTest, expect as baseExpect } from '@playwright/test'; import { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js'; @@ -71,7 +70,6 @@ type TestFixtures = { server: TestServer; httpsServer: TestServer; mcpHeadless: boolean; - loop: Loop; }; type WorkerFixtures = { diff --git a/utils/generate_types/overrides-test.d.ts b/utils/generate_types/overrides-test.d.ts index 92f132d608ea5..d4ff176959753 100644 --- a/utils/generate_types/overrides-test.d.ts +++ b/utils/generate_types/overrides-test.d.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import type { APIRequestContext, Browser, BrowserContext, BrowserContextOptions, Page, PageAgent, LaunchOptions, ViewportSize, Geolocation, HTTPCredentials, Locator, APIResponse, PageScreenshotOptions } from 'playwright-core'; +import type { APIRequestContext, Browser, BrowserContext, BrowserContextOptions, Page, LaunchOptions, ViewportSize, Geolocation, HTTPCredentials, Locator, APIResponse, PageScreenshotOptions } from 'playwright-core'; export * from 'playwright-core'; export type BlobReporterOptions = { outputDir?: string, fileName?: string }; @@ -265,27 +265,7 @@ export interface PlaywrightWorkerOptions { export type ScreenshotMode = 'off' | 'on' | 'only-on-failure' | 'on-first-failure'; export type TraceMode = 'off' | 'on' | 'retain-on-failure' | 'on-first-retry' | 'on-all-retries' | 'retain-on-first-failure' | 'retain-on-failure-and-retries'; export type VideoMode = 'off' | 'on' | 'retain-on-failure' | 'on-first-retry'; -export type AgentOptions = { - provider?: { - api: 'openai' | 'openai-compatible' | 'anthropic' | 'google'; - apiEndpoint?: string; - apiKey: string; - apiTimeout?: number; - model: string; - }, - limits?: { - maxTokens?: number; - maxActions?: number; - maxActionRetries?: number; - }; - cachePathTemplate?: string; - runAgents?: 'all' | 'missing' | 'none'; - secrets?: { [key: string]: string }; - systemPrompt?: string; -}; - export interface PlaywrightTestOptions { - agentOptions: AgentOptions | undefined; acceptDownloads: boolean; bypassCSP: boolean; colorScheme: ColorScheme; @@ -324,7 +304,6 @@ export interface PlaywrightTestArgs { context: BrowserContext; page: Page; request: APIRequestContext; - agent: PageAgent; } type ExcludeProps = { diff --git a/utils/generate_types/overrides.d.ts b/utils/generate_types/overrides.d.ts index f5677d8f6150d..3a0c13eed56ea 100644 --- a/utils/generate_types/overrides.d.ts +++ b/utils/generate_types/overrides.d.ts @@ -85,10 +85,6 @@ export interface Page { }): Promise; } -export interface PageAgent { - extract(query: string, schema: Schema): Promise<{ result: InferZodSchema, usage: { turns: number, inputTokens: number, outputTokens: number } }>; -} - export interface Frame { evaluate(pageFunction: PageFunction, arg: Arg): Promise; evaluate(pageFunction: PageFunction, arg?: any): Promise;