diff --git a/docs/src/release-notes-js.md b/docs/src/release-notes-js.md index 3347b70b8be27..b6bf6189996a6 100644 --- a/docs/src/release-notes-js.md +++ b/docs/src/release-notes-js.md @@ -41,7 +41,7 @@ export default defineConfig({ webServer: { command: 'npm run start', wait: { - stdout: '/Listening on port (?\\d+)/' + stdout: /Listening on port (?\d+)/ }, }, }); diff --git a/docs/src/test-api/class-testconfig.md b/docs/src/test-api/class-testconfig.md index 1d48d6be17423..a01babad1f23b 100644 --- a/docs/src/test-api/class-testconfig.md +++ b/docs/src/test-api/class-testconfig.md @@ -714,8 +714,8 @@ export default defineConfig({ - `stderr` ?<["pipe"|"ignore"]> Whether to pipe the stderr of the command to the process stderr or ignore it. Defaults to `"pipe"`. - `stdout` ?<["pipe"|"ignore"]> If `"pipe"`, it will pipe the stdout of the command to the process stdout. If `"ignore"`, it will ignore the stdout of the command. Default to `"ignore"`. - `wait` ?<[Object]> Consider command started only when given output has been produced. - - `stdout` ?<[RegExp]> Regular expression to wait for in the `stdout` of the command output. Named capture groups are stored in the environment, for example `/Listening on port (?\\d+)/` will store the port number in `process.env['MY_SERVER_PORT']`. - - `stderr` ?<[RegExp]> Regular expression to wait for in the `stderr` of the command output. Named capture groups are stored in the environment, for example `/Listening on port (?\\d+)/` will store the port number in `process.env['MY_SERVER_PORT']`. + - `stdout` ?<[RegExp]> Regular expression to wait for in the `stdout` of the command output. Named capture groups are stored in the environment, for example `/Listening on port (?\d+)/` will store the port number in `process.env['MY_SERVER_PORT']`. + - `stderr` ?<[RegExp]> Regular expression to wait for in the `stderr` of the command output. Named capture groups are stored in the environment, for example `/Listening on port (?\d+)/` will store the port number in `process.env['MY_SERVER_PORT']`. - `timeout` ?<[int]> How long to wait for the process to start up and be available in milliseconds. Defaults to 60000. - `url` ?<[string]> The url on your http server that is expected to return a 2xx, 3xx, 400, 401, 402, or 403 status code when the server is ready to accept connections. Redirects (3xx status codes) are being followed and the new location is checked. Either `port` or `url` should be specified. @@ -797,7 +797,7 @@ export default defineConfig({ webServer: { command: 'npm run start', wait: { - stdout: '/Listening on port (?\\d+)/' + stdout: /Listening on port (?\d+)/ }, }, }); diff --git a/docs/src/test-webserver-js.md b/docs/src/test-webserver-js.md index 10ff28f4dce8d..9662786689bcf 100644 --- a/docs/src/test-webserver-js.md +++ b/docs/src/test-webserver-js.md @@ -41,7 +41,7 @@ export default defineConfig({ | `stdout` | If `"pipe"`, it will pipe the stdout of the command to the process stdout. If `"ignore"`, it will ignore the stdout of the command. Default to `"ignore"`. | | `timeout` | How long to wait for the process to start up and be available in milliseconds. Defaults to 60000. | | `url`| URL of your http server that is expected to return a 2xx, 3xx, 400, 401, 402, or 403 status code when the server is ready to accept connections. Either `port` or `url` should be specified. | -| `wait` | Consider command started only when given output has been produced. Takes an object with optional `stdout` and/or `stderr` regular expressions. Named capture groups in the regex are stored in the environment, for example `/Listening on port (?\\d+)/` will store the port number in `process.env['MY_SERVER_PORT']`. | +| `wait` | Consider command started only when given output has been produced. Takes an object with optional `stdout` and/or `stderr` regular expressions. Named capture groups in the regex are stored in the environment, for example `/Listening on port (?\d+)/` will store the port number in `process.env['MY_SERVER_PORT']`. | ## Adding a server timeout diff --git a/packages/playwright-core/src/client/page.ts b/packages/playwright-core/src/client/page.ts index ad4cf8001175c..61cc51380e554 100644 --- a/packages/playwright-core/src/client/page.ts +++ b/packages/playwright-core/src/client/page.ts @@ -631,7 +631,8 @@ export class Page extends ChannelOwner implements api.Page async close(options: { runBeforeUnload?: boolean, reason?: string } = {}) { this._closeReason = options.reason; - this._closeWasCalled = true; + if (!options.runBeforeUnload) + this._closeWasCalled = true; try { if (this._ownedContext) await this._ownedContext.close(); diff --git a/packages/playwright-core/src/server/trace/recorder/tracing.ts b/packages/playwright-core/src/server/trace/recorder/tracing.ts index 387d8b9305a87..86a449b76bf9e 100644 --- a/packages/playwright-core/src/server/trace/recorder/tracing.ts +++ b/packages/playwright-core/src/server/trace/recorder/tracing.ts @@ -423,39 +423,39 @@ export class Tracing extends SdkObject implements InstrumentationListener, Snaps return { artifact }; } - async _captureSnapshot(snapshotName: string, sdkObject: SdkObject, metadata: CallMetadata): Promise { - if (!this._snapshotter) + private async _captureSnapshot(snapshotName: string | undefined, sdkObject: SdkObject, metadata: CallMetadata): Promise { + if (!snapshotName || !sdkObject.attribution.page) return; - if (!sdkObject.attribution.page) - return; - if (!this._snapshotter.started()) - return; - if (!shouldCaptureSnapshot(metadata)) - return; - await this._snapshotter.captureSnapshot(sdkObject.attribution.page, metadata.id, snapshotName).catch(() => {}); + await this._snapshotter?.captureSnapshot(sdkObject.attribution.page, metadata.id, snapshotName).catch(() => {}); + } + + private _shouldCaptureSnapshot(sdkObject: SdkObject, metadata: CallMetadata) { + return !!this._snapshotter?.started() && shouldCaptureSnapshot(metadata) && !!sdkObject.attribution.page; } onBeforeCall(sdkObject: SdkObject, metadata: CallMetadata) { - // IMPORTANT: no awaits before this._appendTraceEvent in this method. + // IMPORTANT: no awaits in this method, this._appendTraceEvent must be called synchronously. const event = createBeforeActionTraceEvent(metadata, this._currentGroupId()); if (!event) return Promise.resolve(); sdkObject.attribution.page?.screencast.temporarilyDisableThrottling(); - event.beforeSnapshot = `before@${metadata.id}`; + if (this._shouldCaptureSnapshot(sdkObject, metadata)) + event.beforeSnapshot = `before@${metadata.id}`; this._state?.callIds.add(metadata.id); this._appendTraceEvent(event); return this._captureSnapshot(event.beforeSnapshot, sdkObject, metadata); } onBeforeInputAction(sdkObject: SdkObject, metadata: CallMetadata) { + // IMPORTANT: no awaits in this method, this._appendTraceEvent must be called synchronously. if (!this._state?.callIds.has(metadata.id)) return Promise.resolve(); - // IMPORTANT: no awaits before this._appendTraceEvent in this method. const event = createInputActionTraceEvent(metadata); if (!event) return Promise.resolve(); sdkObject.attribution.page?.screencast.temporarilyDisableThrottling(); - event.inputSnapshot = `input@${metadata.id}`; + if (this._shouldCaptureSnapshot(sdkObject, metadata)) + event.inputSnapshot = `input@${metadata.id}`; this._appendTraceEvent(event); return this._captureSnapshot(event.inputSnapshot, sdkObject, metadata); } @@ -472,15 +472,17 @@ export class Tracing extends SdkObject implements InstrumentationListener, Snaps this._appendTraceEvent(event); } - async onAfterCall(sdkObject: SdkObject, metadata: CallMetadata) { + onAfterCall(sdkObject: SdkObject, metadata: CallMetadata) { + // IMPORTANT: no awaits in this method, this._appendTraceEvent must be called synchronously. if (!this._state?.callIds.has(metadata.id)) - return; + return Promise.resolve(); this._state?.callIds.delete(metadata.id); const event = createAfterActionTraceEvent(metadata); if (!event) - return; + return Promise.resolve(); sdkObject.attribution.page?.screencast.temporarilyDisableThrottling(); - event.afterSnapshot = `after@${metadata.id}`; + if (this._shouldCaptureSnapshot(sdkObject, metadata)) + event.afterSnapshot = `after@${metadata.id}`; this._appendTraceEvent(event); return this._captureSnapshot(event.afterSnapshot, sdkObject, metadata); } diff --git a/packages/playwright/src/mcp/browser/browserContextFactory.ts b/packages/playwright/src/mcp/browser/browserContextFactory.ts index f751922c6d7f8..b064f49785e8a 100644 --- a/packages/playwright/src/mcp/browser/browserContextFactory.ts +++ b/packages/playwright/src/mcp/browser/browserContextFactory.ts @@ -152,7 +152,10 @@ class CdpContextFactory extends BaseContextFactory { } protected override async _doObtainBrowser(): Promise { - return playwright.chromium.connectOverCDP(this.config.browser.cdpEndpoint!, { headers: this.config.browser.cdpHeaders }); + return playwright.chromium.connectOverCDP(this.config.browser.cdpEndpoint!, { + headers: this.config.browser.cdpHeaders, + timeout: this.config.browser.cdpTimeout + }); } protected override async _doCreateContext(browser: playwright.Browser): Promise { diff --git a/packages/playwright/src/mcp/config.d.ts b/packages/playwright/src/mcp/config.d.ts index 98706d634c096..bfe28a48483f2 100644 --- a/packages/playwright/src/mcp/config.d.ts +++ b/packages/playwright/src/mcp/config.d.ts @@ -64,6 +64,11 @@ export type Config = { */ cdpHeaders?: Record; + /** + * Timeout in milliseconds for connecting to CDP endpoint. Defaults to 30000 (30 seconds). Pass 0 to disable timeout. + */ + cdpTimeout?: number; + /** * Remote endpoint to connect to an existing Playwright server. */ diff --git a/packages/playwright/types/test.d.ts b/packages/playwright/types/test.d.ts index 108a9119b7484..40cfe8d9181d4 100644 --- a/packages/playwright/types/test.d.ts +++ b/packages/playwright/types/test.d.ts @@ -1001,7 +1001,7 @@ interface TestConfig { * webServer: { * command: 'npm run start', * wait: { - * stdout: '/Listening on port (?\\d+)/' + * stdout: /Listening on port (?\d+)/ * }, * }, * }); @@ -10252,14 +10252,14 @@ interface TestConfigWebServer { wait?: { /** * Regular expression to wait for in the `stdout` of the command output. Named capture groups are stored in the - * environment, for example `/Listening on port (?\\d+)/` will store the port number in + * environment, for example `/Listening on port (?\d+)/` will store the port number in * `process.env['MY_SERVER_PORT']`. */ stdout?: RegExp; /** * Regular expression to wait for in the `stderr` of the command output. Named capture groups are stored in the - * environment, for example `/Listening on port (?\\d+)/` will store the port number in + * environment, for example `/Listening on port (?\d+)/` will store the port number in * `process.env['MY_SERVER_PORT']`. */ stderr?: RegExp; diff --git a/tests/page/page-route.spec.ts b/tests/page/page-route.spec.ts index b601fc95fdad5..d0f2418e947c8 100644 --- a/tests/page/page-route.spec.ts +++ b/tests/page/page-route.spec.ts @@ -1069,3 +1069,25 @@ it('should be able to intercept every navigation to a page controlled by service await page.goto(URL); expect(interceptions).toBe(2); }); + +it('does not get stalled by beforeUnload', { annotation: { type: 'issue', description: 'https://github.com/microsoft/playwright/issues/38731' } }, async ({ page, server }) => { + await page.goto(server.HELLO_WORLD); + + await page.evaluate(() => { + window.addEventListener('beforeunload', event => { + event.preventDefault(); + }); + }); + page.on('dialog', dialog => dialog.dismiss()); + + // We have to interact with a page so that 'beforeunload' handlers + // fire. + await page.click('body'); + + await page.route('**/api', route => route.fulfill({ status: 200, body: 'ok' })); + await page.evaluate(async () => fetch(new URL('/api', window.location.href))); + + await page.close({ runBeforeUnload: true }); + + await page.evaluate(async () => fetch(new URL('/api', window.location.href))); +});