Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -16,16 +16,17 @@ To debug a failing test, run it with Playwright as usual, but set `PWPAUSE=cli`

**IMPORTANT**: run the command in the background and check the output until "Debugging Instructions" is printed.

Once instructions are printed, use `playwright-cli` to explore the page. Debugging instructions include a session name that should be used in `playwright-cli` to connect to the page under test. Do not create a new `playwright-cli` session, make sure to connect to the test session instead.
Once instructions are printed, use `playwright-cli` to explore the page. Debugging instructions include a browser name that should be used in `playwright-cli` to attach to the page under test.

```bash
# Run the test
PLAYWRIGHT_HTML_OPEN=never PWPAUSE=cli npx playwright test
# ...

# Explore the page and interact if needed
playwright-cli --session=test-worker-abcdef snapshot
playwright-cli --session=test-worker-abcdef click e14
playwright-cli --session=test open --attach=test-worker-abcdef
playwright-cli --session=test snapshot
playwright-cli --session=test click e14
```

Keep the test running in the background while you explore and look for a fix. After fixing the test, stop the background test run.
Expand Down
2 changes: 1 addition & 1 deletion packages/playwright-core/src/tools/cli-client/program.ts
Original file line number Diff line number Diff line change
Expand Up @@ -248,7 +248,7 @@ async function ensureConfiguredBrowserInstalled() {
}

