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
+#### 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: {