diff --git a/docs/src/codegen.md b/docs/src/codegen.md index 641383d7b4e0a..fbbb906cd3e5f 100644 --- a/docs/src/codegen.md +++ b/docs/src/codegen.md @@ -414,6 +414,26 @@ pwsh bin/Debug/netX/playwright.ps1 codegen --load-storage=auth.json github.com/m github signed in showing use of load storage scharp +#### Use existing userDataDir + +Run `codegen` with `--user-data-dir` to set a fixed [user data directory](https://playwright.dev/docs/api/class-browsertype#browser-type-launch-persistent-context-option-user-data-dir) for the browser session. If you provide your existing browser's user data directory, codegen will use your existing browser profile and have access to your authentication state. + +```bash js +npx playwright codegen --user-data-dir=/path/to/your/browser/data/ github.com/microsoft/playwright +``` + +```bash java +mvn exec:java -e -D exec.mainClass=com.microsoft.playwright.CLI -D exec.args="codegen --user-data-dir=/path/to/your/browser/data/ github.com/microsoft/playwright" +``` + +```bash python +playwright codegen --user-data-dir=/path/to/your/browser/data/ github.com/microsoft/playwright +``` + +```bash csharp +pwsh bin/Debug/netX/playwright.ps1 codegen --user-data-dir=/path/to/your/browser/data/ github.com/microsoft/playwright +``` + ## Record using custom setup If you would like to use codegen in some non-standard setup (for example, use [`method: BrowserContext.route`]), it is possible to call [`method: Page.pause`] that will open a separate window with codegen controls. diff --git a/packages/playwright-core/src/cli/program.ts b/packages/playwright-core/src/cli/program.ts index 6350781becd9b..8594a40e38659 100644 --- a/packages/playwright-core/src/cli/program.ts +++ b/packages/playwright-core/src/cli/program.ts @@ -29,7 +29,6 @@ import { assert, getPackageManagerExecCommand } from '../utils'; import { wrapInASCIIBox } from '../server/utils/ascii'; import { dotenv, program } from '../utilsBundle'; -import type { Browser } from '../client/browser'; import type { BrowserContext } from '../client/browserContext'; import type { BrowserType } from '../client/browserType'; import type { Page } from '../client/page'; @@ -70,6 +69,7 @@ commandWithOpenOptions('codegen [url]', 'open page and generate code for user ac ['-o, --output ', 'saves the generated script to a file'], ['--target ', `language to generate, one of javascript, playwright-test, python, python-async, python-pytest, csharp, csharp-mstest, csharp-nunit, java, java-junit`, codegenId()], ['--test-id-attribute ', 'use the specified attribute to generate data test ID selectors'], + ['--user-data-dir ', 'use the specified user data directory instead of a new context'], ]).action(function(url, options) { codegen(options, url).catch(logErrorAndExit); }).addHelpText('afterAll', ` @@ -361,6 +361,7 @@ type Options = { timezone?: string; viewportSize?: string; userAgent?: string; + userDataDir?: string }; type CaptureOptions = { @@ -370,7 +371,7 @@ type CaptureOptions = { paperFormat?: string; }; -async function launchContext(options: Options, extraOptions: LaunchOptions): Promise<{ browser: Browser, browserName: string, launchOptions: LaunchOptions, contextOptions: BrowserContextOptions, context: BrowserContext }> { +async function launchContext(options: Options, extraOptions: LaunchOptions): Promise<{ browserName: string, launchOptions: LaunchOptions, contextOptions: BrowserContextOptions, context: BrowserContext }> { validateOptions(options); const browserType = lookupBrowserType(options); const launchOptions: LaunchOptions = extraOptions; @@ -410,33 +411,6 @@ async function launchContext(options: Options, extraOptions: LaunchOptions): Pro launchOptions.proxy.bypass = options.proxyBypass; } - const browser = await browserType.launch(launchOptions); - - if (process.env.PWTEST_CLI_IS_UNDER_TEST) { - (process as any)._didSetSourcesForTest = (text: string) => { - process.stdout.write('\n-------------8<-------------\n'); - process.stdout.write(text); - process.stdout.write('\n-------------8<-------------\n'); - const autoExitCondition = process.env.PWTEST_CLI_AUTO_EXIT_WHEN; - if (autoExitCondition && text.includes(autoExitCondition)) - closeBrowser(); - }; - // Make sure we exit abnormally when browser crashes. - const logs: string[] = []; - require('playwright-core/lib/utilsBundle').debug.log = (...args: any[]) => { - const line = require('util').format(...args) + '\n'; - logs.push(line); - process.stderr.write(line); - }; - browser.on('disconnected', () => { - const hasCrashLine = logs.some(line => line.includes('process did exit:') && !line.includes('process did exit: exitCode=0, signal=null')); - if (hasCrashLine) { - process.stderr.write('Detected browser crash.\n'); - gracefullyProcessExitDoNotHang(1); - } - }); - } - // Viewport size if (options.viewportSize) { try { @@ -501,9 +475,37 @@ async function launchContext(options: Options, extraOptions: LaunchOptions): Pro contextOptions.serviceWorkers = 'block'; } - // Close app when the last window closes. + const context = await createContext(browserType, launchOptions, contextOptions, options.userDataDir); - const context = await browser.newContext(contextOptions); + if (process.env.PWTEST_CLI_IS_UNDER_TEST) { + (process as any)._didSetSourcesForTest = (text: string) => { + process.stdout.write('\n-------------8<-------------\n'); + process.stdout.write(text); + process.stdout.write('\n-------------8<-------------\n'); + const autoExitCondition = process.env.PWTEST_CLI_AUTO_EXIT_WHEN; + if (autoExitCondition && text.includes(autoExitCondition)) + closeBrowser(); + }; + // Make sure we exit abnormally when browser crashes. + const logs: string[] = []; + require('playwright-core/lib/utilsBundle').debug.log = (...args: any[]) => { + const line = require('util').format(...args) + '\n'; + logs.push(line); + process.stderr.write(line); + }; + const crashHandler = () => { + const hasCrashLine = logs.some(line => line.includes('process did exit:') && !line.includes('process did exit: exitCode=0, signal=null')); + if (hasCrashLine) { + process.stderr.write('Detected browser crash.\n'); + gracefullyProcessExitDoNotHang(1); + } + }; + const browser = context.browser(); + if (browser) + browser.on('disconnected', crashHandler); + else + context.on('close', crashHandler); + } let closingBrowser = false; async function closeBrowser() { @@ -514,20 +516,30 @@ async function launchContext(options: Options, extraOptions: LaunchOptions): Pro closingBrowser = true; if (options.saveStorage) await context.storageState({ path: options.saveStorage }).catch(e => null); - if (options.saveHar) + const browser = context.browser(); + // Close the context no matter what if we don't have a browser, as context must be a persistent context + if (options.saveHar || !browser) await context.close(); - await browser.close(); + await browser?.close(); } - context.on('page', page => { + function listenToPage(page: Page) { page.on('dialog', () => {}); // Prevent dialogs from being automatically dismissed. page.on('close', () => { - const hasPage = browser.contexts().some(context => context.pages().length > 0); - if (hasPage) + if (context.pages().length > 0) return; // Avoid the error when the last page is closed because the browser has been closed. closeBrowser().catch(() => {}); }); + } + + // launchPersistentContext creates an initial page, so we need to listen to it + for (const page of context.pages()) + listenToPage(page); + + context.on('page', listenToPage); + context.on('close', () => { + closeBrowser().catch(() => {}); }); process.on('SIGINT', async () => { await closeBrowser(); @@ -543,11 +555,20 @@ async function launchContext(options: Options, extraOptions: LaunchOptions): Pro delete launchOptions.executablePath; delete launchOptions.handleSIGINT; delete contextOptions.deviceScaleFactor; - return { browser, browserName: browserType.name(), context, contextOptions, launchOptions }; + return { browserName: browserType.name(), context, contextOptions, launchOptions }; +} + +async function createContext(browserType: BrowserType, launchOptions: LaunchOptions, contextOptions: BrowserContextOptions, userDataDir: string | undefined): Promise { + if (userDataDir) + return await browserType.launchPersistentContext(userDataDir, { ...launchOptions, ...contextOptions }); + const browser = await browserType.launch(launchOptions); + return await browser.newContext(contextOptions); } -async function openPage(context: BrowserContext, url: string | undefined): Promise { - const page = await context.newPage(); +async function openPageIfNeeded(context: BrowserContext, url: string | undefined): Promise { + let page = context.pages()[0]; + if (!page) + page = await context.newPage(); if (url) { if (fs.existsSync(url)) url = 'file://' + path.resolve(url); @@ -575,7 +596,7 @@ async function open(options: Options, url: string | undefined, language: string) saveStorage: options.saveStorage, handleSIGINT: false, }); - await openPage(context, url); + await openPageIfNeeded(context, url); } async function codegen(options: Options & { target: string, output?: string, testIdAttribute?: string }, url: string | undefined) { @@ -598,7 +619,7 @@ async function codegen(options: Options & { target: string, output?: string, tes outputFile: outputFile ? path.resolve(outputFile) : undefined, handleSIGINT: false, }); - await openPage(context, url); + await openPageIfNeeded(context, url); } async function waitForPage(page: Page, captureOptions: CaptureOptions) { @@ -615,7 +636,7 @@ async function waitForPage(page: Page, captureOptions: CaptureOptions) { async function screenshot(options: Options, captureOptions: CaptureOptions, url: string, path: string) { const { context } = await launchContext(options, { headless: true }); console.log('Navigating to ' + url); - const page = await openPage(context, url); + const page = await openPageIfNeeded(context, url); await waitForPage(page, captureOptions); console.log('Capturing screenshot into ' + path); await page.screenshot({ path, fullPage: !!captureOptions.fullPage }); @@ -628,7 +649,7 @@ async function pdf(options: Options, captureOptions: CaptureOptions, url: string throw new Error('PDF creation is only working with Chromium'); const { context } = await launchContext({ ...options, browser: 'chromium' }, { headless: true }); console.log('Navigating to ' + url); - const page = await openPage(context, url); + const page = await openPageIfNeeded(context, url); await waitForPage(page, captureOptions); console.log('Saving as pdf into ' + path); await page.pdf!({ path, format: captureOptions.paperFormat }); diff --git a/tests/installation/playwright-cli.spec.ts b/tests/installation/playwright-cli.spec.ts index 02e65a0fe8e2b..e03993f34216d 100755 --- a/tests/installation/playwright-cli.spec.ts +++ b/tests/installation/playwright-cli.spec.ts @@ -16,6 +16,7 @@ import { test, expect } from './npmTest'; import path from 'path'; import fs from 'fs'; +import os from 'os'; test('cli should work', async ({ exec, tmpWorkspace }) => { await exec('npm i playwright'); @@ -31,6 +32,23 @@ test('cli should work', async ({ exec, tmpWorkspace }) => { expect(result).toContain(`{ page }`); }); + await test.step('codegen with user data dir', async () => { + const userDataDir = await fs.promises.mkdtemp(path.join(os.tmpdir(), 'playwright-test-')); + + try { + const result = await exec(`npx playwright codegen --user-data-dir ${userDataDir} https://playwright.dev`, { + env: { + PWTEST_CLI_IS_UNDER_TEST: '1', + PWTEST_CLI_AUTO_EXIT_WHEN: `goto('https://playwright.dev/')`, + } + }); + expect(fs.readdirSync(userDataDir).length).toBeGreaterThan(0); + expect(result).toContain(`{ page }`); + } finally { + fs.rmdirSync(userDataDir, { recursive: true }); + } + }); + await test.step('codegen --target=javascript', async () => { const result = await exec('npx playwright codegen --target=javascript', { env: {