diff --git a/docs/src/codegen.md b/docs/src/codegen.md index 641383d7b4e0a..7f2358bdaaafd 100644 --- a/docs/src/codegen.md +++ b/docs/src/codegen.md @@ -414,6 +414,30 @@ 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 create a custom browser user data directory, codegen will use this existing browser profile and have access to any authentication state present in that profile. + +:::warning +[As of Chrome 136, the default user data directory cannot be accessed via automated tooling](https://developer.chrome.com/blog/remote-debugging-port), such as Playwright. You must create a separate user data directory for use in testing. +::: + +```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 a1927b912de50..28971acbc50ac 100644 --- a/packages/playwright-core/src/cli/program.ts +++ b/packages/playwright-core/src/cli/program.ts @@ -70,6 +70,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', ` @@ -423,6 +424,7 @@ type Options = { timezone?: string; viewportSize?: string; userAgent?: string; + userDataDir?: string; }; type CaptureOptions = { @@ -472,33 +474,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 { @@ -563,9 +538,43 @@ async function launchContext(options: Options, extraOptions: LaunchOptions): Pro contextOptions.serviceWorkers = 'block'; } - // Close app when the last window closes. + let browser: Browser; + let context: BrowserContext; + + if (options.userDataDir) { + context = await browserType.launchPersistentContext(options.userDataDir, { ...launchOptions, ...contextOptions }); + const persistentBrowser = context.browser(); + assert(persistentBrowser, 'Could not launch persistent browser context'); + browser = persistentBrowser; + } else { + browser = await browserType.launch(launchOptions); + context = await browser.newContext(contextOptions); + } - 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); + }; + 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); + } + }); + } let closingBrowser = false; async function closeBrowser() { @@ -608,8 +617,10 @@ async function launchContext(options: Options, extraOptions: LaunchOptions): Pro return { browser, browserName: browserType.name(), context, contextOptions, launchOptions }; } -async function openPage(context: BrowserContext, url: string | undefined): Promise { - const page = await context.newPage(); +async function openPage(context: BrowserContext, url: string | undefined, allowPageReuse: boolean = false): Promise { + let page = allowPageReuse ? context.pages()[0] : undefined; + if (!page) + page = await context.newPage(); if (url) { if (fs.existsSync(url)) url = 'file://' + path.resolve(url); @@ -660,7 +671,7 @@ async function codegen(options: Options & { target: string, output?: string, tes outputFile: outputFile ? path.resolve(outputFile) : undefined, handleSIGINT: false, }); - await openPage(context, url); + await openPage(context, url, true); } async function waitForPage(page: Page, captureOptions: CaptureOptions) { diff --git a/tests/installation/playwright-cli.spec.ts b/tests/installation/playwright-cli.spec.ts index 02e65a0fe8e2b..de278ac9defeb 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-custom-user-data-dir')); + + try { + const result = await exec(`npx playwright codegen --user-data-dir ${userDataDir} about:blank`, { + env: { + PWTEST_CLI_IS_UNDER_TEST: '1', + PWTEST_CLI_AUTO_EXIT_WHEN: `goto('about:blank')`, + } + }); + 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: {