async function installBrowser() {
const { program } = require('../program');
const { program } = require('../../cli/program');
const argv = process.argv.map(arg => arg === 'install-browser' ? 'install' : arg);
program.parse(argv);
}
Expand Down
6 changes: 4 additions & 2 deletions packages/playwright-core/src/tools/cli-daemon/program.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,8 @@ program.argument('[session-name]', 'name of the session to create or connect to'
try {
const browser = await createBrowser(mcpConfig, mcpClientInfo);
const browserContext = mcpConfig.browser.isolated ? await browser.newContext(mcpConfig.browser.contextOptions) : browser.contexts()[0];
if (!browserContext)
throw new Error('Error: unable to connect to a browser that does not have any contexts');
const persistent = options.persistent || options.profile || mcpConfig.browser.userDataDir ? true : undefined;
const socketPath = await startCliDaemonServer(sessionName, browserContext, mcpConfig, clientInfo, { persistent, exitOnClose: true });
console.log(`### Success\nDaemon listening on ${socketPath}`);
Expand Down Expand Up @@ -104,9 +106,9 @@ export async function resolveCLIConfig(clientInfo: ClientInfo, sessionName: stri
result = configUtils.mergeConfig(result, envOverrides);

if (result.browser.isolated === undefined)
result.browser.isolated = !options.profile && !options.persistent && !result.browser.userDataDir;
result.browser.isolated = !options.profile && !options.persistent && !result.browser.userDataDir && !result.browser.remoteEndpoint && !result.extension;

if (!result.extension && !result.browser.isolated && !result.browser.userDataDir) {
if (!result.extension && !result.browser.isolated && !result.browser.userDataDir && !result.browser.remoteEndpoint) {
// No custom value provided, use the daemon data dir.
const browserToken = result.browser.launchOptions?.channel ?? result.browser?.browserName;
const userDataDir = path.resolve(clientInfo.daemonProfilesDir, `ud-${sessionName}-${browserToken}`);
Expand Down
6 changes: 3 additions & 3 deletions packages/playwright/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ import { setBoxedStackPrefixes, createGuid, currentZone, debugMode, jsonStringif

import { currentTestInfo } from './common/globals';
import { rootTestType } from './common/testType';
import { createCustomMessageHandler, runDaemonForContext } from './mcp/test/browserBackend';
import { createCustomMessageHandler, runDaemonForBrowser } from './mcp/test/browserBackend';

import type { Fixtures, PlaywrightTestArgs, PlaywrightTestOptions, PlaywrightWorkerArgs, PlaywrightWorkerOptions, ScreenshotMode, TestInfo, TestType, VideoMode } from '../types/test';
import type { ContextReuseMode } from './common/config';
Expand Down Expand Up @@ -432,15 +432,15 @@ const playwrightFixtures: Fixtures<TestFixtures, WorkerFixtures> = ({
if (!_reuseContext) {
const { context, close } = await _contextFactory();
testInfo._onCustomMessageCallback = createCustomMessageHandler(testInfo, context);
testInfo._onDidFinishTestFunctionCallbacks.add(() => runDaemonForContext(testInfo, context));
testInfo._onDidFinishTestFunctionCallbacks.add(() => runDaemonForBrowser(testInfo, browser));
await use(context);
await close();
return;
}

const context = await browserImpl._wrapApiCall(() => browserImpl._newContextForReuse(), { internal: true });
testInfo._onCustomMessageCallback = createCustomMessageHandler(testInfo, context);
testInfo._onDidFinishTestFunctionCallbacks.add(() => runDaemonForContext(testInfo, context));
testInfo._onDidFinishTestFunctionCallbacks.add(() => runDaemonForBrowser(testInfo, browser));
await use(context);
const closeReason = testInfo.status === 'timedOut' ? 'Test timeout of ' + testInfo.timeout + 'ms exceeded.' : 'Test ended.';
await browserImpl._wrapApiCall(() => browserImpl._disconnectFromReusedContext(closeReason), { internal: true });
Expand Down
17 changes: 7 additions & 10 deletions packages/playwright/src/mcp/test/browserBackend.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,14 +14,14 @@
* limitations under the License.
*/

import path from 'path';
import { createGuid } from 'playwright-core/lib/utils';
import * as tools from 'playwright-core/lib/tools/exports';

import { stripAnsiEscapes } from '../../util';

import type * as playwright from '../../../index';
import type { Page } from '../../../../playwright-core/src/client/page';
import type { Browser } from '../../../../playwright-core/src/client/browser';
import type { TestInfoImpl } from '../../worker/testInfo';

export type BrowserMCPRequest = {
Expand Down Expand Up @@ -111,17 +111,12 @@ async function generatePausedMessage(testInfo: TestInfoImpl, context: playwright
return lines.join('\n');
}

export async function runDaemonForContext(testInfo: TestInfoImpl, context: playwright.BrowserContext): Promise<void> {
export async function runDaemonForBrowser(testInfo: TestInfoImpl, browser: playwright.Browser): Promise<void> {
if (process.env.PWPAUSE !== 'cli')
return;

const outputDir = path.join(testInfo.artifactsDir(), '.playwright-mcp');
const sessionName = `test-worker-${createGuid().slice(0, 6)}`;
await tools.startCliDaemonServer(sessionName, context, {
outputMode: 'file',
snapshot: { mode: 'full' },
outputDir,
});
const browserTitle = `test-worker-${createGuid().slice(0, 6)}`;
await (browser as Browser)._startServer(browserTitle, { workspaceDir: testInfo.project.testDir });

const lines = [''];
if (testInfo.errors.length) {
Expand All @@ -133,7 +128,9 @@ export async function runDaemonForContext(testInfo: TestInfoImpl, context: playw
}
lines.push(
`### Debugging Instructions`,
`- Use "playwright-cli --session=${sessionName}" to explore the page and fix the problem.`,
`- Pick a session name, e.g. "test"`,
`- Run "playwright-cli --session=<name> open --attach=${browserTitle}" to attach to this page`,
`- Use "playwright-cli --session=<name>" to explore the page and fix the problem`,
`- Stop this test run when finished. Restart if needed.`,
``,
);
Expand Down
3 changes: 2 additions & 1 deletion tests/library/inspector/recorder-api.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -181,7 +181,8 @@ test('closing page should cancel ongoing pickLocator', async ({ page }) => {
expect(await pickPromise).toContain('Target page, context or browser has been closed');
});

test('page2.pickLocator() should cancel page1.pickLocator()', async ({ page, context }) => {
test('page2.pickLocator() should cancel page1.pickLocator()', async ({ page, context, browserName, headless, isMac, macVersion }) => {
test.fixme(browserName === 'chromium' && !headless && isMac && macVersion === 14, 'times out on chromium headed on macOS 14');
const pick1Promise = page.pickLocator().catch(e => e.message);

const page2 = await context.newPage();
Expand Down
12 changes: 12 additions & 0 deletions tests/mcp/cli-session.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -302,8 +302,11 @@ workspace1:
const browserName = mcpBrowser.replace('chrome', 'chromium');
await using browser = await playwright[browserName].launch({ headless: true });
await (browser as any)._startServer('foobar', { workspaceDir: 'workspace1' });
const page = await browser.newPage();
await page.setContent('<title>My Page</title>');
const { output: openOutput } = await cli('open', '--attach=foobar');
expect(openOutput).toContain('### Browser `default` opened with pid');
expect(openOutput).toContain('My Page');
const { output: listOutput } = await cli('list', '--all');
expect(listOutput).toBe(`### Browsers
/:
Expand All @@ -321,9 +324,18 @@ workspace1:
- run \`playwright-cli open --attach "foobar"\` to attach`);
});

test('fail to attach to browser server without contexts', async ({ cli, mcpBrowser }) => {
const browserName = mcpBrowser.replace('chrome', 'chromium');
await using browser = await playwright[browserName].launch({ headless: true });
await (browser as any)._startServer('foobar', { workspaceDir: 'workspace1' });
const { error } = await cli('open', '--attach=foobar');
expect(error).toContain('Error: unable to connect to a browser that does not have any contexts');
});

test('detach from browser server', async ({ cli, mcpBrowser }) => {
const browserName = mcpBrowser.replace('chrome', 'chromium');
await using browser = await playwright[browserName].launch({ headless: true });
await browser.newPage();
await (browser as any)._startServer('foobar', { workspaceDir: 'workspace1' });
const { output: openOutput } = await cli('open', '--attach=foobar');
expect(openOutput).toContain('### Browser `default` opened with pid');
Expand Down
29 changes: 16 additions & 13 deletions tests/mcp/cli-test.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,39 +22,42 @@ const testEntrypoint = path.join(__dirname, '../../packages/playwright-test/cli.

test('debug test and snapshot', async ({ cliEnv, cli, childProcess }) => {
await writeFiles({
'a.test.ts': `
'subdir/a.test.ts': `
import { test, expect } from '@playwright/test';
test('example test', async ({ page }) => {
await page.setContent('<button>Submit</button>');
await page.setContent('<title>My Page</title><body><button>Submit</button></body>');
await expect(page.getByRole('button', { name: 'Missing' })).toBeVisible({ timeout: 1000 });
});
`,
});

const cwd = test.info().outputDir;

const testProcess = childProcess({
command: [process.argv[0], testEntrypoint, 'test'],
cwd,
cwd: test.info().outputPath('subdir'),
env: { PWPAUSE: 'cli', ...cliEnv },
});

await testProcess.waitForOutput('playwright-cli --session=test-worker');
await testProcess.waitForOutput('playwright-cli --session=<name> open --attach=test-worker');

const listResult1 = await cli('list', { cwd });
expect(listResult1.exitCode).toBe(0);
expect(listResult1.output).toContain('test-worker');
const match = testProcess.output.match(/--attach=([a-zA-Z0-9-_]+)/);
const browserName = match[1];

const match = testProcess.output.match(/--session=([a-zA-Z0-9-_]+)/);
const sessionName = match[1];
const { output: openOutput } = await cli('open', `--session=test-session`, `--attach=${browserName}`);
expect(openOutput).toContain('My Page');

const listResult1 = await cli('list', '--all');
expect(listResult1.exitCode).toBe(0);
expect(listResult1.output).toContain('test-session');
expect(listResult1.output).toContain('subdir');
expect(listResult1.output).toContain(`browser "${browserName}"`);

const snapshotResult = await cli(`--session=${sessionName}`, 'snapshot', { cwd });
const snapshotResult = await cli(`--session=test-session`, 'snapshot');
expect(snapshotResult.exitCode).toBe(0);
expect(snapshotResult.snapshot).toContain('button "Submit"');

await testProcess.kill('SIGINT');

const listResult2 = await cli('list', { cwd });
const listResult2 = await cli('list');
expect(listResult2.exitCode).toBe(0);
expect(listResult2.output).toContain('(no browsers)');
});
Loading