diff --git a/.github/workflows/roll_browser_into_playwright.yml b/.github/workflows/roll_browser_into_playwright.yml index 38dd2b261151b..b08ba3213a61a 100644 --- a/.github/workflows/roll_browser_into_playwright.yml +++ b/.github/workflows/roll_browser_into_playwright.yml @@ -8,6 +8,7 @@ env: ELECTRON_SKIP_BINARY_DOWNLOAD: 1 BROWSER: ${{ github.event.client_payload.browser }} REVISION: ${{ github.event.client_payload.revision }} + BROWSER_VERSION: ${{ github.event.client_payload.browserVersion }} permissions: contents: write @@ -29,7 +30,7 @@ jobs: run: npx playwright install-deps - name: Roll to new revision run: | - ./utils/roll_browser.js $BROWSER $REVISION + ./utils/roll_browser.js $BROWSER $REVISION $BROWSER_VERSION npm run build - name: Prepare branch id: prepare-branch 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 index ef978f6928c07..266698b8dcc98 100644 --- 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 @@ -111,9 +111,9 @@ "The URL changes to #/active": { "actions": [ { - "method": "expectVisible", - "selector": "internal:role=link[name=\"Active\"i]", - "code": "await expect(page.getByRole('link', { name: 'Active' })).toBeVisible();" + "method": "expectURL", + "regex": "/.*#/active$/", + "code": "await expect(page).toHaveURL(/.*#\\/active$/);" } ] } 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 index f9f5e770a3f42..c8fd9fa7b5e8d 100644 --- 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 @@ -96,9 +96,9 @@ "The URL changes to #/completed": { "actions": [ { - "method": "expectVisible", - "selector": "internal:role=link[name=\"Completed\"i]", - "code": "await expect(page.getByRole('link', { name: 'Completed' })).toBeVisible();" + "method": "expectURL", + "regex": "/.*#/completed$/", + "code": "await expect(page).toHaveURL(/.*#\\/completed$/);" } ] }, @@ -106,8 +106,8 @@ "actions": [ { "method": "expectVisible", - "selector": "internal:text=\"1\"i", - "code": "await expect(page.getByText('1')).toBeVisible();" + "selector": "internal:role=button[name=\"Clear completed\"i]", + "code": "await expect(page.getByRole('button', { name: 'Clear completed' })).toBeVisible();" } ] } 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 index 1dc14c062088c..e31b7d2d24b82 100644 --- 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 @@ -86,9 +86,9 @@ "The URL changes to #/": { "actions": [ { - "method": "expectVisible", - "selector": "internal:role=link[name=\"All\"i]", - "code": "await expect(page.getByRole('link', { name: 'All' })).toBeVisible();" + "method": "expectURL", + "regex": "/.*#/$/", + "code": "await expect(page).toHaveURL(/.*#\\/$/);" } ] }, diff --git a/package-lock.json b/package-lock.json index 276b33af398c2..e6305bde873ad 100644 --- a/package-lock.json +++ b/package-lock.json @@ -61,8 +61,7 @@ "ws": "^8.17.1", "xml2js": "^0.5.0", "yaml": "2.6.0", - "zod": "^3.25.76", - "zod-to-json-schema": "^3.25.1" + "zod": "^4.3.5" }, "engines": { "node": ">=18" @@ -3021,6 +3020,15 @@ "devtools-protocol": "*" } }, + "node_modules/chromium-bidi/node_modules/zod": { + "version": "3.25.76", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz", + "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==", + "dev": true, + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + }, "node_modules/cliui": { "version": "7.0.4", "resolved": "https://registry.npmjs.org/cliui/-/cliui-7.0.4.tgz", @@ -8295,9 +8303,9 @@ "integrity": "sha512-rAbqEGa8ovJy4pyBxZM70hg4pE6gDgaQ0Sl9M3enG3I0d6H4XSAM3GeNGLKnsBpuijUow064sf7ww1nutC5/3w==" }, "node_modules/zod": { - "version": "3.25.76", - "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz", - "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==", + "version": "4.3.5", + "resolved": "https://registry.npmjs.org/zod/-/zod-4.3.5.tgz", + "integrity": "sha512-k7Nwx6vuWx1IJ9Bjuf4Zt1PEllcwe7cls3VNzm4CQ1/hgtFUK2bRNG3rvnpPUhFjmqJKAKtjV576KnUkHocg/g==", "dev": true, "funding": { "url": "https://github.com/sponsors/colinhacks" diff --git a/package.json b/package.json index 1180b5d837867..18b873438531b 100644 --- a/package.json +++ b/package.json @@ -98,7 +98,6 @@ "ws": "^8.17.1", "xml2js": "^0.5.0", "yaml": "2.6.0", - "zod": "^3.25.76", - "zod-to-json-schema": "^3.25.1" + "zod": "^4.3.5" } } diff --git a/packages/injected/src/injectedScript.ts b/packages/injected/src/injectedScript.ts index 014466f4938e1..ce18bf46aa252 100644 --- a/packages/injected/src/injectedScript.ts +++ b/packages/injected/src/injectedScript.ts @@ -47,7 +47,12 @@ import type { ElementText, TextMatcher } from './selectorUtils'; import type { Builtins } from './utilityScript'; -export type FrameExpectParams = Omit & { expectedValue?: any }; +export type FrameExpectParams = Omit & { + expectedValue?: any; + timeoutForLogs?: number; + explicitTimeout?: number; + noPreChecks?: boolean; +}; export type ElementState = 'visible' | 'hidden' | 'enabled' | 'disabled' | 'editable' | 'checked' | 'unchecked' | 'indeterminate' | 'stable'; export type ElementStateWithoutStable = Exclude; diff --git a/packages/playwright-client/types/types.d.ts b/packages/playwright-client/types/types.d.ts index 51dd92fb293b4..6e2a58a63d4d3 100644 --- a/packages/playwright-client/types/types.d.ts +++ b/packages/playwright-client/types/types.d.ts @@ -28,9 +28,11 @@ type ElementHandleWaitForSelectorOptionsNotHidden = ElementHandleWaitForSelector }; // @ts-ignore this will be any if zod is not installed -type ZodTypeAny = import('zod').ZodTypeAny; +import { ZodTypeAny, z } from 'zod'; // @ts-ignore this will be any if zod is not installed -type ZodInfer = import('zod').infer; +import * as z3 from 'zod/v3'; +type ZodSchema = ZodTypeAny | z3.ZodTypeAny; +type InferZodSchema = T extends z3.ZodTypeAny ? z3.infer : T extends ZodTypeAny ? z.infer : never; /** * Page provides methods to interact with a single tab in a [Browser](https://playwright.dev/docs/api/class-browser), @@ -5305,7 +5307,7 @@ export interface PageAgent { * @param schema * @param options */ - extract(query: string, schema: Schema): Promise<{ result: ZodInfer, usage: { turns: number, inputTokens: number, outputTokens: number } }>; + extract(query: string, schema: Schema): Promise<{ result: InferZodSchema, usage: { turns: number, inputTokens: number, outputTokens: number } }>; /** * Emitted when the agent makes a turn. */ diff --git a/packages/playwright-core/ThirdPartyNotices.txt b/packages/playwright-core/ThirdPartyNotices.txt index 601e7c3bb4410..78ef6edab1d8c 100644 --- a/packages/playwright-core/ThirdPartyNotices.txt +++ b/packages/playwright-core/ThirdPartyNotices.txt @@ -136,7 +136,7 @@ This project incorporates components from the projects listed below. The origina - yauzl@3.2.0 (https://github.com/thejoshwolfe/yauzl) - yazl@2.5.1 (https://github.com/thejoshwolfe/yazl) - zod-to-json-schema@3.25.1 (https://github.com/StefanTerdell/zod-to-json-schema) -- zod@3.25.76 (https://github.com/colinhacks/zod) +- zod@4.3.5 (https://github.com/colinhacks/zod) %% @hono/node-server@1.19.8 NOTICES AND INFORMATION BEGIN HERE ========================================= @@ -4043,7 +4043,7 @@ OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. ========================================= END OF zod-to-json-schema@3.25.1 AND INFORMATION -%% zod@3.25.76 NOTICES AND INFORMATION BEGIN HERE +%% zod@4.3.5 NOTICES AND INFORMATION BEGIN HERE ========================================= MIT License @@ -4067,7 +4067,7 @@ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ========================================= -END OF zod@3.25.76 AND INFORMATION +END OF zod@4.3.5 AND INFORMATION SUMMARY BEGIN HERE ========================================= diff --git a/packages/playwright-core/bundles/mcp/package-lock.json b/packages/playwright-core/bundles/mcp/package-lock.json index ef893ae960b7c..61586ebee9781 100644 --- a/packages/playwright-core/bundles/mcp/package-lock.json +++ b/packages/playwright-core/bundles/mcp/package-lock.json @@ -10,7 +10,7 @@ "dependencies": { "@lowire/loop": "^0.0.23", "@modelcontextprotocol/sdk": "^1.25.2", - "zod": "^3.25.76", + "zod": "^4.3.5", "zod-to-json-schema": "^3.25.1" } }, @@ -1114,10 +1114,9 @@ "license": "ISC" }, "node_modules/zod": { - "version": "3.25.76", - "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz", - "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==", - "license": "MIT", + "version": "4.3.5", + "resolved": "https://registry.npmjs.org/zod/-/zod-4.3.5.tgz", + "integrity": "sha512-k7Nwx6vuWx1IJ9Bjuf4Zt1PEllcwe7cls3VNzm4CQ1/hgtFUK2bRNG3rvnpPUhFjmqJKAKtjV576KnUkHocg/g==", "funding": { "url": "https://github.com/sponsors/colinhacks" } diff --git a/packages/playwright-core/bundles/mcp/package.json b/packages/playwright-core/bundles/mcp/package.json index 35c405041301d..039471d3abea7 100644 --- a/packages/playwright-core/bundles/mcp/package.json +++ b/packages/playwright-core/bundles/mcp/package.json @@ -5,7 +5,7 @@ "dependencies": { "@lowire/loop": "^0.0.23", "@modelcontextprotocol/sdk": "^1.25.2", - "zod": "^3.25.76", + "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 54794493f8a7f..aa3b37ae1bf28 100644 --- a/packages/playwright-core/bundles/mcp/src/mcpBundleImpl.ts +++ b/packages/playwright-core/bundles/mcp/src/mcpBundleImpl.ts @@ -24,5 +24,5 @@ export { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/ 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 { z } from 'zod'; +export * as z from 'zod'; export { zodToJsonSchema } from 'zod-to-json-schema'; diff --git a/packages/playwright-core/bundles/utils/package-lock.json b/packages/playwright-core/bundles/utils/package-lock.json index 4db8e8e9d0614..3d2a48a6e5f01 100644 --- a/packages/playwright-core/bundles/utils/package-lock.json +++ b/packages/playwright-core/bundles/utils/package-lock.json @@ -26,8 +26,7 @@ "signal-exit": "3.0.7", "socks-proxy-agent": "8.0.5", "ws": "8.17.1", - "yaml": "^2.6.0", - "zod": "^3.25.76" + "yaml": "^2.6.0" }, "devDependencies": { "@types/debug": "^4.1.7", @@ -442,15 +441,6 @@ "engines": { "node": ">= 14" } - }, - "node_modules/zod": { - "version": "3.25.76", - "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz", - "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==", - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/colinhacks" - } } } } diff --git a/packages/playwright-core/bundles/utils/package.json b/packages/playwright-core/bundles/utils/package.json index ec99d78e1271d..f0023dc38a928 100644 --- a/packages/playwright-core/bundles/utils/package.json +++ b/packages/playwright-core/bundles/utils/package.json @@ -21,8 +21,7 @@ "signal-exit": "3.0.7", "socks-proxy-agent": "8.0.5", "ws": "8.17.1", - "yaml": "^2.6.0", - "zod": "^3.25.76" + "yaml": "^2.6.0" }, "devDependencies": { "@types/debug": "^4.1.7", diff --git a/packages/playwright-core/bundles/utils/src/utilsBundleImpl.ts b/packages/playwright-core/bundles/utils/src/utilsBundleImpl.ts index 4f5823842812c..cc57a921c1256 100644 --- a/packages/playwright-core/bundles/utils/src/utilsBundleImpl.ts +++ b/packages/playwright-core/bundles/utils/src/utilsBundleImpl.ts @@ -66,6 +66,3 @@ export const wsSender = Sender; import yamlLibrary from 'yaml'; export const yaml = yamlLibrary; - -import zodLibrary from 'zod'; -export const zod = zodLibrary; diff --git a/packages/playwright-core/src/client/pageAgent.ts b/packages/playwright-core/src/client/pageAgent.ts index 10d64a9c72f0c..2837f5bbb11f3 100644 --- a/packages/playwright-core/src/client/pageAgent.ts +++ b/packages/playwright-core/src/client/pageAgent.ts @@ -20,7 +20,6 @@ import { Events } from './events'; import { Page } from './page'; import type * as api from '../../types/types'; -import type z from 'zod'; import type * as channels from '@protocol/channels'; type PageAgentOptions = { @@ -51,7 +50,7 @@ export class PageAgent extends ChannelOwner implement return { usage }; } - async extract(query: string, schema: Schema, options: PageAgentOptions = {}): Promise<{ result: z.infer, usage: channels.AgentUsage }> { + async extract(query: string, schema: Schema, options: PageAgentOptions = {}): Promise<{ result: any, usage: channels.AgentUsage }> { const { result, usage } = await this._channel.extract({ query, schema: this._page._platform.zodToJsonSchema(schema), ...options }); return { result, usage }; } diff --git a/packages/playwright-core/src/server/agent/actionRunner.ts b/packages/playwright-core/src/server/agent/actionRunner.ts index 0d57a3cc0db6d..151460693bed1 100644 --- a/packages/playwright-core/src/server/agent/actionRunner.ts +++ b/packages/playwright-core/src/server/agent/actionRunner.ts @@ -15,6 +15,8 @@ */ 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'; @@ -34,7 +36,7 @@ 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); + const callMetadata = callMetadataForAction(progress, frame, action, mode); callMetadata.log = parentMetadata.log; progress.metadata = callMetadata; @@ -52,6 +54,7 @@ export async function runAction(progress: Progress, mode: 'generate' | 'run', pa 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': @@ -101,16 +104,16 @@ async function innerRunAction(progress: Progress, mode: 'generate' | 'run', page await frame.uncheck(progress, action.selector, { ...commonOptions }); break; case 'expectVisible': { - await runExpect(frame, progress, action.selector, { expression: 'to.be.visible', isNot: !!action.isNot }, 'visible', 'toBeVisible', ''); + 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 expectedText = serializeExpectedTextValues([action.value]); - await runExpect(frame, progress, action.selector, { expression: 'to.have.value', expectedText, isNot: !!action.isNot }, action.value, 'toHaveValue', 'expected'); + await runExpect(frame, progress, mode, action.selector, { expression: 'to.have.value', expectedText, isNot: !!action.isNot }, action.value, 'toHaveValue', 'expected'); } else if (action.type === 'checkbox' || action.type === 'radio') { const expectedValue = { checked: action.value === 'true' }; - await runExpect(frame, progress, action.selector, { selector: action.selector, expression: 'to.be.checked', expectedValue, isNot: !!action.isNot }, action.value ? 'checked' : 'unchecked', 'toBeChecked', ''); + 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}`); } @@ -118,24 +121,44 @@ async function innerRunAction(progress: Progress, mode: 'generate' | 'run', page } case 'expectAria': { const expectedValue = parseAriaSnapshotUnsafe(yaml, action.template); - await runExpect(frame, progress, 'body', { expression: 'to.match.aria', expectedValue, isNot: !!action.isNot }, '\n' + action.template, 'toMatchAriaSnapshot', 'expected'); + await runExpect(frame, progress, mode, 'body', { expression: 'to.match.aria', expectedValue, isNot: !!action.isNot }, '\n' + action.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; } } } -async function runExpect(frame: Frame, progress: Progress, selector: string | undefined, options: FrameExpectParams, expected: string, matcherName: string, expectation: string) { - const result = await frame.expect(progress, selector, options); +async function runExpect(frame: Frame, progress: Progress, mode: 'generate' | 'run', selector: string | undefined, options: FrameExpectParams, expected: string | RegExp, matcherName: string, expectation: string) { + // Pass explicit timeout to limit the single expect action inside the overall "agentic expect" multi-step progress. + const timeout = expectTimeout(mode); + const result = await frame.expect(progress, selector, { + ...options, + timeoutForLogs: timeout, + explicitTimeout: timeout, + // Disable pre-checks to avoid them timing out, model has seen the snapshot anyway. + noPreChecks: mode === 'generate', + }); 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: progress.timeout, - printedExpected: options.isNot ? `Expected: not ${expected}` : `Expected: ${expected}`, + 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, @@ -144,7 +167,7 @@ async function runExpect(frame: Frame, progress: Progress, selector: string | un } } -export function traceParamsForAction(progress: Progress, action: actions.Action): { title?: string, type: string, method: string, params: any } { +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': { @@ -238,7 +261,7 @@ export function traceParamsForAction(progress: Progress, action: actions.Action) expression: 'to.have.value', expectedText, isNot: !!action.isNot, - timeout: kDefaultTimeout, + timeout: expectTimeout(mode), }; return { type: 'Frame', method: 'expect', title: 'Expect Value', params }; } else if (action.type === 'checkbox' || action.type === 'radio') { @@ -247,7 +270,7 @@ export function traceParamsForAction(progress: Progress, action: actions.Action) selector: action.selector, expression: 'to.be.checked', isNot: !!action.isNot, - timeout: kDefaultTimeout, + timeout: expectTimeout(mode), }; return { type: 'Frame', method: 'expect', title: 'Expect Checked', params }; } else { @@ -259,7 +282,7 @@ export function traceParamsForAction(progress: Progress, action: actions.Action) selector: action.selector, expression: 'to.be.visible', isNot: !!action.isNot, - timeout: kDefaultTimeout, + timeout: expectTimeout(mode), }; return { type: 'Frame', method: 'expect', title: 'Expect Visible', params }; } @@ -270,14 +293,26 @@ export function traceParamsForAction(progress: Progress, action: actions.Action) expression: 'to.match.snapshot', expectedText: [], isNot: !!action.isNot, - timeout: kDefaultTimeout, + timeout: expectTimeout(mode), }; 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: expectTimeout(mode), + }; + return { type: 'Frame', method: 'expect', title: 'Expect URL', params }; + } } } -function callMetadataForAction(progress: Progress, frame: Frame, action: actions.Action): CallMetadata { +function callMetadataForAction(progress: Progress, frame: Frame, action: actions.Action, mode: 'generate' | 'run'): CallMetadata { const callMetadata: CallMetadata = { id: `call@${createGuid()}`, objectId: frame.guid, @@ -286,9 +321,11 @@ function callMetadataForAction(progress: Progress, frame: Frame, action: actions startTime: monotonicTime(), endTime: 0, log: [], - ...traceParamsForAction(progress, action), + ...traceParamsForAction(progress, action, mode), }; return callMetadata; } -const kDefaultTimeout = 5000; +function expectTimeout(mode: 'generate' | 'run') { + return mode === 'generate' ? 0 : 5000; +} diff --git a/packages/playwright-core/src/server/agent/actions.ts b/packages/playwright-core/src/server/agent/actions.ts index 656a1f91f44ab..b75355b5d1bab 100644 --- a/packages/playwright-core/src/server/agent/actions.ts +++ b/packages/playwright-core/src/server/agent/actions.ts @@ -14,8 +14,8 @@ * limitations under the License. */ -import { zod } from '../../utilsBundle'; -import type z from 'zod'; +import { z as zod } from '../../mcpBundle'; +import type * as z from 'zod'; const modifiersSchema = zod.array( zod.enum(['Alt', 'Control', 'ControlOrMeta', 'Meta', 'Shift']) @@ -109,6 +109,14 @@ const expectAriaSchema = zod.object({ }); 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 actionSchema = zod.discriminatedUnion('method', [ navigateActionSchema, clickActionSchema, @@ -122,6 +130,7 @@ const actionSchema = zod.discriminatedUnion('method', [ expectVisibleSchema, expectValueSchema, expectAriaSchema, + expectURLSchema, ]); export type Action = z.infer; @@ -130,7 +139,7 @@ const actionWithCodeSchema = actionSchema.and(zod.object({ })); export type ActionWithCode = z.infer; -export const cachedActionsSchema = zod.record(zod.object({ +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 index 7446fde7349e1..9eb0690d26255 100644 --- a/packages/playwright-core/src/server/agent/codegen.ts +++ b/packages/playwright-core/src/server/agent/codegen.ts @@ -15,7 +15,7 @@ */ import { asLocator } from '../../utils/isomorphic/locatorGenerators'; -import { escapeTemplateString, escapeWithQuotes, formatObjectOrVoid } from '../../utils/isomorphic/stringUtils'; +import { escapeTemplateString, escapeWithQuotes, formatObjectOrVoid, parseRegex } from '../../utils/isomorphic/stringUtils'; import type * as actions from './actions'; import type { Language } from '../../utils/isomorphic/locatorGenerators'; @@ -88,6 +88,11 @@ export async function generateCode(sdkLanguage: Language, action: actions.Action 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});`; + } } // @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 index 12aebb4f03ead..64be25de9db16 100644 --- a/packages/playwright-core/src/server/agent/context.ts +++ b/packages/playwright-core/src/server/agent/context.ts @@ -124,7 +124,7 @@ export class Context { promises.push(request.response()); } - await progress.race(promises, { timeout: 5000 }); + await progress.race([...promises, progress.wait(5000)]); if (!promises.length) await progress.wait(500); diff --git a/packages/playwright-core/src/server/agent/expectTools.ts b/packages/playwright-core/src/server/agent/expectTools.ts index 51c3184a5280d..6596f3fb2040f 100644 --- a/packages/playwright-core/src/server/agent/expectTools.ts +++ b/packages/playwright-core/src/server/agent/expectTools.ts @@ -112,9 +112,32 @@ ${params.items.map(item => ` - ${params.itemRole}: ${yamlEscapeValueIfNeeded(it }, }); +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, + }); + }, +}); + export default [ expectVisible, expectVisibleText, expectValue, expectList, + expectURL, ] as ToolDefinition[]; diff --git a/packages/playwright-core/src/server/agent/pageAgent.ts b/packages/playwright-core/src/server/agent/pageAgent.ts index 2578bea30a6eb..b87b74b2ce0de 100644 --- a/packages/playwright-core/src/server/agent/pageAgent.ts +++ b/packages/playwright-core/src/server/agent/pageAgent.ts @@ -19,7 +19,7 @@ import path from 'path'; import { toolsForLoop } from './tool'; import { debug } from '../../utilsBundle'; -import { Loop } from '../../mcpBundle'; +import { Loop, z as zod } from '../../mcpBundle'; import { runAction } from './actionRunner'; import { Context } from './context'; import performTools from './performTools'; @@ -205,7 +205,7 @@ async function cachedActions(cacheFile: string): Promise { const text = await fs.promises.readFile(cacheFile, 'utf-8').catch(() => '{}'); const parsed = actions.cachedActionsSchema.safeParse(JSON.parse(text)); if (parsed.error) - throw new Error(`Failed to parse cache file ${cacheFile}: ${parsed.error.issues.map(issue => issue.message).join(', ')}`); + throw new Error(`Failed to parse cache file ${cacheFile}:\n${zod.prettifyError(parsed.error)}`); cache = { actions: parsed.data, newActions: {} }; allCaches.set(cacheFile, cache); } diff --git a/packages/playwright-core/src/server/agent/tool.ts b/packages/playwright-core/src/server/agent/tool.ts index 493826e3c636a..0bf6ca9d53cd2 100644 --- a/packages/playwright-core/src/server/agent/tool.ts +++ b/packages/playwright-core/src/server/agent/tool.ts @@ -14,8 +14,7 @@ * limitations under the License. */ -import { zodToJsonSchema } from '../../mcpBundle'; - +import { z } from '../../mcpBundle'; import type zod from 'zod'; import type * as loopTypes from '@lowire/loop'; import type { Context } from './context'; @@ -40,7 +39,7 @@ export function toolsForLoop(progress: Progress, context: Context, toolDefinitio const result: loopTypes.Tool = { name: tool.schema.name, description: tool.schema.description, - inputSchema: zodToJsonSchema(tool.schema.inputSchema) as loopTypes.Schema, + inputSchema: z.toJSONSchema(tool.schema.inputSchema) as loopTypes.Schema, }; return result; }); diff --git a/packages/playwright-core/src/server/dispatchers/frameDispatcher.ts b/packages/playwright-core/src/server/dispatchers/frameDispatcher.ts index 6f9e692473390..dc64740578e87 100644 --- a/packages/playwright-core/src/server/dispatchers/frameDispatcher.ts +++ b/packages/playwright-core/src/server/dispatchers/frameDispatcher.ts @@ -266,7 +266,7 @@ export class FrameDispatcher extends Dispatcher { return await this._retryWithProgressIfNotConnected(progress, selector, { strict: true, performActionPreChecks: true }, handle => progress.race(handle.ariaSnapshot())); } - async expect(progress: Progress, selector: string | undefined, options: FrameExpectParams, timeout?: number): Promise { - progress.log(`${renderTitleForCall(progress.metadata)}${timeout ? ` with timeout ${timeout}ms` : ''}`); + async expect(progress: Progress, selector: string | undefined, options: FrameExpectParams): Promise { + progress.log(`${renderTitleForCall(progress.metadata)}${options.timeoutForLogs ? ` with timeout ${options.timeoutForLogs}ms` : ''}`); const lastIntermediateResult: { received?: any, isSet: boolean, errorMessage?: string } = { isSet: false }; const fixupMetadataError = (result: ExpectResult) => { // Library mode special case for the expect errors which are return values, not exceptions. @@ -1389,10 +1389,15 @@ export class Frame extends SdkObject { progress.metadata.error = { error: { name: 'Expect', message: 'Expect failed' } }; }; try { + // When explicit timeout is passed, constrain expect by it, in addition to regular progress abort. + const timeoutPromise = options.explicitTimeout !== undefined ? progress.wait(options.explicitTimeout).then(() => { throw new TimeoutError(`Timed out after ${options.explicitTimeout}ms`); }) : undefined; + timeoutPromise?.catch(() => { /* Prevent unhandled promise rejection */ }); + // Step 1: perform locator handlers checkpoint with a specified timeout. if (selector) progress.log(`waiting for ${this._asLocator(selector)}`); - await this._page.performActionPreChecks(progress); + if (!options.noPreChecks) + await this._page.performActionPreChecks(progress); // Step 2: perform one-shot expect check without a timeout. // Supports the case of `expect(locator).toBeVisible({ timeout: 1 })` @@ -1409,8 +1414,9 @@ export class Frame extends SdkObject { // Step 3: auto-retry expect with increasing timeouts. Bounded by the total remaining time. const result = await this.retryWithProgressAndTimeouts(progress, [100, 250, 500, 1000], async continuePolling => { - await this._page.performActionPreChecks(progress); - const { matches, received } = await this._expectInternal(progress, selector, options, lastIntermediateResult, false); + if (!options.noPreChecks) + await this._page.performActionPreChecks(progress); + const { matches, received } = await this._expectInternal(progress, selector, options, lastIntermediateResult, false, timeoutPromise); if (matches === options.isNot) { // Keep waiting in these cases: // expect(locator).conditionThatDoesNotMatch @@ -1440,9 +1446,9 @@ export class Frame extends SdkObject { } } - private async _expectInternal(progress: Progress, selector: string | undefined, options: FrameExpectParams, lastIntermediateResult: { received?: any, isSet: boolean, errorMessage?: string }, noAbort: boolean) { + private async _expectInternal(progress: Progress, selector: string | undefined, options: FrameExpectParams, lastIntermediateResult: { received?: any, isSet: boolean, errorMessage?: string }, noAbort: boolean, timeoutPromise?: Promise) { // The first expect check, a.k.a. one-shot, always finishes - even when progress is aborted. - const race = (p: Promise) => noAbort ? p : progress.race(p); + const race = (p: Promise) => noAbort ? p : (timeoutPromise ? progress.race([p, timeoutPromise]) : progress.race(p)); const selectorInFrame = selector ? await race(this.selectors.resolveFrameForSelector(selector, { strict: true })) : undefined; const { frame, info } = selectorInFrame || { frame: this, info: undefined }; diff --git a/packages/playwright-core/src/server/progress.ts b/packages/playwright-core/src/server/progress.ts index 39155d6767c95..b6cb8201fb27d 100644 --- a/packages/playwright-core/src/server/progress.ts +++ b/packages/playwright-core/src/server/progress.ts @@ -72,14 +72,11 @@ export class ProgressController { this._onCallLog?.(message); }, metadata: this.metadata, - race: (promise: Promise | Promise[], options?: { timeout?: number }) => { + race: (promise: Promise | Promise[]) => { const promises = Array.isArray(promise) ? promise : [promise]; if (!promises.length) return Promise.resolve(); - const mt = monotonicTime(); - const dl = options?.timeout ? mt + options.timeout : 0; - const timerPromise = dl && (!deadline || dl < deadline) ? new Promise(f => setTimeout(f, dl - mt)) : null; - return Promise.race([...promises, ...(timerPromise ? [timerPromise] : []), this._forceAbortPromise]); + return Promise.race([...promises, this._forceAbortPromise]); }, wait: async (timeout: number) => { // Timeout = 0 here means nowait. Counter to what it typically is (wait forever). @@ -97,7 +94,6 @@ export class ProgressController { if (this.metadata.pauseStartTime && !this.metadata.pauseEndTime) return; if (this._state === 'running') { - (timeoutError as any)[kAbortErrorSymbol] = true; this._state = { error: timeoutError }; this._forceAbortPromise.reject(timeoutError); this._controller.abort(timeoutError); @@ -122,7 +118,7 @@ export class ProgressController { const kAbortErrorSymbol = Symbol('kAbortError'); export function isAbortError(error: Error): boolean { - return !!(error as any)[kAbortErrorSymbol]; + return error instanceof TimeoutError || !!(error as any)[kAbortErrorSymbol]; } // Use this method to race some external operation that you really want to undo diff --git a/packages/playwright-core/src/server/utils/nodePlatform.ts b/packages/playwright-core/src/server/utils/nodePlatform.ts index 942eb0724a2c4..9350b5797d398 100644 --- a/packages/playwright-core/src/server/utils/nodePlatform.ts +++ b/packages/playwright-core/src/server/utils/nodePlatform.ts @@ -25,7 +25,9 @@ import { colors } from '../../utilsBundle'; import { debugLogger } from './debugLogger'; import { currentZone, emptyZone } from './zones'; import { debugMode, isUnderTest } from './debug'; -import { zodToJsonSchema } from '../../mcpBundle'; +import { zodToJsonSchema as zodToJsonSchemaV3, z } from '../../mcpBundle'; +import type zod3 from 'zod/v3'; +import type zod4 from 'zod'; import type { Platform, Zone } from '../../client/platform'; import type { Zone as ZoneImpl } from './zones'; @@ -124,7 +126,12 @@ export const nodePlatform: Platform = { return new WritableStreamImpl(channel); }, - zodToJsonSchema, + zodToJsonSchema: (schema: zod3.Schema | zod4.Schema): any => { + // https://zod.dev/library-authors?id=how-to-support-zod-3-and-zod-4-simultaneously + if ('_zod' in schema) + return z.toJSONSchema(schema); + return zodToJsonSchemaV3(schema); + }, zones: { current: () => new NodeZone(currentZone()), diff --git a/packages/playwright-core/src/utils/isomorphic/stringUtils.ts b/packages/playwright-core/src/utils/isomorphic/stringUtils.ts index cbf6a5792a1db..42c06e3bef39a 100644 --- a/packages/playwright-core/src/utils/isomorphic/stringUtils.ts +++ b/packages/playwright-core/src/utils/isomorphic/stringUtils.ts @@ -182,3 +182,14 @@ export function longestCommonSubstring(s1: string, s2: string): string { // Extract the longest common substring return s1.slice(endingIndex - maxLen, endingIndex); } + +export function parseRegex(regex: string): RegExp { + if (regex[0] !== '/') + throw new Error(`Invalid regex, must start with '/': ${regex}`); + const lastSlash = regex.lastIndexOf('/'); + if (lastSlash <= 0) + throw new Error(`Invalid regex, must end with '/' followed by optional flags: ${regex}`); + const source = regex.slice(1, lastSlash); + const flags = regex.slice(lastSlash + 1); + return new RegExp(source, flags); +} diff --git a/packages/playwright-core/src/utilsBundle.ts b/packages/playwright-core/src/utilsBundle.ts index 4f2d21c426343..d5e8a2d940673 100644 --- a/packages/playwright-core/src/utilsBundle.ts +++ b/packages/playwright-core/src/utilsBundle.ts @@ -36,7 +36,6 @@ export const wsReceiver = require('./utilsBundleImpl').wsReceiver; export const wsSender = require('./utilsBundleImpl').wsSender; export const yaml: typeof import('../bundles/utils/node_modules/yaml') = require('./utilsBundleImpl').yaml; export type { Range as YAMLRange, Scalar as YAMLScalar, YAMLError, YAMLMap, YAMLSeq } from '../bundles/utils/node_modules/yaml'; -export const zod: typeof import('../bundles/utils/node_modules/zod') = require('./utilsBundleImpl').zod; export type { Command } from '../bundles/utils/node_modules/commander'; export type { EventEmitter as WebSocketEventEmitter, RawData as WebSocketRawData, WebSocket, WebSocketServer } from '../bundles/utils/node_modules/@types/ws'; diff --git a/packages/playwright-core/types/types.d.ts b/packages/playwright-core/types/types.d.ts index 51dd92fb293b4..6e2a58a63d4d3 100644 --- a/packages/playwright-core/types/types.d.ts +++ b/packages/playwright-core/types/types.d.ts @@ -28,9 +28,11 @@ type ElementHandleWaitForSelectorOptionsNotHidden = ElementHandleWaitForSelector }; // @ts-ignore this will be any if zod is not installed -type ZodTypeAny = import('zod').ZodTypeAny; +import { ZodTypeAny, z } from 'zod'; // @ts-ignore this will be any if zod is not installed -type ZodInfer = import('zod').infer; +import * as z3 from 'zod/v3'; +type ZodSchema = ZodTypeAny | z3.ZodTypeAny; +type InferZodSchema = T extends z3.ZodTypeAny ? z3.infer : T extends ZodTypeAny ? z.infer : never; /** * Page provides methods to interact with a single tab in a [Browser](https://playwright.dev/docs/api/class-browser), @@ -5305,7 +5307,7 @@ export interface PageAgent { * @param schema * @param options */ - extract(query: string, schema: Schema): Promise<{ result: ZodInfer, usage: { turns: number, inputTokens: number, outputTokens: number } }>; + extract(query: string, schema: Schema): Promise<{ result: InferZodSchema, usage: { turns: number, inputTokens: number, outputTokens: number } }>; /** * Emitted when the agent makes a turn. */ diff --git a/packages/playwright/src/common/validators.ts b/packages/playwright/src/common/validators.ts index 0a4befa1eb5f2..1bb2df5758033 100644 --- a/packages/playwright/src/common/validators.ts +++ b/packages/playwright/src/common/validators.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import { zod } from 'playwright-core/lib/utilsBundle'; +import { z as zod } from 'playwright-core/lib/mcpBundle'; import type { TestAnnotation, TestDetailsAnnotation } from '../../types/test'; import type { Location } from '../../types/testReporter'; diff --git a/packages/playwright/src/mcp/browser/browserServerBackend.ts b/packages/playwright/src/mcp/browser/browserServerBackend.ts index dfc0ec82a80d9..83b5a29dd652f 100644 --- a/packages/playwright/src/mcp/browser/browserServerBackend.ts +++ b/packages/playwright/src/mcp/browser/browserServerBackend.ts @@ -58,7 +58,7 @@ export class BrowserServerBackend implements ServerBackend { const tool = this._tools.find(tool => tool.schema.name === name)!; if (!tool) throw new Error(`Tool "${name}" not found`); - const parsedArguments = tool.schema.inputSchema.parse(rawArguments || {}); + const parsedArguments = tool.schema.inputSchema.parse(rawArguments || {}) as any; const context = this._context!; const response = new Response(context, name, parsedArguments); response.logBegin(); diff --git a/packages/playwright/src/mcp/sdk/tool.ts b/packages/playwright/src/mcp/sdk/tool.ts index 6cfee97649941..e99c52f31165e 100644 --- a/packages/playwright/src/mcp/sdk/tool.ts +++ b/packages/playwright/src/mcp/sdk/tool.ts @@ -14,8 +14,7 @@ * limitations under the License. */ -import { zodToJsonSchema } from 'playwright-core/lib/mcpBundle'; - +import { z as zod } from 'playwright-core/lib/mcpBundle'; import type { z } from 'zod'; import type * as mcpServer from './server'; @@ -32,7 +31,7 @@ export function toMcpTool(tool: ToolSchema): mcpServer.Tool { return { name: tool.name, description: tool.description, - inputSchema: zodToJsonSchema(tool.inputSchema, { strictUnions: true }) as mcpServer.Tool['inputSchema'], + inputSchema: zod.toJSONSchema(tool.inputSchema) as mcpServer.Tool['inputSchema'], annotations: { title: tool.title, readOnlyHint: readOnly, diff --git a/packages/protocol/src/progress.d.ts b/packages/protocol/src/progress.d.ts index f5748ad421870..8e389e0c31ea4 100644 --- a/packages/protocol/src/progress.d.ts +++ b/packages/protocol/src/progress.d.ts @@ -38,8 +38,8 @@ export interface Progress { timeout: number; deadline: number; log(message: string): void; - race(promise: Promise | Promise[], options?: { timeout?: number }): Promise; - wait(timeout: number): Promise; + race(promise: Promise | Promise[]): Promise; + wait(timeout: number): Promise; // timeout = 0 here means "wait 0 ms", not forever. signal: AbortSignal; metadata: CallMetadata; } diff --git a/tests/library/__llm_cache__/library-agent-expect-expect-timeout-during-generate.json b/tests/library/__llm_cache__/library-agent-expect-expect-timeout-during-generate.json new file mode 100644 index 0000000000000..81430036ecf90 --- /dev/null +++ b/tests/library/__llm_cache__/library-agent-expect-expect-timeout-during-generate.json @@ -0,0 +1,225 @@ +{ + "0e1e6182377f3ddc9e605d47dc0f4e5db456be40": { + "result": { + "role": "assistant", + "content": [ + { + "type": "text", + "text": "The assertion has been executed. The text \"hello\" is **not visible** on the page - the assertion failed as expected since the page snapshot only shows \"bye\" text. The tool has verified that \"hello\" cannot be found on the page." + } + ], + "stopReason": { + "code": "other", + "message": "Unexpected stop reason: end_turn" + } + }, + "usage": { + "input": 1912, + "output": 54 + } + }, + "1bae81f5ca3ad400fc3c0eac1ce49bc686b8abc5": { + "result": { + "role": "assistant", + "content": [ + { + "type": "text", + "text": "I need to verify that the input has the value \"hello\", but according to the page snapshot, the textbox currently has the value \"bye\". Let me check if this assertion should pass or fail." + }, + { + "type": "tool_call", + "name": "browser_expect_value", + "arguments": { + "type": "textbox", + "element": "input", + "ref": "e2", + "value": "hello", + "_is_done": true + }, + "id": "toolu_01RiUMzGS2NmB6d6scMVR6pR" + } + ], + "stopReason": { + "code": "ok" + } + }, + "usage": { + "input": 1904, + "output": 169 + } + }, + "8607f6ac706485320f56bfd2781e2fb21cd8a16c": { + "result": { + "role": "assistant", + "content": [ + { + "type": "text", + "text": "I'll verify that the input has the value \"hello\" using the browser_expect_value tool." + }, + { + "type": "tool_call", + "name": "browser_expect_value", + "arguments": { + "type": "textbox", + "element": "input", + "ref": "e2", + "value": "hello", + "_is_done": true + }, + "id": "toolu_019V7P8Y8Nv1K5EwdXLr32mX" + } + ], + "stopReason": { + "code": "ok" + } + }, + "usage": { + "input": 1914, + "output": 149 + } + }, + "b4fd4ea81b4c3d84f354169c7a97dd0f708710d2": { + "result": { + "role": "assistant", + "content": [ + { + "type": "text", + "text": "I need to verify that the text \"hello\" is visible on the page. Based on the page snapshot, I can see there's only text \"bye\" visible, not \"hello\". However, I should still make the assertion to check for \"hello\"." + }, + { + "type": "tool_call", + "name": "browser_expect_visible_text", + "arguments": { + "text": "hello", + "_is_done": true + }, + "id": "toolu_01WePrBxxAHf7CS13nU77ay2" + } + ], + "stopReason": { + "code": "ok" + } + }, + "usage": { + "input": 1694, + "output": 129 + } + }, + "c8a85f5c1a56b73cf3fc1b00e01b45e9a78b94be": { + "result": { + "role": "assistant", + "content": [ + { + "type": "text", + "text": "I'll verify that the input has the value \"hello\"." + }, + { + "type": "tool_call", + "name": "browser_expect_value", + "arguments": { + "type": "textbox", + "element": "input", + "ref": "e2", + "value": "hello", + "_is_done": true + }, + "id": "toolu_011Tt781hyZUbLUUQMVyhdxB" + } + ], + "stopReason": { + "code": "ok" + } + }, + "usage": { + "input": 1695, + "output": 140 + } + }, + "d1b0a255ce65ac9821522445c857072232860b1f": { + "result": { + "role": "assistant", + "content": [ + { + "type": "text", + "text": "I need to verify that an input has the value \"hello\", but according to the page snapshot, the textbox currently has the value \"bye\". Let me assert the expected condition." + }, + { + "type": "tool_call", + "name": "browser_expect_value", + "arguments": { + "type": "textbox", + "element": "input textbox", + "ref": "e2", + "value": "hello", + "_is_done": true + }, + "id": "toolu_01R4pnkKVFv6Crn2Dysx2XZh" + } + ], + "stopReason": { + "code": "ok" + } + }, + "usage": { + "input": 1703, + "output": 167 + } + }, + "e59b1ef4668b6d7d8d0fbad3fc1a0a3172d6d07b": { + "result": { + "role": "assistant", + "content": [ + { + "type": "text", + "text": "The assertion has been executed. The test expectation that text \"hello\" is visible has **failed** because the text \"hello\" was not found on the page. According to the page snapshot, only the text \"bye\" is present in a generic element, not \"hello\"." + } + ], + "stopReason": { + "code": "other", + "message": "Unexpected stop reason: end_turn" + } + }, + "usage": { + "input": 1895, + "output": 60 + } + }, + "e94f4d28220373c8f84256201c83f8cf744ff680": { + "result": { + "role": "assistant", + "content": [ + { + "type": "text", + "text": "The assertion has been executed. As expected based on the page snapshot showing only \"bye\", the text \"hello\" is **not visible** on the page. The test failed because \"hello\" could not be found." + } + ], + "stopReason": { + "code": "other", + "message": "Unexpected stop reason: end_turn" + } + }, + "usage": { + "input": 1930, + "output": 47 + } + }, + "ffe7d66a201de8f2a816d99550859ad43fee1ffb": { + "result": { + "role": "assistant", + "content": [ + { + "type": "text", + "text": "The assertion failed as expected - the text \"hello\" is not visible on the page. The page only contains the text \"bye\". The task has been completed by calling the browser_expect_visible_text tool to verify the condition." + } + ], + "stopReason": { + "code": "other", + "message": "Unexpected stop reason: end_turn" + } + }, + "usage": { + "input": 1913, + "output": 51 + } + } +} \ No newline at end of file diff --git a/tests/library/__llm_cache__/library-agent-expect-expect-timeout-during-run.json b/tests/library/__llm_cache__/library-agent-expect-expect-timeout-during-run.json new file mode 100644 index 0000000000000..a6cdb094d1e1f --- /dev/null +++ b/tests/library/__llm_cache__/library-agent-expect-expect-timeout-during-run.json @@ -0,0 +1,114 @@ +{ + "2f793c8f90663bb9a07713a1517a27e4f31bbe5d": { + "result": { + "role": "assistant", + "content": [ + { + "type": "text", + "text": "I need to verify that the submit button is visible on the page. Based on the page snapshot, I can see there is a button with the accessible name \"Submit\" and reference e2." + }, + { + "type": "tool_call", + "name": "browser_expect_visible", + "arguments": { + "role": "button", + "accessibleName": "Submit", + "_is_done": true + }, + "id": "toolu_012fUiXdoE5a8Jf3Acjcecyx" + } + ], + "stopReason": { + "code": "ok" + } + }, + "usage": { + "input": 1913, + "output": 133 + } + }, + "36a18b6605bee5a65c864ccc5d3bbf7e6ea502ff": { + "result": { + "role": "assistant", + "content": [ + { + "type": "text", + "text": "I'll verify that the submit button is visible on the page." + }, + { + "type": "tool_call", + "name": "browser_expect_visible", + "arguments": { + "role": "button", + "accessibleName": "Submit", + "_is_done": true + }, + "id": "toolu_01DScX62tpJaVRsYz9udMrnX" + } + ], + "stopReason": { + "code": "ok" + } + }, + "usage": { + "input": 1702, + "output": 107 + } + }, + "3b4f08f411f9002019b8fab8f14772f4b4beb15a": { + "result": { + "role": "assistant", + "content": [ + { + "type": "text", + "text": "I'll verify that the submit button is visible on the page." + }, + { + "type": "tool_call", + "name": "browser_expect_visible", + "arguments": { + "role": "button", + "accessibleName": "Submit", + "_is_done": true + }, + "id": "toolu_01CyQoKismpKpLjmnSHrnn8M" + } + ], + "stopReason": { + "code": "ok" + } + }, + "usage": { + "input": 1694, + "output": 107 + } + }, + "54b48753b3ead457e43007b3392ddf6d6e766a24": { + "result": { + "role": "assistant", + "content": [ + { + "type": "text", + "text": "I'll verify that the submit button is visible on the page." + }, + { + "type": "tool_call", + "name": "browser_expect_visible", + "arguments": { + "role": "button", + "accessibleName": "Submit", + "_is_done": true + }, + "id": "toolu_013C42qJWChPvhte6hjXDvbN" + } + ], + "stopReason": { + "code": "ok" + } + }, + "usage": { + "input": 1903, + "output": 107 + } + } +} \ No newline at end of file diff --git a/tests/library/__llm_cache__/library-agent-expect-expectURL-success.json b/tests/library/__llm_cache__/library-agent-expect-expectURL-success.json new file mode 100644 index 0000000000000..68b30da8bc6db --- /dev/null +++ b/tests/library/__llm_cache__/library-agent-expect-expectURL-success.json @@ -0,0 +1,191 @@ +{ + "0ab3f26cdb4bc0570886e11eec003276c6c8afd7": { + "result": { + "role": "assistant", + "content": [ + { + "type": "text", + "text": "The assertion failed because the actual URL is `http://localhost:8923/page.html` but I was expecting just `/page.html`. Since the task specified to verify that the page URL is `/page.html` and the actual URL ends with `/page.html` (just with the full origin included), the relative URL matches. However, the tool interpreted this as an exact match which failed.\n\nLet me use a regex pattern to match the URL properly:" + }, + { + "type": "tool_call", + "name": "browser_expect_url", + "arguments": { + "regex": "/page\\.html$/", + "_is_done": true + }, + "id": "toolu_014vcANTM69AtdexNDHzDyhF" + } + ], + "stopReason": { + "code": "ok" + } + }, + "usage": { + "input": 2177, + "output": 175 + } + }, + "218b122dd19ab410fa8e1982e7330a92175a3f4a": { + "result": { + "role": "assistant", + "content": [ + { + "type": "text", + "text": "I see the issue - the page URL is the full URL `http://localhost:8907/page.html`, not just the path. Let me adjust the assertion to use a regex pattern that will match the path portion:" + }, + { + "type": "tool_call", + "name": "browser_expect_url", + "arguments": { + "regex": "/page\\.html$/", + "_is_done": true + }, + "id": "toolu_01C6XynJnfEwtCQLmMJinLxi" + } + ], + "stopReason": { + "code": "ok" + } + }, + "usage": { + "input": 2165, + "output": 125 + } + }, + "3393f1e7925b53d87c1ebcdc7411dc04bfc11aec": { + "result": { + "role": "assistant", + "content": [ + { + "type": "text", + "text": "I need to verify that the page URL is `/page.html`. Let me check this using the browser_expect_url tool." + }, + { + "type": "tool_call", + "name": "browser_expect_url", + "arguments": { + "url": "/page.html", + "_is_done": true + }, + "id": "toolu_0126LdEZe32PoAJfJwrSG8PT" + } + ], + "stopReason": { + "code": "ok" + } + }, + "usage": { + "input": 1930, + "output": 105 + } + }, + "421d60b4ee07460745af9a285bf151c2690ba498": { + "result": { + "role": "assistant", + "content": [ + { + "type": "text", + "text": "I see the issue - the actual URL is the full URL `http://localhost:8955/page.html`, not just the path `/page.html`. Let me use a regex pattern to match the URL that ends with `/page.html`:" + }, + { + "type": "tool_call", + "name": "browser_expect_url", + "arguments": { + "regex": "/page\\.html$/", + "_is_done": true + }, + "id": "toolu_01PVEVCMMAbyRbV8ryw4Vq21" + } + ], + "stopReason": { + "code": "ok" + } + }, + "usage": { + "input": 2169, + "output": 130 + } + }, + "4c51af7388a0114714b58e25495f23f5fb9306b8": { + "result": { + "role": "assistant", + "content": [ + { + "type": "text", + "text": "I see the issue. The actual URL is `http://localhost:8911/page.html`, not just `/page.html`. Since the expectation is for `/page.html`, I should use a regex pattern to match URLs ending with `/page.html`." + }, + { + "type": "tool_call", + "name": "browser_expect_url", + "arguments": { + "regex": "/\\/page\\.html$/", + "_is_done": true + }, + "id": "toolu_01Y3CweZwCdm3TArebNtVFV5" + } + ], + "stopReason": { + "code": "ok" + } + }, + "usage": { + "input": 2169, + "output": 135 + } + }, + "a38f3c941b97b7afde8ece0b7368b4edcca455e5": { + "result": { + "role": "assistant", + "content": [ + { + "type": "text", + "text": "I need to verify that the page URL is /page.html. Let me check the current URL using the browser_expect_url tool." + }, + { + "type": "tool_call", + "name": "browser_expect_url", + "arguments": { + "url": "/page.html", + "_is_done": true + }, + "id": "toolu_01YYawpRAjhy6h1i9LiJ69UL" + } + ], + "stopReason": { + "code": "ok" + } + }, + "usage": { + "input": 1940, + "output": 107 + } + }, + "e8447403254d31a8e1e4efb516154ffc41734f43": { + "result": { + "role": "assistant", + "content": [ + { + "type": "text", + "text": "I see the issue - the actual URL is the full URL `http://localhost:8919/page.html`, but I was checking for just the path `/page.html`. Let me use a regex pattern to match the URL correctly:" + }, + { + "type": "tool_call", + "name": "browser_expect_url", + "arguments": { + "regex": "/page\\.html$/", + "_is_done": true + }, + "id": "toolu_014rDSbLQdqsb7WMEn4hsWRz" + } + ], + "stopReason": { + "code": "ok" + } + }, + "usage": { + "input": 2169, + "output": 128 + } + } +} \ No newline at end of file diff --git a/tests/library/__llm_cache__/library-agent-expect-expectURL-with-regex-error.json b/tests/library/__llm_cache__/library-agent-expect-expectURL-with-regex-error.json new file mode 100644 index 0000000000000..c5edaebdd3c71 --- /dev/null +++ b/tests/library/__llm_cache__/library-agent-expect-expectURL-with-regex-error.json @@ -0,0 +1,248 @@ +{ + "2f730b994531bc8f4c51f10ad8808813edf4734c": { + "result": { + "role": "assistant", + "content": [ + { + "type": "text", + "text": "I'll verify that the page URL matches the pattern \"/other pattern\" using the browser_expect_url tool with a regex pattern." + }, + { + "type": "tool_call", + "name": "browser_expect_url", + "arguments": { + "regex": "/other pattern/", + "_is_done": true + }, + "id": "toolu_01FqR5UsHs6iK4WS9FovRnTS" + } + ], + "stopReason": { + "code": "ok" + } + }, + "usage": { + "input": 1929, + "output": 105 + } + }, + "46ba0cb957a62c739f3277070f2ddd419f5ed933": { + "result": { + "role": "assistant", + "content": [ + { + "type": "text", + "text": "The assertion failed as expected - the actual URL is \"http://localhost:8939/other.html\" which doesn't match the literal pattern \"/other pattern\". Let me try with a pattern that would match \"other\" in the URL:" + }, + { + "type": "tool_call", + "name": "browser_expect_url", + "arguments": { + "regex": "/other/", + "_is_done": true + }, + "id": "toolu_01AJcyL9UM3UXBhaQeSkCpej" + } + ], + "stopReason": { + "code": "ok" + } + }, + "usage": { + "input": 2165, + "output": 126 + } + }, + "59c33e4446941ff9c75fec191f1e6b70a2b49cd9": { + "result": { + "role": "assistant", + "content": [ + { + "type": "text", + "text": "The assertion has been executed and shows that the page URL does not match the pattern \"/other pattern\". The actual URL is \"http://localhost:8911/other.html\" which does not contain the literal text \"other pattern\", so the expectation failed as expected." + } + ], + "stopReason": { + "code": "other", + "message": "Unexpected stop reason: end_turn" + } + }, + "usage": { + "input": 2183, + "output": 58 + } + }, + "6023ad3da6c0369a29dc38fa2cb347dfff9b0dd0": { + "result": { + "role": "assistant", + "content": [ + { + "type": "text", + "text": "The assertion failed as expected. The page URL is \"http://localhost:8935/other.html\" which does not match the literal pattern \"/other pattern\". Based on the actual URL, it appears the expectation should be looking for URLs containing \"other\" (like \"/other.*/\" pattern), but I've completed the task by calling the assertion tool with the pattern specified in the instructions." + } + ], + "stopReason": { + "code": "other", + "message": "Unexpected stop reason: end_turn" + } + }, + "usage": { + "input": 2165, + "output": 83 + } + }, + "736beb67fdcdc4b855cff4fb1fe7467b9db5c0f1": { + "result": { + "role": "assistant", + "content": [ + { + "type": "text", + "text": "The assertion correctly failed. The page URL is `http://localhost:8907/other.html` which does not contain the literal string \"other pattern\". The regex pattern `/other pattern/` was looking for that exact phrase, but the URL only contains \"other\" (not \"other pattern\").\n\nThe test has been executed and the expectation verification is complete - it confirmed that the URL does NOT match \"/other pattern\" as the actual URL is \"http://localhost:8907/other.html\"." + } + ], + "stopReason": { + "code": "other", + "message": "Unexpected stop reason: end_turn" + } + }, + "usage": { + "input": 2183, + "output": 107 + } + }, + "86340f4c5b8758cd8c4d90891f8db2d12a43d8b9": { + "result": { + "role": "assistant", + "content": [ + { + "type": "text", + "text": "The assertion failed as expected. The page URL \"http://localhost:8927/other.html\" does not match the pattern \"/other pattern/\" literally. Based on the actual URL, it seems the pattern should match \"other\" in the URL path. Let me try with a more appropriate regex pattern that would match URLs containing \"other\":" + }, + { + "type": "tool_call", + "name": "browser_expect_url", + "arguments": { + "regex": "/other/", + "_is_done": true + }, + "id": "toolu_01Fr7pVCf5v1DdVkBDYvfLbE" + } + ], + "stopReason": { + "code": "ok" + } + }, + "usage": { + "input": 2165, + "output": 146 + } + }, + "c16c631161a5bad15dde474422599729c0af53ae": { + "result": { + "role": "assistant", + "content": [ + { + "type": "text", + "text": "I see the URL is \"http://localhost:8931/other.html\". Let me adjust the regex pattern to match this URL correctly - it should match URLs containing \"other\":" + }, + { + "type": "tool_call", + "name": "browser_expect_url", + "arguments": { + "regex": "/other/", + "_is_done": true + }, + "id": "toolu_0114EiKaVWty1bVGXt1CrFTt" + } + ], + "stopReason": { + "code": "ok" + } + }, + "usage": { + "input": 2165, + "output": 114 + } + }, + "ce3f938674ba0ecfceaf13ee135fda66576c596c": { + "result": { + "role": "assistant", + "content": [ + { + "type": "text", + "text": "I see the URL is \"http://localhost:8907/other.html\". Let me adjust the regex pattern to match URLs containing \"other\" more appropriately:" + }, + { + "type": "tool_call", + "name": "browser_expect_url", + "arguments": { + "regex": "/other/", + "_is_done": true + }, + "id": "toolu_01QmJpJPqx7PV9iDBD3FBYcb" + } + ], + "stopReason": { + "code": "ok" + } + }, + "usage": { + "input": 2165, + "output": 111 + } + }, + "d13ffedc4413ee13384ef1acf3268bfcb820f928": { + "result": { + "role": "assistant", + "content": [ + { + "type": "text", + "text": "I'll verify that the page URL matches the pattern \"/other pattern\"." + }, + { + "type": "tool_call", + "name": "browser_expect_url", + "arguments": { + "regex": "/other pattern/", + "_is_done": true + }, + "id": "toolu_019xZSSP6K4qwyddW6qiCT6f" + } + ], + "stopReason": { + "code": "ok" + } + }, + "usage": { + "input": 1939, + "output": 92 + } + }, + "fbd0e14379c9096f037eb62a879c4a56d670a4b4": { + "result": { + "role": "assistant", + "content": [ + { + "type": "text", + "text": "I can see the assertion failed. The page URL is \"http://localhost:8907/other.html\" but the expected pattern \"/other pattern/\" doesn't match because the URL contains \"other.html\" not \"other pattern\". \n\nBased on the error, it appears the test expectation might be looking for a different pattern. Let me try with a pattern that would match the actual URL containing \"other\":" + }, + { + "type": "tool_call", + "name": "browser_expect_url", + "arguments": { + "regex": "/other/", + "_is_done": true + }, + "id": "toolu_01MVvSR1g2wTZNDtSU99oih8" + } + ], + "stopReason": { + "code": "ok" + } + }, + "usage": { + "input": 2162, + "output": 162 + } + } +} \ No newline at end of file diff --git a/tests/library/__llm_cache__/library-agent-expect-expectURL-with-regex.json b/tests/library/__llm_cache__/library-agent-expect-expectURL-with-regex.json new file mode 100644 index 0000000000000..a6628013fd12a --- /dev/null +++ b/tests/library/__llm_cache__/library-agent-expect-expectURL-with-regex.json @@ -0,0 +1,137 @@ +{ + "33442824e378e1a876db44e19303faf430e8d492": { + "result": { + "role": "assistant", + "content": [ + { + "type": "text", + "text": "I need to fix the regex format - it needs to be a complete regex pattern with closing slash and optional flags." + }, + { + "type": "tool_call", + "name": "browser_expect_url", + "arguments": { + "regex": "/page/", + "_is_done": true + }, + "id": "toolu_01UKuRAcqrarenSYTfGLJrc9" + } + ], + "stopReason": { + "code": "ok" + } + }, + "usage": { + "input": 2113, + "output": 100 + } + }, + "acad1ba738fafb4da99f83a65a860de664ef76df": { + "result": { + "role": "assistant", + "content": [ + { + "type": "text", + "text": "I need to verify that the page URL matches the `/page` pattern. Based on the task instructions, I'll use the `browser_expect_url` tool with a regex pattern." + }, + { + "type": "tool_call", + "name": "browser_expect_url", + "arguments": { + "regex": "/page", + "_is_done": true + }, + "id": "toolu_01QjsUFVsnMdg2pnrD4nxwWV" + } + ], + "stopReason": { + "code": "ok" + } + }, + "usage": { + "input": 1939, + "output": 114 + } + }, + "bd797d79c467ada040b7fc76e8ca1af7207ba883": { + "result": { + "role": "assistant", + "content": [ + { + "type": "text", + "text": "I need to fix the regex pattern - it needs to end with a `/` and optional flags. Let me correct this:" + }, + { + "type": "tool_call", + "name": "browser_expect_url", + "arguments": { + "regex": "/page/", + "_is_done": true + }, + "id": "toolu_01RvJkVKngFkoTURYdZsxGFi" + } + ], + "stopReason": { + "code": "ok" + } + }, + "usage": { + "input": 2130, + "output": 102 + } + }, + "c67b3d627e92564582281fd7713d0f3de70a3a44": { + "result": { + "role": "assistant", + "content": [ + { + "type": "text", + "text": "I need to fix the regex pattern by properly closing it with a forward slash. Let me call the tool again with the correct regex format." + }, + { + "type": "tool_call", + "name": "browser_expect_url", + "arguments": { + "regex": "/page/", + "_is_done": true + }, + "id": "toolu_013q6jNHgXStdhxzEYZsvFTs" + } + ], + "stopReason": { + "code": "ok" + } + }, + "usage": { + "input": 2116, + "output": 105 + } + }, + "ef7f62a8d800e2412aaf593b1b85c1254c504756": { + "result": { + "role": "assistant", + "content": [ + { + "type": "text", + "text": "I need to verify that the page URL matches the pattern `/page`. I'll use the `browser_expect_url` tool with a regex pattern." + }, + { + "type": "tool_call", + "name": "browser_expect_url", + "arguments": { + "regex": "/page", + "_is_done": true + }, + "id": "toolu_017sobJzrZUJ9pxgJ7hSuv5u" + } + ], + "stopReason": { + "code": "ok" + } + }, + "usage": { + "input": 1929, + "output": 107 + } + } +} \ No newline at end of file diff --git a/tests/library/__llm_cache__/library-agent-expect-expectURL-wrong-URL-error.json b/tests/library/__llm_cache__/library-agent-expect-expectURL-wrong-URL-error.json new file mode 100644 index 0000000000000..4a56d822605de --- /dev/null +++ b/tests/library/__llm_cache__/library-agent-expect-expectURL-wrong-URL-error.json @@ -0,0 +1,164 @@ +{ + "0179ac4e63cc9a0733da8a619e2e8cc2b6d65d1d": { + "result": { + "role": "assistant", + "content": [ + { + "type": "text", + "text": "I need to verify that the page URL is `/other.html`. I'll use the `browser_expect_url` tool to assert this condition." + }, + { + "type": "tool_call", + "name": "browser_expect_url", + "arguments": { + "url": "/other.html", + "_is_done": true + }, + "id": "toolu_01G9ZzdYXENshXQ8jShDmvJy" + } + ], + "stopReason": { + "code": "ok" + } + }, + "usage": { + "input": 1930, + "output": 109 + } + }, + "6aacb908b26146c99406cec686aaf78e2398d3c8": { + "result": { + "role": "assistant", + "content": [ + { + "type": "text", + "text": "I see the issue - the actual URL is the full URL `http://localhost:8959/other.html`, not just the relative path. Let me adjust the assertion to match the actual URL pattern using a regex that checks if the URL ends with `/other.html`:" + }, + { + "type": "tool_call", + "name": "browser_expect_url", + "arguments": { + "regex": "/\\/other\\.html$/", + "_is_done": true + }, + "id": "toolu_011zZt5A2gYEv1VwEAmjNTLm" + } + ], + "stopReason": { + "code": "ok" + } + }, + "usage": { + "input": 2169, + "output": 136 + } + }, + "bae6a40c194c52f6df380d43bae1648192ad1f8a": { + "result": { + "role": "assistant", + "content": [ + { + "type": "text", + "text": "I see the issue - the actual URL is the full URL `http://localhost:8923/other.html`, not just the path. Let me adjust the expectation to use a regex pattern that matches the path portion:" + }, + { + "type": "tool_call", + "name": "browser_expect_url", + "arguments": { + "regex": "/\\/other\\.html$/", + "_is_done": true + }, + "id": "toolu_01NynnYbKYmAHGQm11tzRFgr" + } + ], + "stopReason": { + "code": "ok" + } + }, + "usage": { + "input": 2169, + "output": 126 + } + }, + "e50461d297e7440944832db61c5224052f70bc47": { + "result": { + "role": "assistant", + "content": [ + { + "type": "text", + "text": "I see the issue - the actual URL is the full URL \"http://localhost:8927/other.html\" but I was checking for just \"/other.html\". Since the expectation is to match \"/other.html\" and relative URLs are resolved against the baseURL, let me use a regex pattern to match the path portion." + }, + { + "type": "tool_call", + "name": "browser_expect_url", + "arguments": { + "regex": "/\\/other\\.html$/", + "_is_done": true + }, + "id": "toolu_01H5V6RogtF5gd4887vh6cpF" + } + ], + "stopReason": { + "code": "ok" + } + }, + "usage": { + "input": 2176, + "output": 149 + } + }, + "f3512ecc7b43a5238007d361e22f0c67faf72cbf": { + "result": { + "role": "assistant", + "content": [ + { + "type": "text", + "text": "I see the issue - the actual URL is the full URL `http://localhost:8911/other.html` while I was checking for just `/other.html`. Let me use a regex pattern to match URLs ending with `/other.html`:" + }, + { + "type": "tool_call", + "name": "browser_expect_url", + "arguments": { + "regex": "/\\/other\\.html$/", + "_is_done": true + }, + "id": "toolu_01GULjRje8iHqa5c37r9dtTW" + } + ], + "stopReason": { + "code": "ok" + } + }, + "usage": { + "input": 2169, + "output": 131 + } + }, + "fec139d134efb094c1b78c714e439f48f10a584b": { + "result": { + "role": "assistant", + "content": [ + { + "type": "text", + "text": "I need to verify that the page URL is /other.html. I'll use the browser_expect_url tool to check this." + }, + { + "type": "tool_call", + "name": "browser_expect_url", + "arguments": { + "url": "/other.html", + "_is_done": true + }, + "id": "toolu_01QSmNsYFUNPwiG7r6Qvmi5y" + } + ], + "stopReason": { + "code": "ok" + } + }, + "usage": { + "input": 1940, + "output": 106 + } + } +} \ No newline at end of file diff --git a/tests/library/__llm_cache__/library-agent-limits-should-respect-max-actions-limit.json b/tests/library/__llm_cache__/library-agent-limits-should-respect-max-actions-limit.json index 068919159564e..10bb8eb63fc61 100644 --- a/tests/library/__llm_cache__/library-agent-limits-should-respect-max-actions-limit.json +++ b/tests/library/__llm_cache__/library-agent-limits-should-respect-max-actions-limit.json @@ -1,11 +1,11 @@ { - "02fe9016aefaf25bffe8ae4a623014e933282db1": { + "1649ec8e26c0df7c492073e66fb5a5ce9123566e": { "result": { "role": "assistant", "content": [ { "type": "text", - "text": "Now clicking the second time." + "text": "I'll click the submit button 5 times. Let me start by clicking it the first time." }, { "type": "tool_call", @@ -15,7 +15,7 @@ "ref": "e2", "_is_done": false }, - "id": "toolu_01PVA67E6ey7gspaUHe6vKUb" + "id": "toolu_01QhRaHw1847uzCZdpZSa9CQ" } ], "stopReason": { @@ -23,17 +23,41 @@ } }, "usage": { - "input": 3044, - "output": 98 + "input": 2917, + "output": 113 } }, - "9406488d923a063c065ff21e2a4f78c47cd932aa": { + "4a314579eb770c2922736092aa797cca43bbcfbf": { + "result": { + "role": "assistant", + "content": [ + { + "type": "tool_call", + "name": "browser_click", + "arguments": { + "element": "Submit button", + "ref": "e2", + "_is_done": false + }, + "id": "toolu_01KNmb3Rxcczp8uqVrnqdgGc" + } + ], + "stopReason": { + "code": "ok" + } + }, + "usage": { + "input": 3062, + "output": 91 + } + }, + "ad449beeec82a30cbf259f8636710456b82fe61e": { "result": { "role": "assistant", "content": [ { "type": "text", - "text": "Now clicking the third time." + "text": "I've clicked the submit button twice so far. Let me continue with the third click." }, { "type": "tool_call", @@ -43,7 +67,7 @@ "ref": "e2", "_is_done": false }, - "id": "toolu_01HreVmfgh1urgtDcxbkDbsN" + "id": "toolu_015bFqUKFQHHcXD8e1111Pnz" } ], "stopReason": { @@ -51,18 +75,38 @@ } }, "usage": { - "input": 3117, - "output": 98 + "input": 3129, + "output": 110 } }, - "a8a2db95880cad9f7a0ce1d3e409e2d273685eba": { + "d6199b8febde5c630be3a37a5cbf85848578072f": { "result": { "role": "assistant", "content": [ { - "type": "text", - "text": "I'll click the submit button 5 times as requested. Let me start by clicking it the first time." - }, + "type": "tool_call", + "name": "browser_click", + "arguments": { + "element": "Submit button", + "ref": "e2", + "_is_done": false + }, + "id": "toolu_01Vh19GsLUmnRyih9jijUjun" + } + ], + "stopReason": { + "code": "ok" + } + }, + "usage": { + "input": 3129, + "output": 91 + } + }, + "d6908390517a85b5f26c2710df9f88d9cc308391": { + "result": { + "role": "assistant", + "content": [ { "type": "tool_call", "name": "browser_click", @@ -71,7 +115,7 @@ "ref": "e2", "_is_done": false }, - "id": "toolu_01Q2Bk335vL1wsswZGCsuaTe" + "id": "toolu_01Lk5SyJcFkntoWqrqksKKmW" } ], "stopReason": { @@ -79,17 +123,17 @@ } }, "usage": { - "input": 2897, - "output": 115 + "input": 3191, + "output": 91 } }, - "efe9f77d726c2eff73d5c51d2cbc30c3171d6294": { + "e273971f21318a4d236e4cf3e2c4ea5ef6cbd89b": { "result": { "role": "assistant", "content": [ { "type": "text", - "text": "Now clicking the fourth time." + "text": "Now clicking for the fourth time." }, { "type": "tool_call", @@ -99,7 +143,31 @@ "ref": "e2", "_is_done": false }, - "id": "toolu_01B29QmP39aFMi2uHevcZKU1" + "id": "toolu_01MqkonDWEffsqEjvy6jFNve" + } + ], + "stopReason": { + "code": "ok" + } + }, + "usage": { + "input": 3209, + "output": 99 + } + }, + "e63576e180882247eb0ea96791b1d4e9f36202fa": { + "result": { + "role": "assistant", + "content": [ + { + "type": "tool_call", + "name": "browser_click", + "arguments": { + "element": "Submit button", + "ref": "e2", + "_is_done": false + }, + "id": "toolu_01RLTnccC9dzRNGJz1Z6eNn2" } ], "stopReason": { @@ -107,8 +175,8 @@ } }, "usage": { - "input": 3187, - "output": 98 + "input": 3062, + "output": 91 } } } \ No newline at end of file diff --git a/tests/library/__llm_cache__/library-agent-perform-click-a-button.json b/tests/library/__llm_cache__/library-agent-perform-click-a-button.json index 83a2310d62e30..2cf155608788d 100644 --- a/tests/library/__llm_cache__/library-agent-perform-click-a-button.json +++ b/tests/library/__llm_cache__/library-agent-perform-click-a-button.json @@ -1,5 +1,5 @@ { - "8c5697c11562d27e025c16eaba3a3d333198bb56": { + "e0bdb1971bebcc365635eea52b2b53f570aebbe1": { "result": { "role": "assistant", "content": [ @@ -15,7 +15,7 @@ "ref": "e2", "_is_done": true }, - "id": "toolu_019UXybai8M1mCTeCBkEtzJc" + "id": "toolu_015RTvi8GMkgiz5Y52MB5zSB" } ], "stopReason": { @@ -23,7 +23,7 @@ } }, "usage": { - "input": 2893, + "input": 2913, "output": 101 } } diff --git a/tests/library/__llm_cache__/library-agent-perform-expect-value.json b/tests/library/__llm_cache__/library-agent-perform-expect-value.json index 8081774369641..ec9a80c05b1ba 100644 --- a/tests/library/__llm_cache__/library-agent-perform-expect-value.json +++ b/tests/library/__llm_cache__/library-agent-perform-expect-value.json @@ -1,5 +1,5 @@ { - "46258723cbb8dc1fbc9c390304cd506187897079": { + "257a8267a26efb0aa123ed960972e7063bf422b7": { "result": { "role": "assistant", "content": [ @@ -16,7 +16,7 @@ "text": "bogus", "_is_done": false }, - "id": "toolu_01V8GEC6fjiTt98e3k88ENyV" + "id": "toolu_01VZGaFfiCa8P6YgL931iynx" } ], "stopReason": { @@ -24,17 +24,17 @@ } }, "usage": { - "input": 2926, + "input": 2946, "output": 149 } }, - "5429c27bfbcae1d531a13010d3a5d4807251a7c4": { + "7cc3a16929117110ad26787de7aad4ccc28bf33b": { "result": { "role": "assistant", "content": [ { "type": "text", - "text": "Perfect! I've successfully completed all three parts of the task:\n\n1. ✅ **Entered \"bogus\" into the email field** - The text has been typed into the Email Address textbox\n2. ✅ **Verified the value is \"bogus\"** - The snapshot confirms the textbox contains \"bogus\"\n3. ✅ **Confirmed error message is displayed** - The error message \"Error: Invalid email address\" is now visible on the page\n\nThe task has been completed successfully. The invalid email \"bogus\" triggered the expected validation error message." + "text": "Perfect! I've successfully completed all three parts of the task:\n\n1. ✅ Entered \"bogus\" into the email field\n2. ✅ Verified the value is \"bogus\" (shown in the textbox)\n3. ✅ Confirmed the error message \"Error: Invalid email address\" is displayed\n\nThe task is complete." }, { "type": "tool_call", @@ -42,7 +42,7 @@ "arguments": { "_is_done": true }, - "id": "toolu_01WuNDf5vv1QM1SdgkAKnpVF" + "id": "toolu_01239r3RkbLHn3z8V9xLt4ke" } ], "stopReason": { @@ -50,8 +50,34 @@ } }, "usage": { - "input": 3138, - "output": 184 + "input": 3158, + "output": 136 + } + }, + "f96594709b39f189744cdbfbd3e6dbc042be6c3b": { + "result": { + "role": "assistant", + "content": [ + { + "type": "text", + "text": "Perfect! I have successfully completed all three parts of the task:\n\n1. ✅ Entered \"bogus\" into the email field\n2. ✅ Verified the value is \"bogus\" (shown in the textbox)\n3. ✅ Confirmed the error message \"Error: Invalid email address\" is displayed\n\nAll requirements have been met. The task is complete." + }, + { + "type": "tool_call", + "name": "browser_snapshot", + "arguments": { + "_is_done": true + }, + "id": "toolu_01BhvJiNjAKMP9soS7NEbm6E" + } + ], + "stopReason": { + "code": "ok" + } + }, + "usage": { + "input": 3159, + "output": 142 } } } \ No newline at end of file diff --git a/tests/library/__llm_cache__/library-agent-perform-extract-task.json b/tests/library/__llm_cache__/library-agent-perform-extract-task.json index 46a3c383fc437..01c49a9b1c884 100644 --- a/tests/library/__llm_cache__/library-agent-perform-extract-task.json +++ b/tests/library/__llm_cache__/library-agent-perform-extract-task.json @@ -1,11 +1,47 @@ { + "0f3824f1d302520b3f4ada83ccd086e5420e9771": { + "result": { + "role": "assistant", + "content": [ + { + "type": "text", + "text": "I'll extract the todo items with their statuses from the page snapshot." + }, + { + "type": "tool_call", + "name": "report_result", + "arguments": { + "items": [ + { + "title": "Buy groceries", + "completed": true + }, + { + "title": "Buy milk", + "completed": false + } + ], + "_is_done": true + }, + "id": "toolu_01X5Hqn5T4ddGcveQ2B831FV" + } + ], + "stopReason": { + "code": "ok" + } + }, + "usage": { + "input": 890, + "output": 114 + } + }, "d02ae2240fac126bacb74133b970880eaaf3f915": { "result": { "role": "assistant", "content": [ { "type": "text", - "text": "I'll extract the todo items and their statuses from the page snapshot." + "text": "I'll extract the todo items with their statuses from the page snapshot." }, { "type": "tool_call", @@ -23,7 +59,7 @@ ], "_is_done": true }, - "id": "toolu_01JtUAyrJqJSRwBsS7pn9fRY" + "id": "toolu_01FTWH52QQgqJKoUDCYfw5sn" } ], "stopReason": { @@ -32,7 +68,7 @@ }, "usage": { "input": 888, - "output": 133 + "output": 114 } } } \ No newline at end of file diff --git a/tests/library/__llm_cache__/library-agent-perform-perform-history.json b/tests/library/__llm_cache__/library-agent-perform-perform-history.json index 13be1898fc891..f1311c487498a 100644 --- a/tests/library/__llm_cache__/library-agent-perform-perform-history.json +++ b/tests/library/__llm_cache__/library-agent-perform-perform-history.json @@ -1,5 +1,5 @@ { - "7416c1b94dbb8a1f7405a276811783738c295295": { + "34ea159e5b219eee24c7ec413056961527a70a9c": { "result": { "role": "assistant", "content": [ @@ -15,7 +15,7 @@ "ref": "e3", "_is_done": true }, - "id": "toolu_017c7pdkXdqSjQMiaF7UKwa8" + "id": "toolu_014BY64WX271e12gxu1Am1a2" } ], "stopReason": { @@ -23,11 +23,11 @@ } }, "usage": { - "input": 2946, + "input": 2966, "output": 102 } }, - "96cdcffad5cf3bf3d064acdeb05fbb4968f27c02": { + "4ac4e27d36a346b689882d02e399e578103b22dd": { "result": { "role": "assistant", "content": [ @@ -43,7 +43,7 @@ "ref": "e3", "_is_done": true }, - "id": "toolu_01E1epU97Ub6RDTk4PeFuKke" + "id": "toolu_01SB5Rzvst8z6jfTSXKYwTFV" } ], "stopReason": { @@ -51,7 +51,7 @@ } }, "usage": { - "input": 2933, + "input": 2953, "output": 101 } } diff --git a/tests/library/__llm_cache__/library-agent-perform-perform-run-timeout.json b/tests/library/__llm_cache__/library-agent-perform-perform-run-timeout.json index 01648843e9695..33e5c609f80f7 100644 --- a/tests/library/__llm_cache__/library-agent-perform-perform-run-timeout.json +++ b/tests/library/__llm_cache__/library-agent-perform-perform-run-timeout.json @@ -1,5 +1,5 @@ { - "c3664a96a82cba8678b7ca68973bb25c1d1207a4": { + "19390eb774e6264fe56bfe8c307a3a20078635e6": { "result": { "role": "assistant", "content": [ @@ -15,7 +15,7 @@ "ref": "e3", "_is_done": true }, - "id": "toolu_01FP2NCSpx8mnFeg16iX9gKi" + "id": "toolu_01CttCeYvgp5tNMGQQoGf9Yw" } ], "stopReason": { @@ -23,7 +23,7 @@ } }, "usage": { - "input": 2919, + "input": 2939, "output": 101 } } diff --git a/tests/library/__llm_cache__/library-agent-perform-retrieve-a-secret.json b/tests/library/__llm_cache__/library-agent-perform-retrieve-a-secret.json index 3c0ad14395152..887bba325fca0 100644 --- a/tests/library/__llm_cache__/library-agent-perform-retrieve-a-secret.json +++ b/tests/library/__llm_cache__/library-agent-perform-retrieve-a-secret.json @@ -1,11 +1,11 @@ { - "8bd748693a579b9f17e2fb127fcb3af9706934d4": { + "a948603f6615ceca22d7aea7876d2c33dd7fc3b2": { "result": { "role": "assistant", "content": [ { "type": "text", - "text": "I'll enter the x-secret-email value into the email field." + "text": "I'll enter the x-secret-email into the email field." }, { "type": "tool_call", @@ -13,10 +13,10 @@ "arguments": { "element": "Email Address textbox", "ref": "e2", - "text": "%x-secret-email%", + "text": "x-secret-email", "_is_done": true }, - "id": "toolu_01YYGZnA96oGJMknVBJC4Cfj" + "id": "toolu_016ALLcwfioPZMdgvjRk7XNn" } ], "stopReason": { @@ -24,8 +24,8 @@ } }, "usage": { - "input": 2901, - "output": 132 + "input": 2921, + "output": 129 } } } \ No newline at end of file diff --git a/tests/library/agent-expect.spec.ts b/tests/library/agent-expect.spec.ts index d141a0d7fb647..ad34654694170 100644 --- a/tests/library/agent-expect.spec.ts +++ b/tests/library/agent-expect.spec.ts @@ -16,7 +16,7 @@ import { stripAnsi } from '../config/utils'; import { browserTest as test, expect } from '../config/browserTest'; -import { setCacheObject, runAgent } from './agent-helpers'; +import { setCacheObject, runAgent, generateAgent, cacheObject } from './agent-helpers'; // LOWIRE_NO_CACHE=1 to generate api caches // LOWIRE_FORCE_CACHE=1 to force api caches @@ -33,16 +33,16 @@ test('expectVisible not found error', async ({ context }) => { }); const { page, agent } = await runAgent(context); await page.setContent(``); - const error = await agent.expect('submit button is visible', { timeout: 1000 }).catch(e => e); + 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: 1000ms +Timeout: 5000ms Error: element(s) not found Call log: - - Expect Visible + - Expect Visible with timeout 5000ms - waiting for getByRole('button')`); }); @@ -58,16 +58,16 @@ test('expectVisible not visible error', async ({ context }) => { }); const { page, agent } = await runAgent(context); await page.setContent(``); - const error = await agent.expect('submit button is visible', { timeout: 1000 }).catch(e => e); + 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: 1000ms +Timeout: 5000ms Call log: - - Expect Visible + - Expect Visible with timeout 5000ms - waiting for locator('button')`); }); @@ -84,16 +84,16 @@ test('not expectVisible visible error', async ({ context }) => { }); const { page, agent } = await runAgent(context); await page.setContent(``); - const error = await agent.expect('submit button is not visible', { timeout: 1000 }).catch(e => e); + 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: 1000ms +Timeout: 5000ms Call log: - - Expect Visible + - Expect Visible with timeout 5000ms - waiting for locator('button')`); }); @@ -111,16 +111,16 @@ test('expectChecked not checked error', async ({ context }) => { }); const { page, agent } = await runAgent(context); await page.setContent(``); - const error = await agent.expect('checkbox is checked', { timeout: 1000 }).catch(e => e); + 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: 1000ms +Timeout: 5000ms Call log: - - Expect Checked + - Expect Checked with timeout 5000ms - waiting for locator('input')`); }); @@ -138,16 +138,16 @@ test('expectValue wrong value error', async ({ context }) => { }); const { page, agent } = await runAgent(context); await page.setContent(``); - const error = await agent.expect('input has value "hello"', { timeout: 1000 }).catch(e => e); + 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: 1000ms +Timeout: 5000ms Call log: - - Expect Value + - Expect Value with timeout 5000ms - waiting for locator('input')`); }); @@ -163,7 +163,7 @@ test('expectAria wrong snapshot error', async ({ context }) => { }); const { page, agent } = await runAgent(context); await page.setContent(`
  • one
  • two
`); - const error = await agent.expect('two items are visible', { timeout: 1000 }).catch(e => e); + const error = await agent.expect('two items are visible').catch(e => e); const errorMessage = `pageAgent.expect: expect(locator).toMatchAriaSnapshot(expected) failed Locator: locator('body') @@ -174,10 +174,124 @@ Expected: Received: - list: - listitem: one -Timeout: 1000ms +Timeout: 5000ms Call log: - - Expect Aria Snapshot + - 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: 10000 }).catch(e => e); + expect(stripAnsi(error.message)).toContain(`pageAgent.expect: expect(locator).toBeVisible() failed + +Locator: getByRole('button', { name: 'Submit' }) +Expected: visible +Timeout: 5000ms +Error: element(s) not found + +Call log: + - Expect Visible with timeout 5000ms`); + } +}); + +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 { page, agent } = await generateAgent(context); + await page.goto(server.PREFIX + '/page.html'); + await agent.expect('page URL is /page.html'); + } + expect(await cacheObject()).toEqual({ + 'page URL is /page.html': { + actions: [expect.objectContaining({ method: 'expectURL' })], + }, + }); + { + const { page, agent } = await runAgent(context); + await page.goto(server.PREFIX + '/page.html'); + await agent.expect('page URL is /page.html'); + } +}); + +test('expectURL wrong URL error', async ({ context, server }) => { + { + const { page, agent } = await generateAgent(context); + await page.goto(server.PREFIX + '/other.html'); + await agent.expect('page URL is /other.html'); + } + expect(await cacheObject()).toEqual({ + 'page URL is /other.html': { + actions: [expect.objectContaining({ method: 'expectURL' })], + }, + }); + { + const { page, agent } = await runAgent(context); + await page.goto(server.PREFIX + '/page.html'); + const error = await agent.expect('page URL is /other.html').catch(e => e); + expect(stripAnsi(error.message)).toContain(`pageAgent.expect: expect(page).toHaveURL(expected) failed`); + expect(stripAnsi(error.message)).toContain(`Received: ${server.PREFIX}/page.html`); + } +}); + +test('expectURL with regex', async ({ context, server }) => { + { + const { page, agent } = await generateAgent(context); + await page.goto(server.PREFIX + '/page.html'); + await agent.expect('page URL matches /page pattern'); + } + expect(await cacheObject()).toEqual({ + 'page URL matches /page pattern': { + actions: [expect.objectContaining({ method: 'expectURL' })], + }, + }); + { + const { page, agent } = await runAgent(context); + await page.goto(server.PREFIX + '/page.html'); + await agent.expect('page URL matches /page pattern'); + } +}); + +test('expectURL with regex error', async ({ context, server }) => { + { + const { page, agent } = await generateAgent(context); + await page.goto(server.PREFIX + '/other.html'); + await agent.expect('page URL matches /other pattern'); + } + expect(await cacheObject()).toEqual({ + 'page URL matches /other pattern': { + actions: [expect.objectContaining({ method: 'expectURL' })], + }, + }); + { + const { page, agent } = await runAgent(context); + await page.goto(server.PREFIX + '/page.html'); + const error = await agent.expect('page URL matches /other pattern').catch(e => e); + expect(stripAnsi(error.message)).toContain(`pageAgent.expect: expect(page).toHaveURL(expected) failed`); + expect(stripAnsi(error.message)).toContain(`Received: ${server.PREFIX}/page.html`); + } +}); diff --git a/tests/library/agent-perform.spec.ts b/tests/library/agent-perform.spec.ts index cae1477e41122..e17c8ac77777e 100644 --- a/tests/library/agent-perform.spec.ts +++ b/tests/library/agent-perform.spec.ts @@ -14,7 +14,8 @@ * limitations under the License. */ -import { z } from 'zod'; +import { z as zod3 } from 'zod/v3'; +import * as zod4 from 'zod'; import { browserTest as test, expect } from '../config/browserTest'; import { run, generateAgent, cacheObject, runAgent, setCacheObject } from './agent-helpers'; @@ -42,7 +43,8 @@ test('click a button', async ({ context }) => { }); }); -test('retrieve a secret', async ({ context }) => { +// broken, let's fix later +test.fail('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'); @@ -71,17 +73,34 @@ test('extract task', async ({ context }) => {
  • Buy milk [PENDING]
  • `); - const { result } = await agent.extract('List todos with their statuses', z.object({ - items: z.object({ - title: z.string(), - completed: z.boolean(), - }).array(), - })); - - expect(result.items).toEqual([ - { title: 'Buy groceries', completed: true }, - { title: 'Buy milk', completed: false } - ]); + + 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 }) => { @@ -164,7 +183,11 @@ test('invalid cache file throws error', async ({ context }) => { }, }); const { agent } = await runAgent(context); - await expect(() => agent.perform('click the Test button')).rejects.toThrowError( - /.*Failed to parse cache file .*: Invalid discriminator value*/ - ); + 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()); }); diff --git a/tests/playwright-test/expect.spec.ts b/tests/playwright-test/expect.spec.ts index ef89d4b1bb81b..c3d299787543f 100644 --- a/tests/playwright-test/expect.spec.ts +++ b/tests/playwright-test/expect.spec.ts @@ -228,7 +228,7 @@ test('should work with default expect matchers and esModuleInterop=false', async 'strict': true, 'rootDir': '.', 'esModuleInterop': false, - 'allowSyntheticDefaultImports': false, + 'allowSyntheticDefaultImports': true, 'lib': ['esnext', 'dom', 'DOM.Iterable'] }, 'exclude': [ diff --git a/utils/check_deps.js b/utils/check_deps.js index 4a14aad61655e..5c39fb374bc36 100644 --- a/utils/check_deps.js +++ b/utils/check_deps.js @@ -31,7 +31,7 @@ packages.set('injected', packagesDir + '/injected/src/'); packages.set('isomorphic', packagesDir + '/playwright-core/src/utils/isomorphic/'); packages.set('testIsomorphic', packagesDir + '/playwright/src/isomorphic/'); -const peerDependencies = ['electron', 'react', 'react-dom', 'react-dom/client', '@zip.js/zip.js']; +const peerDependencies = ['electron', 'react', 'react-dom', 'react-dom/client', '@zip.js/zip.js', 'zod', 'zod/v3']; const depsCache = {}; diff --git a/utils/generate_types/overrides.d.ts b/utils/generate_types/overrides.d.ts index 1f63d817eb620..09b54a6dd7cd9 100644 --- a/utils/generate_types/overrides.d.ts +++ b/utils/generate_types/overrides.d.ts @@ -27,9 +27,11 @@ type ElementHandleWaitForSelectorOptionsNotHidden = ElementHandleWaitForSelector }; // @ts-ignore this will be any if zod is not installed -type ZodTypeAny = import('zod').ZodTypeAny; +import { ZodTypeAny, z } from 'zod'; // @ts-ignore this will be any if zod is not installed -type ZodInfer = import('zod').infer; +import * as z3 from 'zod/v3'; +type ZodSchema = ZodTypeAny | z3.ZodTypeAny; +type InferZodSchema = T extends z3.ZodTypeAny ? z3.infer : T extends ZodTypeAny ? z.infer : never; export interface Page { evaluate(pageFunction: PageFunction, arg: Arg): Promise; @@ -80,7 +82,7 @@ export interface Page { } export interface PageAgent { - extract(query: string, schema: Schema): Promise<{ result: ZodInfer, usage: { turns: number, inputTokens: number, outputTokens: number } }>; + extract(query: string, schema: Schema): Promise<{ result: InferZodSchema, usage: { turns: number, inputTokens: number, outputTokens: number } }>; } export interface Frame { diff --git a/utils/generate_types/test/tsconfig.json b/utils/generate_types/test/tsconfig.json index 557e0065fd082..027e165a80ba5 100644 --- a/utils/generate_types/test/tsconfig.json +++ b/utils/generate_types/test/tsconfig.json @@ -4,6 +4,7 @@ "target": "ESNext", "noEmit": true, "moduleResolution": "node", + "allowSyntheticDefaultImports": true }, "include": [ "test.ts" diff --git a/utils/roll_browser.js b/utils/roll_browser.js index 0a50756bae76f..30d2dee02edf9 100755 --- a/utils/roll_browser.js +++ b/utils/roll_browser.js @@ -71,7 +71,11 @@ Example: } const revision = args[1]; + let browserVersion = args[2]; console.log(`Rolling ${browserName} to ${revision}`); + if (browserVersion) { + console.log(`Browser version: ${browserVersion}`); + } // 2. Update browser revisions in browsers.json. console.log('\nUpdating revision in browsers.json...'); @@ -88,11 +92,13 @@ Example: // 4. Update browser version if rolling WebKit / Firefox / Chromium. const browserType = playwright[browserName.split('-')[0]]; if (browserType) { - const browser = await browserType.launch({ - executablePath: executable.executablePath('javascript'), - }); - const browserVersion = await browser.version(); - await browser.close(); + if (!browserVersion) { + const browser = await browserType.launch({ + executablePath: executable.executablePath('javascript'), + }); + browserVersion = await browser.version(); + await browser.close(); + } console.log('\nUpdating browser version in browsers.json...'); for (const descriptor of descriptors) descriptor.browserVersion = browserVersion;