diff --git a/README.md b/README.md
index c194fd48d7963..49d976fe792df 100644
--- a/README.md
+++ b/README.md
@@ -1,6 +1,6 @@
# 🎠Playwright
-[](https://www.npmjs.com/package/playwright) [](https://www.chromium.org/Home) [](https://www.mozilla.org/en-US/firefox/new/) [](https://webkit.org/) [](https://aka.ms/playwright/discord)
+[](https://www.npmjs.com/package/playwright) [](https://www.chromium.org/Home) [](https://www.mozilla.org/en-US/firefox/new/) [](https://webkit.org/) [](https://aka.ms/playwright/discord)
## [Documentation](https://playwright.dev) | [API reference](https://playwright.dev/docs/api/class-playwright)
@@ -10,7 +10,7 @@ Playwright is a framework for Web Testing and Automation. It allows testing [Chr
| :--- | :---: | :---: | :---: |
| Chromium1 146.0.7680.31 | :white_check_mark: | :white_check_mark: | :white_check_mark: |
| WebKit 26.0 | :white_check_mark: | :white_check_mark: | :white_check_mark: |
-| Firefox 146.0.1 | :white_check_mark: | :white_check_mark: | :white_check_mark: |
+| Firefox 148.0.2 | :white_check_mark: | :white_check_mark: | :white_check_mark: |
Headless execution is supported for all browsers on all platforms. Check out [system requirements](https://playwright.dev/docs/intro#system-requirements) for details.
diff --git a/packages/playwright-core/browsers.json b/packages/playwright-core/browsers.json
index 9441b3b6101e1..d994050724c3e 100644
--- a/packages/playwright-core/browsers.json
+++ b/packages/playwright-core/browsers.json
@@ -31,16 +31,16 @@
},
{
"name": "firefox",
- "revision": "1509",
+ "revision": "1511",
"installByDefault": true,
- "browserVersion": "146.0.1",
+ "browserVersion": "148.0.2",
"title": "Firefox"
},
{
"name": "firefox-beta",
- "revision": "1504",
+ "revision": "1505",
"installByDefault": false,
- "browserVersion": "146.0b8",
+ "browserVersion": "148.0b9",
"title": "Firefox Beta"
},
{
diff --git a/packages/playwright-core/src/remote/playwrightConnection.ts b/packages/playwright-core/src/remote/playwrightConnection.ts
index 53d7c46521a90..a9b46700650e6 100644
--- a/packages/playwright-core/src/remote/playwrightConnection.ts
+++ b/packages/playwright-core/src/remote/playwrightConnection.ts
@@ -110,6 +110,8 @@ export class PlaywrightConnection {
}
private async _onDisconnect(error?: Error) {
+ if (this._disconnected)
+ return;
this._disconnected = true;
debugLogger.log('server', `[${this._id}] disconnected. error: ${error}`);
await this._root.stopPendingOperations(new Error('Disconnected')).catch(() => {});
diff --git a/packages/playwright-core/src/server/browserContext.ts b/packages/playwright-core/src/server/browserContext.ts
index 7d9cfe12a52da..9ff01936ce8ab 100644
--- a/packages/playwright-core/src/server/browserContext.ts
+++ b/packages/playwright-core/src/server/browserContext.ts
@@ -19,7 +19,7 @@ import fs from 'fs';
import path from 'path';
import { createGuid } from './utils/crypto';
-import { debugMode } from './utils/debug';
+import { debugMode, isUnderTest } from './utils/debug';
import { Clock } from './clock';
import { Debugger } from './debugger';
import { DialogManager } from './dialog';
@@ -149,18 +149,21 @@ export abstract class BrowserContext extends Sdk
// Debugger will pause execution upon page.pause in headed mode.
this._debugger = new Debugger(this);
- // When PWDEBUG=1, show inspector for each context.
- if (debugMode() === 'inspector')
- await RecorderApp.show(this, { pauseOnNextStatement: true });
-
// When paused, show inspector.
- if (this._debugger.isPaused())
- RecorderApp.showInspectorNoReply(this);
+ const shouldEnableDebugger = !this.attribution.playwright.options.isServer && (isUnderTest() || !!this._browser.options.headful);
+ if (shouldEnableDebugger) {
+ this._debugger.setPauseAt();
+ this._debugger.on(Debugger.Events.PausedStateChanged, () => {
+ if (this._debugger.isPaused())
+ RecorderApp.showInspectorNoReply(this);
+ });
+ }
- this._debugger.on(Debugger.Events.PausedStateChanged, () => {
- if (this._debugger.isPaused())
- RecorderApp.showInspectorNoReply(this);
- });
+ // When PWDEBUG=1, show inspector for each context.
+ if (debugMode() === 'inspector') {
+ this._debugger.setPauseAt({ next: true });
+ await RecorderApp.show(this, { pauseOnNextStatement: true });
+ }
if (debugMode() === 'console')
await this.exposeConsoleApi();
diff --git a/packages/playwright-core/src/server/debugger.ts b/packages/playwright-core/src/server/debugger.ts
index d8f76990d631d..63e27358029f9 100644
--- a/packages/playwright-core/src/server/debugger.ts
+++ b/packages/playwright-core/src/server/debugger.ts
@@ -15,7 +15,7 @@
*/
import { SdkObject } from './instrumentation';
-import { debugMode, isUnderTest, monotonicTime } from '../utils';
+import { monotonicTime } from '../utils';
import { BrowserContext } from './browserContext';
import { methodMetainfo } from '../utils/isomorphic/protocolMetainfo';
@@ -28,7 +28,7 @@ type PauseAt = { next?: boolean, location?: { file: string, line?: number, colum
export class Debugger extends SdkObject implements InstrumentationListener {
private _pauseAt: PauseAt = {};
private _pausedCallsMetadata = new Map void, sdkObject: SdkObject }>();
- private _enabled: boolean;
+ private _enabled = false;
private _context: BrowserContext;
static Events = {
@@ -40,9 +40,6 @@ export class Debugger extends SdkObject implements InstrumentationListener {
super(context, 'debugger');
this._context = context;
(this._context as any)[symbol] = this;
- this._enabled = !context.attribution.playwright.options.isServer && (isUnderTest() || !!context._browser.options.headful);
- if (debugMode() === 'inspector')
- this.setPauseAt({ next: true });
context.instrumentation.addListener(this, context);
this._context.once(BrowserContext.Events.Close, () => {
this._context.instrumentation.removeListener(this);
diff --git a/packages/playwright-core/src/server/deviceDescriptorsSource.json b/packages/playwright-core/src/server/deviceDescriptorsSource.json
index 4f113eb52ee60..40d56122332a7 100644
--- a/packages/playwright-core/src/server/deviceDescriptorsSource.json
+++ b/packages/playwright-core/src/server/deviceDescriptorsSource.json
@@ -1702,7 +1702,7 @@
"defaultBrowserType": "chromium"
},
"Desktop Firefox HiDPI": {
- "userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:146.0.1) Gecko/20100101 Firefox/146.0.1",
+ "userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:148.0.2) Gecko/20100101 Firefox/148.0.2",
"screen": {
"width": 1792,
"height": 1120
@@ -1762,7 +1762,7 @@
"defaultBrowserType": "chromium"
},
"Desktop Firefox": {
- "userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:146.0.1) Gecko/20100101 Firefox/146.0.1",
+ "userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:148.0.2) Gecko/20100101 Firefox/148.0.2",
"screen": {
"width": 1920,
"height": 1080
diff --git a/packages/playwright-core/src/server/electron/electron.ts b/packages/playwright-core/src/server/electron/electron.ts
index 3bdf770ae9fe6..9df97f097a9f4 100644
--- a/packages/playwright-core/src/server/electron/electron.ts
+++ b/packages/playwright-core/src/server/electron/electron.ts
@@ -156,8 +156,8 @@ export class Electron extends SdkObject {
async launch(progress: Progress, options: Omit): Promise {
let app: ElectronApplication | undefined = undefined;
- // --inspect=0 must be the last playwright's argument, loader.ts relies on it.
- let electronArguments = ['--inspect=0', ...(options.args || [])];
+ // --remote-debugging-port=0 must be the last playwright's argument, loader.ts relies on it.
+ let electronArguments = ['--inspect=0', '--remote-debugging-port=0', ...(options.args || [])];
if (os.platform() === 'linux') {
if (!options.chromiumSandbox && electronArguments.indexOf('--no-sandbox') === -1)
diff --git a/packages/playwright-core/src/server/electron/loader.ts b/packages/playwright-core/src/server/electron/loader.ts
index 534fc5300fb46..c3aee60db97b3 100644
--- a/packages/playwright-core/src/server/electron/loader.ts
+++ b/packages/playwright-core/src/server/electron/loader.ts
@@ -21,9 +21,7 @@ const { chromiumSwitches } = require('../chromium/chromiumSwitches');
// Always pass user arguments first, see https://github.com/microsoft/playwright/issues/16614 and
// https://github.com/microsoft/playwright/issues/29198.
// [Electron, -r, loader.js[, --no-sandbox>], --inspect=0, --remote-debugging-port=0, ...args]
-process.argv.splice(1, process.argv.indexOf('--inspect=0'));
-
-app.commandLine.appendSwitch('remote-debugging-port', '0');
+process.argv.splice(1, process.argv.indexOf('--remote-debugging-port=0'));
for (const arg of chromiumSwitches()) {
const match = arg.match(/--([^=]*)=?(.*)/)!;
diff --git a/packages/playwright-core/src/tools/cli-client/program.ts b/packages/playwright-core/src/tools/cli-client/program.ts
index 97320860b9c3c..cba03b08ee44d 100644
--- a/packages/playwright-core/src/tools/cli-client/program.ts
+++ b/packages/playwright-core/src/tools/cli-client/program.ts
@@ -23,7 +23,7 @@ import crypto from 'crypto';
import fs from 'fs';
import os from 'os';
import path from 'path';
-import { createClientInfo, Registry, resolveSessionName } from './registry';
+import { createClientInfo, explicitSessionName, Registry, resolveSessionName } from './registry';
import { Session, renderResolvedConfig } from './session';
import { serverRegistry } from '../../serverRegistry';
@@ -43,6 +43,7 @@ type GlobalOptions = {
};
type OpenOptions = {
+ attach?: string;
browser?: string;
config?: string;
extension?: boolean;
@@ -52,6 +53,7 @@ type OpenOptions = {
};
const globalOptions: (keyof (GlobalOptions & OpenOptions))[] = [
+ 'attach',
'browser',
'config',
'extension',
@@ -148,13 +150,15 @@ export async function program(options?: { embedderVersion?: string}) {
return;
}
case 'open': {
- const entry = registry.entry(clientInfo, sessionName);
- if (entry)
- await new Session(entry).stop(true);
-
- await Session.startDaemon(clientInfo, args);
- const newEntry = await registry.loadEntry(clientInfo, sessionName);
- await runInSession(newEntry, clientInfo, args);
+ await startSession(sessionName, registry, clientInfo, args);
+ return;
+ }
+ case 'attach': {
+ const attachTarget = args._[1];
+ const attachSessionName = explicitSessionName(args.session) ?? attachTarget;
+ args.attach = attachTarget;
+ args.session = attachSessionName;
+ await startSession(attachSessionName, registry, clientInfo, args);
return;
}
case 'close':
@@ -194,6 +198,16 @@ export async function program(options?: { embedderVersion?: string}) {
}
}
+async function startSession(sessionName: string, registry: Registry, clientInfo: ClientInfo, args: MinimistArgs) {
+ const entry = registry.entry(clientInfo, sessionName);
+ if (entry)
+ await new Session(entry).stop(true);
+
+ await Session.startDaemon(clientInfo, args);
+ const newEntry = await registry.loadEntry(clientInfo, sessionName);
+ await runInSession(newEntry, clientInfo, args);
+}
+
async function runInSession(entry: SessionFile, clientInfo: ClientInfo, args: MinimistArgs) {
for (const globalOption of globalOptions)
delete args[globalOption];
@@ -413,7 +427,7 @@ async function gcAndPrintBrowserSessions(workspace: string, list: BrowserDescrip
text.push(`- browser "${descriptor.title}":`);
text.push(` - browser: ${descriptor.browser.browserName}`);
text.push(` - version: v${descriptor.playwrightVersion}`);
- text.push(` - run \`playwright-cli open --attach "${descriptor.title}"\` to attach`);
+ text.push(` - run \`playwright-cli attach "${descriptor.title}"\` to attach`);
console.log(text.join('\n'));
}
diff --git a/packages/playwright-core/src/tools/cli-client/registry.ts b/packages/playwright-core/src/tools/cli-client/registry.ts
index 0a9f8d359070d..018b8de7caa59 100644
--- a/packages/playwright-core/src/tools/cli-client/registry.ts
+++ b/packages/playwright-core/src/tools/cli-client/registry.ts
@@ -186,6 +186,10 @@ const daemonProfilesDir = (workspaceDirHash: string) => {
return path.join(baseDaemonDir, workspaceDirHash);
};
+export function explicitSessionName(sessionName?: string): string | undefined {
+ return sessionName || process.env.PLAYWRIGHT_CLI_SESSION;
+}
+
export function resolveSessionName(sessionName?: string): string {
if (sessionName)
return sessionName;
diff --git a/packages/playwright-core/src/tools/cli-client/session.ts b/packages/playwright-core/src/tools/cli-client/session.ts
index c0af5595a5d86..2702c346efd17 100644
--- a/packages/playwright-core/src/tools/cli-client/session.ts
+++ b/packages/playwright-core/src/tools/cli-client/session.ts
@@ -200,7 +200,12 @@ export class Session {
child.stdout!.destroy();
child.unref();
- console.log(`### Browser \`${sessionName}\` opened with pid ${child.pid}.`);
+ if (cliArgs['attach']) {
+ console.log(`### Session \`${sessionName}\` created, attached to \`${cliArgs['attach']}\`.`);
+ console.log(`Run commands with: playwright --session=${sessionName} `);
+ } else {
+ console.log(`### Browser \`${sessionName}\` opened with pid ${child.pid}.`);
+ }
}
private async _stopDaemon(): Promise {
diff --git a/packages/playwright-core/src/tools/cli-daemon/commands.ts b/packages/playwright-core/src/tools/cli-daemon/commands.ts
index 06b28a0cd1ce2..69dfc9439f17d 100644
--- a/packages/playwright-core/src/tools/cli-daemon/commands.ts
+++ b/packages/playwright-core/src/tools/cli-daemon/commands.ts
@@ -55,12 +55,26 @@ const open = declareCommand({
headed: z.boolean().optional().describe('Run browser in headed mode'),
persistent: z.boolean().optional().describe('Use persistent browser profile'),
profile: z.string().optional().describe('Use persistent browser profile, store profile in specified directory.'),
- attach: z.string().optional().describe('Attach to a running Playwright browser by name or endpoint'),
}),
toolName: ({ url }) => url ? 'browser_navigate' : 'browser_snapshot',
toolParams: ({ url }) => ({ url: url || 'about:blank' }),
});
+const attach = declareCommand({
+ name: 'attach',
+ description: 'Attach to a running Playwright browser',
+ category: 'core',
+ args: z.object({
+ name: z.string().describe('Name or endpoint of the browser to attach to'),
+ }),
+ options: z.object({
+ config: z.string().optional().describe('Path to the configuration file, defaults to .playwright/cli.config.json'),
+ session: z.string().optional().describe('Session name alias (defaults to the attach target name)'),
+ }),
+ toolName: 'browser_snapshot',
+ toolParams: () => ({}),
+});
+
const close = declareCommand({
name: 'close',
description: 'Close the browser',
@@ -888,6 +902,7 @@ const tray = declareCommand({
const commandsArray: AnyCommandSchema[] = [
// core category
open,
+ attach,
close,
goto,
type,
diff --git a/packages/playwright/src/index.ts b/packages/playwright/src/index.ts
index 38346b9774894..06bf458ad962e 100644
--- a/packages/playwright/src/index.ts
+++ b/packages/playwright/src/index.ts
@@ -237,7 +237,7 @@ const playwrightFixtures: Fixtures = ({
testInfo.snapshotSuffix = process.platform;
testInfo._onCustomMessageCallback = () => Promise.reject(new Error('Only tests that use default Playwright context or page fixture support test_debug'));
if (debugMode() === 'inspector')
- (testInfo as TestInfoImpl)._setDebugMode();
+ (testInfo as TestInfoImpl)._setIgnoreTimeouts(true);
playwright._defaultContextTimeout = actionTimeout || 0;
playwright._defaultContextNavigationTimeout = navigationTimeout || 0;
@@ -256,6 +256,7 @@ const playwrightFixtures: Fixtures = ({
await artifactsRecorder.willStartTest(testInfo as TestInfoImpl);
const tracingGroupSteps: TestStepInternal[] = [];
+ const pausedContexts = new Set();
const csiListener: ClientInstrumentationListener = {
onApiCallBegin: (data, channel) => {
const testInfo = currentTestInfo();
@@ -304,7 +305,7 @@ const playwrightFixtures: Fixtures = ({
},
onWillPause: ({ keepTestTimeout }) => {
if (!keepTestTimeout)
- currentTestInfo()?._setDebugMode();
+ currentTestInfo()?._setIgnoreTimeouts(true);
},
runBeforeCreateBrowserContext: async (options: BrowserContextOptions) => {
for (const [key, value] of Object.entries(_combinedContextOptions)) {
@@ -319,6 +320,17 @@ const playwrightFixtures: Fixtures = ({
}
},
runAfterCreateBrowserContext: async (context: BrowserContextImpl) => {
+ context.debugger.on('pausedstatechanged', () => {
+ const paused = context.debugger.pausedDetails().length > 0;
+ if (pausedContexts.has(context) && !paused) {
+ pausedContexts.delete(context);
+ (testInfo as TestInfoImpl)._setIgnoreTimeouts(false);
+ } else if (!pausedContexts.has(context) && paused) {
+ pausedContexts.add(context);
+ (testInfo as TestInfoImpl)._setIgnoreTimeouts(true);
+ }
+ });
+
await artifactsRecorder.didCreateBrowserContext(context);
const testInfo = currentTestInfo();
if (testInfo)
diff --git a/packages/playwright/src/isomorphic/testServerConnection.ts b/packages/playwright/src/isomorphic/testServerConnection.ts
index aa4313553ecaf..e87c0119b5b3c 100644
--- a/packages/playwright/src/isomorphic/testServerConnection.ts
+++ b/packages/playwright/src/isomorphic/testServerConnection.ts
@@ -22,9 +22,6 @@ import type * as reporterTypes from '../../types/testReporter';
// -- Reuse boundary -- Everything below this line is reused in the vscode extension.
export class TestServerConnectionClosedError extends Error {
- constructor() {
- super('Test server connection closed');
- }
}
export interface TestServerTransport {
@@ -86,7 +83,7 @@ export class TestServerConnection implements TestServerInterface, TestServerInte
private _lastId = 0;
private _transport: TestServerTransport;
- private _callbacks = new Map void, reject: (arg: Error) => void }>();
+ private _callbacks = new Map void, reject: (arg: Error) => void, error: TestServerConnectionClosedError }>();
private _connectedPromise: Promise;
private _isClosed = false;
@@ -125,7 +122,7 @@ export class TestServerConnection implements TestServerInterface, TestServerInte
this._onCloseEmitter.fire();
clearInterval(pingInterval);
for (const callback of this._callbacks.values())
- callback.reject(new TestServerConnectionClosedError());
+ callback.reject(callback.error);
this._callbacks.clear();
});
}
@@ -141,9 +138,11 @@ export class TestServerConnection implements TestServerInterface, TestServerInte
await this._connectedPromise;
const id = ++this._lastId;
const message = { id, method, params };
+ // Capture proper stack trace in the error here.
+ const error = new TestServerConnectionClosedError(`${method}: test server connection closed`);
this._transport.send(JSON.stringify(message));
return new Promise((resolve, reject) => {
- this._callbacks.set(id, { resolve, reject });
+ this._callbacks.set(id, { resolve, reject, error });
});
}
diff --git a/packages/playwright/src/worker/testInfo.ts b/packages/playwright/src/worker/testInfo.ts
index 1a5a6f3091055..3dca10c7fac30 100644
--- a/packages/playwright/src/worker/testInfo.ts
+++ b/packages/playwright/src/worker/testInfo.ts
@@ -134,6 +134,7 @@ export class TestInfoImpl implements TestInfo {
errors: ipc.TestInfoErrorImpl[] = [];
readonly _attachmentsPush: (...items: TestInfo['attachments']) => number;
private _workerParams: ipc.WorkerInitParams;
+ private _ignoreTimeoutsCounter = 0;
get error(): ipc.TestInfoErrorImpl | undefined {
return this.errors[0];
@@ -201,7 +202,7 @@ export class TestInfoImpl implements TestInfo {
this._timeoutManager = new TimeoutManager(this.project.timeout);
if (configInternal.configCLIOverrides.debug)
- this._setDebugMode();
+ this._setIgnoreTimeouts(true);
this.outputDir = (() => {
const relativeTestFilePath = path.relative(this.project.testDir, this._requireFile.replace(/\.(spec|test)\.(js|ts|jsx|tsx|mjs|mts|cjs|cts)$/, ''));
@@ -462,8 +463,9 @@ export class TestInfoImpl implements TestInfo {
return ['beforeAll', 'afterAll', 'beforeEach', 'afterEach'].includes(type) ? type : undefined;
}
- _setDebugMode() {
- this._timeoutManager.setIgnoreTimeouts();
+ _setIgnoreTimeouts(ignoreTimeouts: boolean) {
+ this._ignoreTimeoutsCounter += ignoreTimeouts ? 1 : -1;
+ this._timeoutManager.setIgnoreTimeouts(this._ignoreTimeoutsCounter > 0);
}
async _didFinishTestFunction() {
diff --git a/packages/playwright/src/worker/timeoutManager.ts b/packages/playwright/src/worker/timeoutManager.ts
index 5d930f533897b..ec4f0fb691d2d 100644
--- a/packages/playwright/src/worker/timeoutManager.ts
+++ b/packages/playwright/src/worker/timeoutManager.ts
@@ -62,10 +62,17 @@ export class TimeoutManager {
this._defaultSlot = { timeout, elapsed: 0 };
}
- setIgnoreTimeouts() {
- this._ignoreTimeouts = true;
- if (this._running)
+ setIgnoreTimeouts(ignoreTimeouts: boolean) {
+ if (this._ignoreTimeouts === ignoreTimeouts)
+ return;
+ this._ignoreTimeouts = ignoreTimeouts;
+ if (this._running) {
+ if (ignoreTimeouts)
+ this._running.slot.elapsed += monotonicTime() - this._running.start;
+ else
+ this._running.start = monotonicTime();
this._updateTimeout(this._running);
+ }
}
interrupt() {
diff --git a/tests/library/popup.spec.ts b/tests/library/popup.spec.ts
index 521dd497ac256..16b98a7e56fc7 100644
--- a/tests/library/popup.spec.ts
+++ b/tests/library/popup.spec.ts
@@ -99,7 +99,8 @@ it('should inherit http credentials from browser context', async function({ brow
await context.close();
});
-it('should inherit touch support from browser context', async function({ browser, server }) {
+it('should inherit touch support from browser context', async function({ browser, server, browserName, browserMajorVersion }) {
+ it.fixme(browserName === 'firefox' && browserMajorVersion >= 148, 'https://bugzilla.mozilla.org/show_bug.cgi?id=2014330');
const context = await browser.newContext({
viewport: { width: 400, height: 500 },
hasTouch: true
diff --git a/tests/mcp/cli-session.spec.ts b/tests/mcp/cli-session.spec.ts
index cb596539714f2..081099bd2e0fc 100644
--- a/tests/mcp/cli-session.spec.ts
+++ b/tests/mcp/cli-session.spec.ts
@@ -295,7 +295,7 @@ workspace1:
- browser "foobar":
- browser: ${/* FIX browser._options */ mcpBrowser.replace('chrome', 'chromium')}
- version: ${version}
- - run \`playwright-cli open --attach "foobar"\` to attach`);
+ - run \`playwright-cli attach "foobar"\` to attach`);
});
test('attach to browser server', async ({ cli, mcpBrowser }) => {
@@ -304,31 +304,24 @@ workspace1:
await (browser as any)._register('foobar', { workspaceDir: 'workspace1' });
const page = await browser.newPage();
await page.setContent('My Page');
- const { output: openOutput } = await cli('open', '--attach=foobar');
- expect(openOutput).toContain('### Browser `default` opened with pid');
- expect(openOutput).toContain('My Page');
+ const { output: openOutput } = await cli('attach', 'foobar');
+ expect(openOutput).toContain('### Session `foobar` created, attached to `foobar`.');
+ expect(openOutput).toContain('Run commands with: playwright --session=foobar ');
const { output: listOutput } = await cli('list', '--all');
expect(listOutput).toBe(`### Browsers
/:
-- default:
+- foobar:
- status: open
- browser-type: ${/* FIX browser._options */ mcpBrowser.replace('chrome', 'chromium')}
- user-data-dir:
- - headed: true
-
-### Browser servers available for attach
-workspace1:
-- browser "foobar":
- - browser: ${/* FIX browser._options */ mcpBrowser.replace('chrome', 'chromium')}
- - version: ${version}
- - run \`playwright-cli open --attach "foobar"\` to attach`);
+ - headed: true`);
});
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)._register('foobar', { workspaceDir: 'workspace1' });
- const { error } = await cli('open', '--attach=foobar');
+ const { error } = await cli('attach', 'foobar');
expect(error).toContain('Error: unable to connect to a browser that does not have any contexts');
});
@@ -352,21 +345,33 @@ workspace1:
- headed: true`);
});
+ test('attach with session alias', async ({ cli, mcpBrowser }) => {
+ const browserName = mcpBrowser.replace('chrome', 'chromium');
+ await using browser = await playwright[browserName].launch({ headless: true });
+ await (browser as any)._register('foobar', { workspaceDir: 'workspace1' });
+ const page = await browser.newPage();
+ await page.setContent('Alias Page');
+ const { output: openOutput } = await cli('attach', 'foobar', '--session=mybrowser');
+ expect(openOutput).toContain('### Session `mybrowser` created, attached to `foobar`.');
+ expect(openOutput).toContain('Run commands with: playwright --session=mybrowser ');
+ await cli('-s', 'mybrowser', 'close');
+ });
+
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)._register('foobar', { workspaceDir: 'workspace1' });
- const { output: openOutput } = await cli('open', '--attach=foobar');
- expect(openOutput).toContain('### Browser `default` opened with pid');
- await cli('close');
+ const { output: openOutput } = await cli('attach', 'foobar');
+ expect(openOutput).toContain('Session `foobar` created, attached to `foobar`');
+ await cli('-s', 'foobar', 'close');
const { output: listOutput } = await cli('list', '--all');
expect(listOutput).toBe(`### Browser servers available for attach
workspace1:
- browser \"foobar\":
- browser: ${/* FIX browser._options */ mcpBrowser.replace('chrome', 'chromium')}
- version: ${version}
- - run \`playwright-cli open --attach \"foobar\"\` to attach`);
+ - run \`playwright-cli attach \"foobar\"\` to attach`);
});
});
diff --git a/tests/mcp/cli-test.spec.ts b/tests/mcp/cli-test.spec.ts
index 704db0b741993..05dbd653a09f2 100644
--- a/tests/mcp/cli-test.spec.ts
+++ b/tests/mcp/cli-test.spec.ts
@@ -20,7 +20,7 @@ import { writeFiles } from './fixtures';
const testEntrypoint = path.join(__dirname, '../../packages/playwright-test/cli.js');
-test('debug test and snapshot', async ({ cliEnv, cli, childProcess }) => {
+test.skip('debug test and snapshot', async ({ cliEnv, cli, childProcess }) => {
await writeFiles({
'subdir/a.test.ts': `
import { test, expect } from '@playwright/test';
diff --git a/tests/page/page-wait-for-load-state.spec.ts b/tests/page/page-wait-for-load-state.spec.ts
index b23754b84b90b..95d03275c00ac 100644
--- a/tests/page/page-wait-for-load-state.spec.ts
+++ b/tests/page/page-wait-for-load-state.spec.ts
@@ -70,7 +70,7 @@ it('should work with pages that have loaded before being connected to', async ({
expect(popup.url()).toBe(server.EMPTY_PAGE);
});
-it('should wait for load state of empty url popup', async ({ page, browserName, isBidi }) => {
+it('should wait for load state of empty url popup', async ({ page, browserName, isBidi, browserMajorVersion }) => {
const [popup, readyState] = await Promise.all([
page.waitForEvent('popup'),
page.evaluate(() => {
@@ -79,8 +79,9 @@ it('should wait for load state of empty url popup', async ({ page, browserName,
}),
]);
await popup.waitForLoadState();
- expect(readyState).toBe(browserName === 'firefox' && !isBidi ? 'uninitialized' : 'complete');
- expect(await popup.evaluate(() => document.readyState)).toBe(browserName === 'firefox' && !isBidi ? 'uninitialized' : 'complete');
+ const isOldFirefox = browserName === 'firefox' && browserMajorVersion < 148;
+ expect(readyState).toBe(isOldFirefox && !isBidi ? 'uninitialized' : 'complete');
+ expect(await popup.evaluate(() => document.readyState)).toBe(isOldFirefox && !isBidi ? 'uninitialized' : 'complete');
});
it('should wait for load state of about:blank popup ', async ({ page }) => {
diff --git a/tests/playwright-test/playwright.spec.ts b/tests/playwright-test/playwright.spec.ts
index 52300f66f0e5c..1d2fd3b665c3f 100644
--- a/tests/playwright-test/playwright.spec.ts
+++ b/tests/playwright-test/playwright.spec.ts
@@ -963,3 +963,22 @@ test('init script should not observe playwright internals', async ({ server, run
}, {}, { PWDEBUG: '0' });
expect(result.exitCode).toBe(0);
});
+
+test('should pause test timeout while on pause', async ({ runInlineTest }) => {
+ const result = await runInlineTest({
+ 'a.test.ts': `
+ import { test, expect } from '@playwright/test';
+
+ test('test', async ({ page, context }) => {
+ await context.debugger.setPauseAt({ next: true });
+ const paused = new Promise(f => context.debugger.once('pausedstatechanged', f));
+ const contentPromise = page.setContent('hello
');
+ await paused;
+ await new Promise(f => setTimeout(f, 5000));
+ await context.debugger.resume();
+ await contentPromise;
+ });
+ `,
+ }, { timeout: 3000 });
+ expect(result.exitCode).toBe(0);
+});