Skip to content

Commit 506cce6

Browse files
authored
chore: add expect text/value into perform (microsoft#38520)
1 parent 8f97da7 commit 506cce6

9 files changed

Lines changed: 183 additions & 11 deletions

File tree

packages/playwright-core/src/protocol/serializers.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,10 @@ export function serializeValue(value: any, handleSerializer: (value: any) => Han
9494
return innerSerializeValue(value, handleSerializer, { lastId: 0, visited: new Map() }, []);
9595
}
9696

97+
export function serializePlainValue(arg: any): SerializedValue {
98+
return serializeValue(arg, value => ({ fallThrough: value }));
99+
}
100+
97101
function innerSerializeValue(value: any, handleSerializer: (value: any) => HandleOrValue, visitorInfo: VisitorInfo, accessChain: Array<string | number>): SerializedValue {
98102
const handle = handleSerializer(value);
99103
if ('fallThrough' in handle)

packages/playwright-core/src/server/agent/DEPS.list

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,5 +2,8 @@
22
../browserContext.ts
33
../page.ts
44
../progress.ts
5+
../utils/expectUtils.ts
56
../../mcpBundle.ts
7+
../../protocol/
68
../../utilsBundle.ts
9+
../../utils/isomorphic/

packages/playwright-core/src/server/agent/actionRunner.ts

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,10 +14,15 @@
1414
* limitations under the License.
1515
*/
1616

17+
import { serializeExpectedTextValues } from '../utils/expectUtils';
18+
import { serializePlainValue } from '../../protocol/serializers';
19+
1720
import type * as actions from './actions';
21+
import type * as channels from '@protocol/channels';
1822
import type { Page } from '../page';
1923
import type { Progress } from '../progress';
2024
import type { NameValue } from '@protocol/channels';
25+
import type { ExpectResult } from '../frames';
2126

2227
export async function runAction(progress: Progress, page: Page, action: actions.Action, secrets: NameValue[]) {
2328
const frame = page.mainFrame();
@@ -57,7 +62,35 @@ export async function runAction(progress: Progress, page: Page, action: actions.
5762
else
5863
await frame.uncheck(progress, action.selector, { ...strictTrue });
5964
break;
65+
case 'expectVisible': {
66+
const result = await frame.expect(progress, action.selector, { expression: 'to.be.visible', isNot: false }, 5000);
67+
if (result.errorMessage)
68+
throw new Error(result.errorMessage);
69+
break;
70+
}
71+
case 'expectValue': {
72+
let result: ExpectResult;
73+
if (action.type === 'textbox' || action.type === 'combobox' || action.type === 'slider') {
74+
const expectedText = serializeExpectedTextValues([action.value]);
75+
result = await frame.expect(progress, action.selector, { expression: 'to.have.value', expectedText, isNot: false }, 5000);
76+
} else if (action.type === 'checkbox' || action.type === 'radio') {
77+
const expectedValue = serializeArgument({ checked: true });
78+
result = await frame.expect(progress, action.selector, { expression: 'to.be.checked', expectedValue, isNot: false }, 5000);
79+
} else {
80+
throw new Error(`Unsupported element type: ${action.type}`);
81+
}
82+
if (result.errorMessage)
83+
throw new Error(result.errorMessage);
84+
break;
85+
}
6086
}
6187
}
6288

89+
export function serializeArgument(arg: any): channels.SerializedArgument {
90+
return {
91+
value: serializePlainValue(arg),
92+
handles: []
93+
};
94+
}
95+
6396
const strictTrue = { strict: true };

packages/playwright-core/src/server/agent/actions.ts

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -65,4 +65,16 @@ export type SetChecked = {
6565
checked: boolean;
6666
};
6767

68-
export type Action = ClickAction | DragAction | HoverAction | SelectOptionAction | PressAction | PressSequentiallyAction | FillAction | SetChecked;
68+
export type ExpectVisible = {
69+
method: 'expectVisible';
70+
selector: string;
71+
};
72+
73+
export type ExpectValue = {
74+
method: 'expectValue';
75+
selector: string;
76+
type: 'textbox' | 'checkbox' | 'radio' | 'combobox' | 'slider';
77+
value: string;
78+
};
79+
80+
export type Action = ClickAction | DragAction | HoverAction | SelectOptionAction | PressAction | PressSequentiallyAction | FillAction | SetChecked | ExpectVisible | ExpectValue;

packages/playwright-core/src/server/agent/context.ts

Lines changed: 24 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -43,13 +43,17 @@ export class Context {
4343
}
4444

4545
async runActionsAndWait(action: actions.Action[]) {
46-
await this.waitForCompletion(async () => {
47-
for (const a of action) {
48-
await runAction(this.progress, this.page, a, this.options?.secrets ?? []);
49-
this.actions.push(a);
50-
}
51-
});
52-
return await this.snapshotResult();
46+
try {
47+
await this.waitForCompletion(async () => {
48+
for (const a of action) {
49+
await runAction(this.progress, this.page, a, this.options?.secrets ?? []);
50+
this.actions.push(a);
51+
}
52+
});
53+
return await this.snapshotResult();
54+
} catch (e) {
55+
return await this.snapshotResult(e);
56+
}
5357
}
5458

5559
async waitForCompletion<R>(callback: () => Promise<R>): Promise<R> {
@@ -88,18 +92,29 @@ export class Context {
8892
return result;
8993
}
9094

91-
async snapshotResult(): Promise<loopTypes.ToolResult> {
95+
async snapshotResult(error?: Error): Promise<loopTypes.ToolResult> {
9296
let { full } = await this.page.snapshotForAI(this.progress);
9397
full = this._redactText(full);
9498

95-
const text = [`# Page snapshot\n${full}`];
99+
const text: string[] = [];
100+
if (error)
101+
text.push(`# Error\n${error.message}`);
102+
else
103+
text.push(`# Success`);
104+
105+
text.push(`# Page snapshot\n${full}`);
96106

97107
return {
98108
_meta: {
99109
'dev.lowire/state': {
100110
'Page snapshot': full
101111
},
112+
'dev.lowire/history': error ? [{
113+
category: 'error',
114+
content: error.message,
115+
}] : [],
102116
},
117+
isError: !!error,
103118
content: [{ type: 'text', text: text.join('\n\n') }],
104119
};
105120
}

packages/playwright-core/src/server/agent/tools.ts

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
*/
1616

1717
import { z } from '../../mcpBundle';
18+
import { getByRoleSelector, getByTextSelector } from '../../utils/isomorphic/locatorUtils';
1819

1920
import type zod from 'zod';
2021
import type * as loopTypes from '@lowire/loop';
@@ -253,6 +254,67 @@ const fillForm = defineTool({
253254
},
254255
});
255256

257+
const expectVisible = defineTool({
258+
schema: {
259+
name: 'browser_expect_visible',
260+
title: 'Expect element visible',
261+
description: 'Expect element is visible on the page',
262+
inputSchema: baseSchema.extend({
263+
role: z.string().describe('ROLE of the element. Can be found in the snapshot like this: \`- {ROLE} "Accessible Name":\`'),
264+
accessibleName: z.string().describe('ACCESSIBLE_NAME of the element. Can be found in the snapshot like this: \`- role "{ACCESSIBLE_NAME}"\`'),
265+
}),
266+
},
267+
268+
handle: async (context, params) => {
269+
return await context.runActionAndWait({
270+
method: 'expectVisible',
271+
selector: getByRoleSelector(params.role, { name: params.accessibleName }),
272+
});
273+
},
274+
});
275+
276+
const expectVisibleText = defineTool({
277+
schema: {
278+
name: 'browser_expect_visible_text',
279+
title: 'Expect text visible',
280+
description: `Expect text is visible on the page. Prefer ${expectVisible.schema.name} if possible.`,
281+
inputSchema: baseSchema.extend({
282+
text: z.string().describe('TEXT to expect. Can be found in the snapshot like this: \`- role "Accessible Name": {TEXT}\` or like this: \`- text: {TEXT}\`'),
283+
}),
284+
},
285+
286+
handle: async (context, params) => {
287+
return await context.runActionAndWait({
288+
method: 'expectVisible',
289+
selector: getByTextSelector(params.text),
290+
});
291+
},
292+
});
293+
294+
const expectValue = defineTool({
295+
schema: {
296+
name: 'browser_expect_value',
297+
title: 'Expect value',
298+
description: 'Expect element value',
299+
inputSchema: baseSchema.extend({
300+
type: z.enum(['textbox', 'checkbox', 'radio', 'combobox', 'slider']).describe('Type of the element'),
301+
element: z.string().describe('Human-readable element description'),
302+
ref: z.string().describe('Exact target element reference from the page snapshot'),
303+
value: z.string().describe('Value to expect. For checkbox, use "true" or "false".'),
304+
}),
305+
},
306+
307+
handle: async (context, params) => {
308+
const [selector] = await context.refSelectors([{ ref: params.ref, element: params.element }]);
309+
return await context.runActionAndWait({
310+
method: 'expectValue',
311+
selector,
312+
type: params.type,
313+
value: params.value,
314+
});
315+
},
316+
});
317+
256318
export default [
257319
snapshot,
258320
click,
@@ -262,4 +324,7 @@ export default [
262324
pressKey,
263325
type,
264326
fillForm,
327+
expectVisible,
328+
expectVisibleText,
329+
expectValue,
265330
];

packages/playwright-core/src/server/frames.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -90,7 +90,7 @@ export class NavigationAbortedError extends Error {
9090
}
9191
}
9292

93-
type ExpectResult = { matches: boolean, received?: any, log?: string[], timedOut?: boolean, errorMessage?: string };
93+
export type ExpectResult = { matches: boolean, received?: any, log?: string[], timedOut?: boolean, errorMessage?: string };
9494

9595
const kDummyFrameId = '<dummy>';
9696

tests/library/perform-task.spec.ts

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,3 +64,23 @@ test.skip('extract task', async ({ page }) => {
6464
}).array()
6565
})));
6666
});
67+
68+
test('page.perform expect value', async ({ page, server }) => {
69+
await page.setContent(`
70+
<script>
71+
function onInput(event) {
72+
if (!event.target.value.match(/^[^@]+@[^@]+$/))
73+
document.getElementById('error').style.display = 'block';
74+
else
75+
document.getElementById('error').style.display = 'none';
76+
}
77+
</script>
78+
<input type="email" name="email" placeholder="Email Address" oninput="onInput(event);"/>
79+
<div id="error" style="color: red; display: none;">Error: Invalid email address</div>
80+
`);
81+
await page.perform(`
82+
- Enter "bogus" into the email field
83+
- Check that the value is in fact "bogus"
84+
- Check that the error message is displayed
85+
`);
86+
});

tests/library/perform-task.spec.ts-cache.json

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,5 +43,25 @@
4343
"text": "94043"
4444
}
4545
]
46+
},
47+
"\n - Enter \"bogus\" into the email field\n - Check that the value is in fact \"bogus\"\n - Check that the error message is displayed\n ": {
48+
"timestamp": 1765416520724,
49+
"actions": [
50+
{
51+
"method": "fill",
52+
"selector": "internal:role=textbox[name=\"Email Address\"i]",
53+
"text": "bogus"
54+
},
55+
{
56+
"method": "expectValue",
57+
"selector": "internal:role=textbox[name=\"Email Address\"i]",
58+
"type": "textbox",
59+
"value": "bogus"
60+
},
61+
{
62+
"method": "expectVisible",
63+
"selector": "internal:text=\"Error: Invalid email address\"i"
64+
}
65+
]
4666
}
4767
}

0 commit comments

Comments
 (0)