diff --git a/packages/playwright-core/src/server/recorder.ts b/packages/playwright-core/src/server/recorder.ts index d2a4d0afe7..24a14b3f2b 100644 --- a/packages/playwright-core/src/server/recorder.ts +++ b/packages/playwright-core/src/server/recorder.ts @@ -306,8 +306,9 @@ class ContextRecorder extends EventEmitter { new JavaLanguageGenerator(), new JavaScriptLanguageGenerator(false), new JavaScriptLanguageGenerator(true), - new PythonLanguageGenerator(false), - new PythonLanguageGenerator(true), + new PythonLanguageGenerator(false, false), + new PythonLanguageGenerator(true, false), + new PythonLanguageGenerator(false, true), new CSharpLanguageGenerator(), ]); const primaryLanguage = [...languages].find(l => l.id === language)!; diff --git a/packages/playwright-core/src/server/recorder/python.ts b/packages/playwright-core/src/server/recorder/python.ts index 254e3b6a0c..69dc0a6be8 100644 --- a/packages/playwright-core/src/server/recorder/python.ts +++ b/packages/playwright-core/src/server/recorder/python.ts @@ -33,17 +33,22 @@ export class PythonLanguageGenerator implements LanguageGenerator { private _awaitPrefix: '' | 'await '; private _asyncPrefix: '' | 'async '; private _isAsync: boolean; + private _isTest: boolean; - constructor(isAsync: boolean) { - this.id = isAsync ? 'python-async' : 'python'; - this.fileName = isAsync ? 'Python Async' : 'Python'; + constructor(isAsync: boolean, isTest: boolean) { + this.id = isTest ? 'pytest' : (isAsync ? 'python-async' : 'python'); + this.fileName = isTest ? 'Pytest' : (isAsync ? 'Python Async' : 'Python'); this._isAsync = isAsync; + this._isTest = isTest; this._awaitPrefix = isAsync ? 'await ' : ''; this._asyncPrefix = isAsync ? 'async ' : ''; } generateAction(actionInContext: ActionInContext): string { const action = actionInContext.action; + if (this._isTest && (action.name === 'openPage' || action.name === 'closePage')) + return ''; + const pageAlias = actionInContext.frame.pageAlias; const formatter = new PythonFormatter(4); formatter.newLine(); @@ -150,7 +155,23 @@ export class PythonLanguageGenerator implements LanguageGenerator { generateHeader(options: LanguageGeneratorOptions): string { const formatter = new PythonFormatter(); - if (this._isAsync) { + if (this._isTest) { + formatter.add(`${options.deviceName ? 'import pytest\n' : ''} +from playwright.sync_api import Page, expect +${options.deviceName ? ` + +@pytest.fixture(scope="session") +def browser_context_args(browser_context_args, playwright) { + device = playwright.devices["${options.deviceName}"] + return dict( + **browser_context_args, + **device, + ) +} +` : ''} + +def test_example(page: Page) -> None {`); + } else if (this._isAsync) { formatter.add(` import asyncio @@ -173,7 +194,9 @@ def run(playwright: Playwright) -> None { } generateFooter(saveStorage: string | undefined): string { - if (this._isAsync) { + if (this._isTest) { + return ''; + } else if (this._isAsync) { const storageStateLine = saveStorage ? `\n await context.storage_state(path=${quote(saveStorage)})` : ''; return `\n # ---------------------${storageStateLine} await context.close() diff --git a/tests/library/inspector/cli-codegen-pytest.spec.ts b/tests/library/inspector/cli-codegen-pytest.spec.ts new file mode 100644 index 0000000000..d3daf64e5c --- /dev/null +++ b/tests/library/inspector/cli-codegen-pytest.spec.ts @@ -0,0 +1,74 @@ +/** + * 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 fs from 'fs'; +import path from 'path'; +import { test, expect } from './inspectorTest'; + +const emptyHTML = new URL('file://' + path.join(__dirname, '..', '..', 'assets', 'empty.html')).toString(); + +test('should print the correct imports and context options', async ({ runCLI }) => { + const cli = runCLI(['--target=pytest', emptyHTML]); + const expectedResult = `from playwright.sync_api import Page, expect + + +def test_example(page: Page) -> None:`; + await cli.waitFor(expectedResult); + expect(cli.text()).toContain(expectedResult); +}); + +test('should print the correct context options when using a device', async ({ browserName, runCLI }, testInfo) => { + test.skip(browserName !== 'webkit'); + + const tmpFile = testInfo.outputPath('script.js'); + const cli = runCLI(['--target=pytest', '--device=iPhone 11', '--output', tmpFile, emptyHTML]); + await cli.exited; + const content = fs.readFileSync(tmpFile); + expect(content.toString()).toBe(`import pytest + +from playwright.sync_api import Page, expect + + +@pytest.fixture(scope="session") +def browser_context_args(browser_context_args, playwright): + device = playwright.devices["iPhone 11"] + return dict( + **browser_context_args, + **device, + ) + + +def test_example(page: Page) -> None: + + # Go to ${emptyHTML} + page.goto("${emptyHTML}") +`); +}); + +test('should save the codegen output to a file if specified', async ({ runCLI }, testInfo) => { + const tmpFile = testInfo.outputPath('script.js'); + const cli = runCLI(['--target=pytest', '--output', tmpFile, emptyHTML]); + await cli.exited; + const content = fs.readFileSync(tmpFile); + expect(content.toString()).toBe(`from playwright.sync_api import Page, expect + + +def test_example(page: Page) -> None: + + # Go to ${emptyHTML} + page.goto("${emptyHTML}") +`); +});