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",
|
||||
"check-deps": "node utils/check_deps.js",
|
||||
"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": [
|
||||
"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) => {
|
||||
const flags = params.ignoreCase ? 'i' : '';
|
||||
const code = [
|
||||
`await expect(page).toHaveURL(/${params.url}/);`
|
||||
`await expect(page).toHaveURL(/${params.url}/${flags});`
|
||||
];
|
||||
|
||||
return {
|
||||
code,
|
||||
action: async () => {
|
||||
const re = new RegExp(params.url, params.ignoreCase ? 'i' : '');
|
||||
const re = new RegExp(params.url, flags);
|
||||
const url = context.page.url();
|
||||
if (!re.test(url))
|
||||
throw new Error(`Expected URL to match ${params.url}, but got ${url}.`);
|
||||
|
@ -81,21 +81,14 @@ export class Context {
|
||||
return result;
|
||||
}
|
||||
|
||||
async runScript(tasks: string[]): Promise<string> {
|
||||
this._codeCollector = [
|
||||
`test('generated code', async ({ page }) => {`,
|
||||
];
|
||||
async runScript(tasks: string[]): Promise<{ code: string[] }> {
|
||||
await runTasks(this, tasks);
|
||||
this._codeCollector.push('});');
|
||||
return this._codeCollector.join('\n');
|
||||
return { code: this._codeCollector };
|
||||
}
|
||||
|
||||
async beforeTask(task: string) {
|
||||
this._codeCollector.push(`// ${task}`);
|
||||
}
|
||||
|
||||
async afterTask() {
|
||||
this._codeCollector.push('');
|
||||
this._codeCollector.push(`// ${task}`);
|
||||
}
|
||||
|
||||
async runTool(tool: Tool, params: Record<string, unknown> | undefined): Promise<{ content: string }> {
|
||||
@ -136,7 +129,7 @@ export class Context {
|
||||
if (captureSnapshot && !this._javaScriptBlocked())
|
||||
result.push(await this._snapshot());
|
||||
|
||||
this._codeCollector.push(...code.map(c => ` ${c}`));
|
||||
this._codeCollector.push(...code);
|
||||
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.
|
||||
*/
|
||||
|
||||
import fs from 'fs';
|
||||
|
||||
import dotenv from 'dotenv';
|
||||
import { program } from 'commander';
|
||||
|
||||
import { Context } from './browser/context';
|
||||
import { Context } from './codegen/context';
|
||||
|
||||
/* eslint-disable no-console */
|
||||
|
||||
@ -27,32 +29,17 @@ const packageJSON = require('../package.json');
|
||||
|
||||
program
|
||||
.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)
|
||||
.action(async () => {
|
||||
const context = await Context.create();
|
||||
const code = await context.runScript(script);
|
||||
console.log('Output code:');
|
||||
console.log('```javascript');
|
||||
console.log(code);
|
||||
console.log('```');
|
||||
await context.close();
|
||||
.action(async (spec, options) => {
|
||||
const content = await fs.promises.readFile(spec, 'utf8');
|
||||
const codegenContext = new Context();
|
||||
const code = await codegenContext.generateCode(content);
|
||||
if (options.output)
|
||||
await fs.promises.writeFile(options.output, code);
|
||||
else
|
||||
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 };
|
||||
|
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