mirror of
https://github.com/microsoft/playwright.git
synced 2025-06-26 21:40:17 +00:00
chore: add the codegen tool (#36446)
This commit is contained in:
parent
bf5fea29f2
commit
216e6be464
@ -44,7 +44,8 @@
|
|||||||
"roll": "node utils/roll_browser.js",
|
"roll": "node utils/roll_browser.js",
|
||||||
"check-deps": "node utils/check_deps.js",
|
"check-deps": "node utils/check_deps.js",
|
||||||
"build-android-driver": "./utils/build_android_driver.sh",
|
"build-android-driver": "./utils/build_android_driver.sh",
|
||||||
"innerloop": "playwright run-server --reuse-browser"
|
"innerloop": "playwright run-server --reuse-browser",
|
||||||
|
"mdd": "playwright-mdd packages/playwright-mdd/specs/integration.spec.md -o packages/playwright-mdd/tests/integration.spec.ts"
|
||||||
},
|
},
|
||||||
"workspaces": [
|
"workspaces": [
|
||||||
"packages/*"
|
"packages/*"
|
||||||
|
8
packages/playwright-mdd/specs/integration.spec.md
Normal file
8
packages/playwright-mdd/specs/integration.spec.md
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
# Pass
|
||||||
|
- Navigate to https://debs-obrien.github.io/playwright-movies-app
|
||||||
|
- Click search icon
|
||||||
|
- Type "Twister" in the search field and hit Enter
|
||||||
|
- Verify that the URL contains the search term "twister"
|
||||||
|
- Verify that the search results contain an image named "Twisters"
|
||||||
|
- Click on the link for the movie "Twisters"
|
||||||
|
- Verify that the main heading on the movie page is "Twisters"
|
@ -58,14 +58,15 @@ const assertURL = defineTool({
|
|||||||
},
|
},
|
||||||
|
|
||||||
handle: async (context, params) => {
|
handle: async (context, params) => {
|
||||||
|
const flags = params.ignoreCase ? 'i' : '';
|
||||||
const code = [
|
const code = [
|
||||||
`await expect(page).toHaveURL(/${params.url}/);`
|
`await expect(page).toHaveURL(/${params.url}/${flags});`
|
||||||
];
|
];
|
||||||
|
|
||||||
return {
|
return {
|
||||||
code,
|
code,
|
||||||
action: async () => {
|
action: async () => {
|
||||||
const re = new RegExp(params.url, params.ignoreCase ? 'i' : '');
|
const re = new RegExp(params.url, flags);
|
||||||
const url = context.page.url();
|
const url = context.page.url();
|
||||||
if (!re.test(url))
|
if (!re.test(url))
|
||||||
throw new Error(`Expected URL to match ${params.url}, but got ${url}.`);
|
throw new Error(`Expected URL to match ${params.url}, but got ${url}.`);
|
||||||
|
@ -81,21 +81,14 @@ export class Context {
|
|||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
async runScript(tasks: string[]): Promise<string> {
|
async runScript(tasks: string[]): Promise<{ code: string[] }> {
|
||||||
this._codeCollector = [
|
|
||||||
`test('generated code', async ({ page }) => {`,
|
|
||||||
];
|
|
||||||
await runTasks(this, tasks);
|
await runTasks(this, tasks);
|
||||||
this._codeCollector.push('});');
|
return { code: this._codeCollector };
|
||||||
return this._codeCollector.join('\n');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async beforeTask(task: string) {
|
async beforeTask(task: string) {
|
||||||
this._codeCollector.push(`// ${task}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
async afterTask() {
|
|
||||||
this._codeCollector.push('');
|
this._codeCollector.push('');
|
||||||
|
this._codeCollector.push(`// ${task}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
async runTool(tool: Tool, params: Record<string, unknown> | undefined): Promise<{ content: string }> {
|
async runTool(tool: Tool, params: Record<string, unknown> | undefined): Promise<{ content: string }> {
|
||||||
@ -136,7 +129,7 @@ export class Context {
|
|||||||
if (captureSnapshot && !this._javaScriptBlocked())
|
if (captureSnapshot && !this._javaScriptBlocked())
|
||||||
result.push(await this._snapshot());
|
result.push(await this._snapshot());
|
||||||
|
|
||||||
this._codeCollector.push(...code.map(c => ` ${c}`));
|
this._codeCollector.push(...code);
|
||||||
return { content: result.join('\n') };
|
return { content: result.join('\n') };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
39
packages/playwright-mdd/src/codegen/context.ts
Normal file
39
packages/playwright-mdd/src/codegen/context.ts
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) Microsoft Corporation.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { tools } from './tools';
|
||||||
|
import { runTasks } from '../loop';
|
||||||
|
|
||||||
|
import type { Tool } from './tool';
|
||||||
|
|
||||||
|
export class Context {
|
||||||
|
readonly tools = tools;
|
||||||
|
private _codeCollector: string[] = [];
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
}
|
||||||
|
|
||||||
|
async runTool(tool: Tool<any>, params: Record<string, unknown>): Promise<{ content: string }> {
|
||||||
|
const { content, code } = await tool.handle(this, params);
|
||||||
|
this._codeCollector.push(...code);
|
||||||
|
return { content };
|
||||||
|
}
|
||||||
|
|
||||||
|
async generateCode(content: string) {
|
||||||
|
await runTasks(this, ['Generate code for the following test spec: ' + content]);
|
||||||
|
return this._codeCollector.join('\n');
|
||||||
|
}
|
||||||
|
}
|
33
packages/playwright-mdd/src/codegen/done.ts
Normal file
33
packages/playwright-mdd/src/codegen/done.ts
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) Microsoft Corporation.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { z } from 'zod';
|
||||||
|
|
||||||
|
import { defineTool } from './tool';
|
||||||
|
|
||||||
|
const doneTool = defineTool({
|
||||||
|
schema: {
|
||||||
|
name: 'done',
|
||||||
|
description: 'Call this tool to indicate that the task is complete',
|
||||||
|
inputSchema: z.object({}),
|
||||||
|
},
|
||||||
|
|
||||||
|
handle: async () => ({ content: 'Done', code: [] }),
|
||||||
|
});
|
||||||
|
|
||||||
|
export default [
|
||||||
|
doneTool,
|
||||||
|
];
|
60
packages/playwright-mdd/src/codegen/generateCode.ts
Normal file
60
packages/playwright-mdd/src/codegen/generateCode.ts
Normal file
@ -0,0 +1,60 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) Microsoft Corporation.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { z } from 'zod';
|
||||||
|
|
||||||
|
import { defineTool } from './tool';
|
||||||
|
import { Context as BrowserContext } from '../browser/context';
|
||||||
|
|
||||||
|
const generateCode = defineTool({
|
||||||
|
schema: {
|
||||||
|
name: 'codegen',
|
||||||
|
description: 'Generate code for the given test spec',
|
||||||
|
inputSchema: z.object({
|
||||||
|
tests: z.array(z.object({
|
||||||
|
name: z.string(),
|
||||||
|
steps: z.array(z.string()),
|
||||||
|
})),
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
|
||||||
|
handle: async (context, params) => {
|
||||||
|
const { tests } = params;
|
||||||
|
const code: string[] = [
|
||||||
|
`/* eslint-disable notice/notice */`,
|
||||||
|
'',
|
||||||
|
`import { test, expect } from '@playwright/test';`,
|
||||||
|
'',
|
||||||
|
];
|
||||||
|
for (const test of tests) {
|
||||||
|
code.push(`test('${test.name}', async ({ page }) => {`);
|
||||||
|
const context = await BrowserContext.create();
|
||||||
|
const result = await context.runScript(test.steps);
|
||||||
|
code.push(...result.code.map(c => c ? ` ${c}` : ''));
|
||||||
|
code.push('});');
|
||||||
|
code.push('');
|
||||||
|
await context.close();
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
content: 'Generated code has been saved and delivered to the user. Call the "done" tool, do not produce any other output.',
|
||||||
|
code
|
||||||
|
};
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export default [
|
||||||
|
generateCode,
|
||||||
|
];
|
28
packages/playwright-mdd/src/codegen/tool.ts
Normal file
28
packages/playwright-mdd/src/codegen/tool.ts
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) Microsoft Corporation.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import type { z } from 'zod';
|
||||||
|
import type { Context } from './context';
|
||||||
|
import type { ToolSchema } from '../loop';
|
||||||
|
|
||||||
|
export type Tool<Input extends z.Schema = z.Schema> = {
|
||||||
|
schema: ToolSchema<Input>;
|
||||||
|
handle: (context: Context, params: z.output<Input>) => Promise<{ content: string, code: string[] }>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function defineTool<Input extends z.Schema>(tool: Tool<Input>): Tool<Input> {
|
||||||
|
return tool;
|
||||||
|
}
|
25
packages/playwright-mdd/src/codegen/tools.ts
Normal file
25
packages/playwright-mdd/src/codegen/tools.ts
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) Microsoft Corporation.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import generateCode from './generateCode';
|
||||||
|
import done from './done';
|
||||||
|
|
||||||
|
import type { Tool } from './tool.js';
|
||||||
|
|
||||||
|
export const tools: Tool<any>[] = [
|
||||||
|
...generateCode,
|
||||||
|
...done,
|
||||||
|
];
|
@ -14,10 +14,12 @@
|
|||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import fs from 'fs';
|
||||||
|
|
||||||
import dotenv from 'dotenv';
|
import dotenv from 'dotenv';
|
||||||
import { program } from 'commander';
|
import { program } from 'commander';
|
||||||
|
|
||||||
import { Context } from './browser/context';
|
import { Context } from './codegen/context';
|
||||||
|
|
||||||
/* eslint-disable no-console */
|
/* eslint-disable no-console */
|
||||||
|
|
||||||
@ -27,32 +29,17 @@ const packageJSON = require('../package.json');
|
|||||||
|
|
||||||
program
|
program
|
||||||
.version('Version ' + packageJSON.version)
|
.version('Version ' + packageJSON.version)
|
||||||
|
.argument('<spec>', 'The test spec to generate code for')
|
||||||
|
.option('-o, --output <path>', 'The path to save the generated code')
|
||||||
.name(packageJSON.name)
|
.name(packageJSON.name)
|
||||||
.action(async () => {
|
.action(async (spec, options) => {
|
||||||
const context = await Context.create();
|
const content = await fs.promises.readFile(spec, 'utf8');
|
||||||
const code = await context.runScript(script);
|
const codegenContext = new Context();
|
||||||
console.log('Output code:');
|
const code = await codegenContext.generateCode(content);
|
||||||
console.log('```javascript');
|
if (options.output)
|
||||||
console.log(code);
|
await fs.promises.writeFile(options.output, code);
|
||||||
console.log('```');
|
else
|
||||||
await context.close();
|
console.log(code);
|
||||||
});
|
});
|
||||||
|
|
||||||
// An example of a failing script.
|
|
||||||
//
|
|
||||||
// const script = [
|
|
||||||
// 'Navigate to https://debs-obrien.github.io/playwright-movies-app/search?searchTerm=Twister&page=1',
|
|
||||||
// 'Verify that the URL contains the search term "twisters"',
|
|
||||||
// ];
|
|
||||||
|
|
||||||
const script = [
|
|
||||||
'Navigate to https://debs-obrien.github.io/playwright-movies-app',
|
|
||||||
'Click search icon',
|
|
||||||
'Type "Twister" in the search field and hit Enter',
|
|
||||||
'Verify that the URL contains the search term "twister"',
|
|
||||||
'Verify that the search results contain an image named "Twisters"',
|
|
||||||
'Click on the link for the movie "Twisters"',
|
|
||||||
'Verify that the main heading on the movie page is "Twisters"',
|
|
||||||
];
|
|
||||||
|
|
||||||
export { program };
|
export { program };
|
||||||
|
28
packages/playwright-mdd/tests/integration.spec.ts
Normal file
28
packages/playwright-mdd/tests/integration.spec.ts
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
/* eslint-disable notice/notice */
|
||||||
|
|
||||||
|
import { test, expect } from '@playwright/test';
|
||||||
|
|
||||||
|
test('Search and verify Twisters movie', async ({ page }) => {
|
||||||
|
|
||||||
|
// Navigate to https://debs-obrien.github.io/playwright-movies-app
|
||||||
|
await page.goto('https://debs-obrien.github.io/playwright-movies-app');
|
||||||
|
|
||||||
|
// Click search icon
|
||||||
|
await page.getByRole('search').click();
|
||||||
|
|
||||||
|
// Type "Twister" in the search field and hit Enter
|
||||||
|
await page.getByRole('textbox', { name: 'Search Input' }).fill('Twister');
|
||||||
|
await page.getByRole('textbox', { name: 'Search Input' }).press('Enter');
|
||||||
|
|
||||||
|
// Verify that the URL contains the search term "twister"
|
||||||
|
await expect(page).toHaveURL(/twister/i);
|
||||||
|
|
||||||
|
// Verify that the search results contain an image named "Twisters"
|
||||||
|
await expect(page.getByRole('link', { name: 'poster of Twisters Twisters' })).toBeVisible();
|
||||||
|
|
||||||
|
// Click on the link for the movie "Twisters"
|
||||||
|
await page.getByRole('link', { name: 'poster of Twisters Twisters' }).click();
|
||||||
|
|
||||||
|
// Verify that the main heading on the movie page is "Twisters"
|
||||||
|
await expect(page.getByTestId('movie-summary').getByRole('heading', { name: 'Twisters' })).toBeVisible();
|
||||||
|
});
|
Loading…
x
Reference in New Issue
Block a user