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
2 changes: 1 addition & 1 deletion docs/src/release-notes-js.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ export default defineConfig({
webServer: {
command: 'npm run start',
wait: {
stdout: '/Listening on port (?<my_server_port>\\d+)/'
stdout: /Listening on port (?<my_server_port>\d+)/
},
},
});
Expand Down
6 changes: 3 additions & 3 deletions docs/src/test-api/class-testconfig.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 (?<my_server_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 (?<my_server_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 (?<my_server_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 (?<my_server_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.

Expand Down Expand Up @@ -797,7 +797,7 @@ export default defineConfig({
webServer: {
command: 'npm run start',
wait: {
stdout: '/Listening on port (?<my_server_port>\\d+)/'
stdout: /Listening on port (?<my_server_port>\d+)/
},
},
});
Expand Down
2 changes: 1 addition & 1 deletion docs/src/test-webserver-js.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 (?<my_server_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 (?<my_server_port>\d+)/` will store the port number in `process.env['MY_SERVER_PORT']`. |

## Adding a server timeout

Expand Down
3 changes: 2 additions & 1 deletion packages/playwright-core/src/client/page.ts
Original file line number Diff line number Diff line change
Expand Up @@ -631,7 +631,8 @@ export class Page extends ChannelOwner<channels.PageChannel> 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();
Expand Down
36 changes: 19 additions & 17 deletions packages/playwright-core/src/server/trace/recorder/tracing.ts
Original file line number Diff line number Diff line change
Expand Up @@ -423,39 +423,39 @@ export class Tracing extends SdkObject implements InstrumentationListener, Snaps
return { artifact };
}

async _captureSnapshot(snapshotName: string, sdkObject: SdkObject, metadata: CallMetadata): Promise<void> {
if (!this._snapshotter)
private async _captureSnapshot(snapshotName: string | undefined, sdkObject: SdkObject, metadata: CallMetadata): Promise<void> {
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);
}
Expand All @@ -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);
}
Expand Down
5 changes: 4 additions & 1 deletion packages/playwright/src/mcp/browser/browserContextFactory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -152,7 +152,10 @@ class CdpContextFactory extends BaseContextFactory {
}

protected override async _doObtainBrowser(): Promise<playwright.Browser> {
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<playwright.BrowserContext> {
Expand Down
5 changes: 5 additions & 0 deletions packages/playwright/src/mcp/config.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,11 @@ export type Config = {
*/
cdpHeaders?: Record<string, string>;

/**
* 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.
*/
Expand Down
6 changes: 3 additions & 3 deletions packages/playwright/types/test.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1001,7 +1001,7 @@ interface TestConfig<TestArgs = {}, WorkerArgs = {}> {
* webServer: {
* command: 'npm run start',
* wait: {
* stdout: '/Listening on port (?<my_server_port>\\d+)/'
* stdout: /Listening on port (?<my_server_port>\d+)/
* },
* },
* });
Expand Down Expand Up @@ -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 (?<my_server_port>\\d+)/` will store the port number in
* environment, for example `/Listening on port (?<my_server_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 (?<my_server_port>\\d+)/` will store the port number in
* environment, for example `/Listening on port (?<my_server_port>\d+)/` will store the port number in
* `process.env['MY_SERVER_PORT']`.
*/
stderr?: RegExp;
Expand Down
22 changes: 22 additions & 0 deletions tests/page/page-route.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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)));
});
Loading