diff --git a/README.md b/README.md index c194fd48d7963..49d976fe792df 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # 🎭 Playwright -[![npm version](https://img.shields.io/npm/v/playwright.svg)](https://www.npmjs.com/package/playwright) [![Chromium version](https://img.shields.io/badge/chromium-146.0.7680.31-blue.svg?logo=google-chrome)](https://www.chromium.org/Home) [![Firefox version](https://img.shields.io/badge/firefox-146.0.1-blue.svg?logo=firefoxbrowser)](https://www.mozilla.org/en-US/firefox/new/) [![WebKit version](https://img.shields.io/badge/webkit-26.0-blue.svg?logo=safari)](https://webkit.org/) [![Join Discord](https://img.shields.io/badge/join-discord-informational)](https://aka.ms/playwright/discord) +[![npm version](https://img.shields.io/npm/v/playwright.svg)](https://www.npmjs.com/package/playwright) [![Chromium version](https://img.shields.io/badge/chromium-146.0.7680.31-blue.svg?logo=google-chrome)](https://www.chromium.org/Home) [![Firefox version](https://img.shields.io/badge/firefox-148.0.2-blue.svg?logo=firefoxbrowser)](https://www.mozilla.org/en-US/firefox/new/) [![WebKit version](https://img.shields.io/badge/webkit-26.0-blue.svg?logo=safari)](https://webkit.org/) [![Join Discord](https://img.shields.io/badge/join-discord-informational)](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); +});