Skip to content

Commit e150010

Browse files
authored
chore: include generated code in generated actions (#38531)
1 parent 4309f5f commit e150010

File tree

14 files changed

+168
-121
lines changed

14 files changed

+168
-121
lines changed

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

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,3 +78,6 @@ export type ExpectValue = {
7878
};
7979

8080
export type Action = ClickAction | DragAction | HoverAction | SelectOptionAction | PressAction | PressSequentiallyAction | FillAction | SetChecked | ExpectVisible | ExpectValue;
81+
export type ActionWithCode = Action & {
82+
code: string;
83+
};
Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
/**
2+
* Copyright (c) Microsoft Corporation.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
import { asLocator } from '../../utils/isomorphic/locatorGenerators';
18+
import { escapeWithQuotes, formatObjectOrVoid } from '../../utils/isomorphic/stringUtils';
19+
20+
import type * as actions from './actions';
21+
import type { Language } from '../../utils/isomorphic/locatorGenerators';
22+
23+
export async function generateCode(sdkLanguage: Language, action: actions.Action) {
24+
switch (action.method) {
25+
case 'click': {
26+
const locator = asLocator(sdkLanguage, action.selector);
27+
return `await page.${locator}.click(${formatObjectOrVoid(action.options)});`;
28+
}
29+
case 'drag': {
30+
const sourceLocator = asLocator(sdkLanguage, action.sourceSelector);
31+
const targetLocator = asLocator(sdkLanguage, action.targetSelector);
32+
return `await page.${sourceLocator}.dragAndDrop(${targetLocator});`;
33+
}
34+
case 'hover': {
35+
const locator = asLocator(sdkLanguage, action.selector);
36+
return `await page.${locator}.hover(${formatObjectOrVoid(action.options)});`;
37+
}
38+
case 'pressKey': {
39+
return `await page.keyboard.press(${escapeWithQuotes(action.key, '\'')});`;
40+
}
41+
case 'selectOption': {
42+
const locator = asLocator(sdkLanguage, action.selector);
43+
return `await page.${locator}.selectOption(${action.labels.length === 1 ? escapeWithQuotes(action.labels[0]) : '[' + action.labels.map(label => escapeWithQuotes(label)).join(', ') + ']'});`;
44+
}
45+
case 'pressSequentially': {
46+
const locator = asLocator(sdkLanguage, action.selector);
47+
const code = [`await page.${locator}.pressSequentially(${escapeWithQuotes(action.text)});`];
48+
if (action.submit)
49+
code.push(`await page.keyboard.press('Enter');`);
50+
return code.join('\n');
51+
}
52+
case 'fill': {
53+
const locator = asLocator(sdkLanguage, action.selector);
54+
const code = [`await page.${locator}.fill(${escapeWithQuotes(action.text)});`];
55+
if (action.submit)
56+
code.push(`await page.keyboard.press('Enter');`);
57+
return code.join('\n');
58+
}
59+
case 'setChecked': {
60+
const locator = asLocator(sdkLanguage, action.selector);
61+
if (action.checked)
62+
return `await page.${locator}.check();`;
63+
else
64+
return `await page.${locator}.uncheck();`;
65+
}
66+
case 'expectVisible': {
67+
const locator = asLocator(sdkLanguage, action.selector);
68+
return `await expect(page.${locator}).toBeVisible();`;
69+
}
70+
case 'expectValue': {
71+
const locator = asLocator(sdkLanguage, action.selector);
72+
return `await expect(page.${locator}).toHaveValue(${escapeWithQuotes(action.value)});`;
73+
}
74+
}
75+
// @ts-expect-error
76+
throw new Error('Unknown action ' + action.method);
77+
}

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

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,26 +16,30 @@
1616

1717
import { BrowserContext } from '../browserContext';
1818
import { runAction } from './actionRunner';
19+
import { generateCode } from './codegen';
1920

2021
import type { Request } from '../network';
2122
import type * as loopTypes from '@lowire/loop';
2223
import type * as actions from './actions';
2324
import type { Page } from '../page';
2425
import type { Progress } from '../progress';
2526
import type { BrowserContextOptions } from '../types';
27+
import type { Language } from '../../utils/isomorphic/locatorGenerators.ts';
2628

2729
type AgentOptions = BrowserContextOptions['agent'];
2830

2931
export class Context {
3032
readonly options: AgentOptions;
3133
readonly progress: Progress;
3234
readonly page: Page;
33-
readonly actions: actions.Action[] = [];
35+
readonly actions: actions.ActionWithCode[] = [];
36+
readonly sdkLanguage: Language;
3437

3538
constructor(progress: Progress, page: Page) {
3639
this.progress = progress;
3740
this.page = page;
3841
this.options = page.browserContext._options.agent;
42+
this.sdkLanguage = page.browserContext._browser.sdkLanguage();
3943
}
4044

4145
async runActionAndWait(action: actions.Action) {
@@ -47,7 +51,8 @@ export class Context {
4751
await this.waitForCompletion(async () => {
4852
for (const a of action) {
4953
await runAction(this.progress, this.page, a, this.options?.secrets ?? []);
50-
this.actions.push(a);
54+
const code = await generateCode(this.sdkLanguage, a);
55+
this.actions.push({ ...a, code });
5156
}
5257
});
5358
return await this.snapshotResult();

packages/playwright-core/src/server/codegen/javascript.ts

Lines changed: 1 addition & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515
*/
1616

1717
import { sanitizeDeviceOptions, toClickOptionsForSourceCode, toKeyboardModifiers, toSignalMap } from './language';
18-
import { asLocator, escapeWithQuotes } from '../../utils';
18+
import { asLocator, escapeWithQuotes, formatObject, formatObjectOrVoid } from '../../utils';
1919
import { deviceDescriptors } from '../deviceDescriptors';
2020

2121
import type { Language, LanguageGenerator, LanguageGeneratorOptions } from './types';
@@ -190,28 +190,6 @@ function formatOptions(value: any, hasArguments: boolean): string {
190190
return (hasArguments ? ', ' : '') + formatObject(value);
191191
}
192192

193-
function formatObject(value: any, indent = ' '): string {
194-
if (typeof value === 'string')
195-
return quote(value);
196-
if (Array.isArray(value))
197-
return `[${value.map(o => formatObject(o)).join(', ')}]`;
198-
if (typeof value === 'object') {
199-
const keys = Object.keys(value).filter(key => value[key] !== undefined).sort();
200-
if (!keys.length)
201-
return '{}';
202-
const tokens: string[] = [];
203-
for (const key of keys)
204-
tokens.push(`${key}: ${formatObject(value[key])}`);
205-
return `{\n${indent}${tokens.join(`,\n${indent}`)}\n}`;
206-
}
207-
return String(value);
208-
}
209-
210-
function formatObjectOrVoid(value: any, indent = ' '): string {
211-
const result = formatObject(value, indent);
212-
return result === '{}' ? '' : result;
213-
}
214-
215193
function formatContextOptions(options: BrowserContextOptions, deviceName: string | undefined, isTest: boolean): string {
216194
const device = deviceName && deviceDescriptors[deviceName];
217195
// recordHAR is replaced with routeFromHAR in the generated code.

packages/playwright-core/src/utils/isomorphic/stringUtils.ts

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,30 @@ export function toSnakeCase(name: string): string {
4747
return name.replace(/([a-z0-9])([A-Z])/g, '$1_$2').replace(/([A-Z])([A-Z][a-z])/g, '$1_$2').toLowerCase();
4848
}
4949

50+
export function formatObject(value: any, indent = ' ', mode: 'multiline' | 'oneline' = 'multiline'): string {
51+
if (typeof value === 'string')
52+
return escapeWithQuotes(value, '\'');
53+
if (Array.isArray(value))
54+
return `[${value.map(o => formatObject(o)).join(', ')}]`;
55+
if (typeof value === 'object') {
56+
const keys = Object.keys(value).filter(key => value[key] !== undefined).sort();
57+
if (!keys.length)
58+
return '{}';
59+
const tokens: string[] = [];
60+
for (const key of keys)
61+
tokens.push(`${key}: ${formatObject(value[key])}`);
62+
if (mode === 'multiline')
63+
return `{\n${tokens.join(`,\n${indent}`)}\n}`;
64+
return `{ ${tokens.join(', ')} }`;
65+
}
66+
return String(value);
67+
}
68+
69+
export function formatObjectOrVoid(value: any, indent = ' '): string {
70+
const result = formatObject(value, indent);
71+
return result === '{}' ? '' : result;
72+
}
73+
5074
export function quoteCSSAttributeValue(text: string): string {
5175
return `"${text.replace(/["\\]/g, char => '\\' + char)}"`;
5276
}

packages/playwright/src/mcp/browser/codegen.ts

Lines changed: 0 additions & 55 deletions
This file was deleted.

packages/playwright/src/mcp/browser/context.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,12 +17,12 @@
1717
import fs from 'fs';
1818

1919
import { debug } from 'playwright-core/lib/utilsBundle';
20+
import { escapeWithQuotes } from 'playwright-core/lib/utils';
2021
import { selectors } from 'playwright-core';
2122

2223
import { logUnhandledError } from '../log';
2324
import { Tab } from './tab';
2425
import { outputFile } from './config';
25-
import * as codegen from './codegen';
2626
import { dateAsFileName } from './tools/utils';
2727

2828
import type * as playwright from '../../../types/test';
@@ -256,7 +256,7 @@ export class Context {
256256

257257
lookupSecret(secretName: string): { value: string, code: string } {
258258
if (!this.config.secrets?.[secretName])
259-
return { value: secretName, code: codegen.quote(secretName) };
259+
return { value: secretName, code: escapeWithQuotes(secretName, '\'') };
260260
return {
261261
value: this.config.secrets[secretName]!,
262262
code: `process.env['${secretName}']`,

packages/playwright/src/mcp/browser/tools/evaluate.ts

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,9 @@
1515
*/
1616

1717
import { z } from 'playwright-core/lib/mcpBundle';
18+
import { escapeWithQuotes } from 'playwright-core/lib/utils';
19+
1820
import { defineTabTool } from './tool';
19-
import * as javascript from '../codegen';
2021

2122
import type { Tab } from '../tab';
2223

@@ -42,9 +43,9 @@ const evaluate = defineTabTool({
4243
let locator: Awaited<ReturnType<Tab['refLocator']>> | undefined;
4344
if (params.ref && params.element) {
4445
locator = await tab.refLocator({ ref: params.ref, element: params.element });
45-
response.addCode(`await page.${locator.resolved}.evaluate(${javascript.quote(params.function)});`);
46+
response.addCode(`await page.${locator.resolved}.evaluate(${escapeWithQuotes(params.function)});`);
4647
} else {
47-
response.addCode(`await page.evaluate(${javascript.quote(params.function)});`);
48+
response.addCode(`await page.evaluate(${escapeWithQuotes(params.function)});`);
4849
}
4950

5051
await tab.waitForCompletion(async () => {

packages/playwright/src/mcp/browser/tools/form.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,9 @@
1515
*/
1616

1717
import { z } from 'playwright-core/lib/mcpBundle';
18+
import { escapeWithQuotes } from 'playwright-core/lib/utils';
19+
1820
import { defineTabTool } from './tool';
19-
import * as codegen from '../codegen';
2021

2122
const fillForm = defineTabTool({
2223
capability: 'core',
@@ -49,7 +50,7 @@ const fillForm = defineTabTool({
4950
response.addCode(`${locatorSource}.setChecked(${field.value});`);
5051
} else if (field.type === 'combobox') {
5152
await locator.selectOption({ label: field.value });
52-
response.addCode(`${locatorSource}.selectOption(${codegen.quote(field.value)});`);
53+
response.addCode(`${locatorSource}.selectOption(${escapeWithQuotes(field.value)});`);
5354
}
5455
}
5556
},

packages/playwright/src/mcp/browser/tools/pdf.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,9 @@
1515
*/
1616

1717
import { z } from 'playwright-core/lib/mcpBundle';
18+
import { formatObject } from 'playwright-core/lib/utils';
19+
1820
import { defineTabTool } from './tool';
19-
import * as javascript from '../codegen';
2021
import { dateAsFileName } from './utils';
2122

2223
const pdfSchema = z.object({
@@ -36,7 +37,7 @@ const pdf = defineTabTool({
3637

3738
handle: async (tab, params, response) => {
3839
const fileName = await response.addFile(params.filename ?? dateAsFileName('pdf'), { origin: 'llm', reason: 'Page saved as PDF' });
39-
response.addCode(`await page.pdf(${javascript.formatObject({ path: fileName })});`);
40+
response.addCode(`await page.pdf(${formatObject({ path: fileName })});`);
4041
await tab.page.pdf({ path: fileName });
4142
},
4243
});

0 commit comments

Comments
 (0)