Skip to content

feat(codegen): add user-data-dir option #35814

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 13 commits into
base: main
Choose a base branch
from
20 changes: 20 additions & 0 deletions docs/src/codegen.md
Original file line number Diff line number Diff line change
Expand Up @@ -414,6 +414,26 @@ pwsh bin/Debug/netX/playwright.ps1 codegen --load-storage=auth.json github.com/m

<img width="1394" alt="github signed in showing use of load storage scharp" src="https://user-images.githubusercontent.com/13063165/220928354-caa0e958-fe09-4125-9b54-67483064da51.png" />

#### 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.
Expand Down
107 changes: 64 additions & 43 deletions packages/playwright-core/src/cli/program.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -70,6 +69,7 @@ commandWithOpenOptions('codegen [url]', 'open page and generate code for user ac
['-o, --output <file name>', 'saves the generated script to a file'],
['--target <language>', `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 <attributeName>', 'use the specified attribute to generate data test ID selectors'],
['--user-data-dir <directory>', 'use the specified user data directory instead of a new context'],
]).action(function(url, options) {
codegen(options, url).catch(logErrorAndExit);
}).addHelpText('afterAll', `
Expand Down Expand Up @@ -361,6 +361,7 @@ type Options = {
timezone?: string;
viewportSize?: string;
userAgent?: string;
userDataDir?: string
};

type CaptureOptions = {
Expand All @@ -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;
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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() {
Expand All @@ -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();
Expand All @@ -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<BrowserContext> {
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<Page> {
const page = await context.newPage();
async function openPageIfNeeded(context: BrowserContext, url: string | undefined): Promise<Page> {
let page = context.pages()[0];
if (!page)
page = await context.newPage();
if (url) {
if (fs.existsSync(url))
url = 'file://' + path.resolve(url);
Expand Down Expand Up @@ -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) {
Expand All @@ -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) {
Expand All @@ -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 });
Expand All @@ -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 });
Expand Down
18 changes: 18 additions & 0 deletions tests/installation/playwright-cli.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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');
Expand All @@ -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: {
Expand Down
Loading