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
3 changes: 1 addition & 2 deletions .github/workflows/create_test_report.yml
Original file line number Diff line number Diff line change
Expand Up @@ -190,7 +190,7 @@ jobs:
if: needs.triage.outputs.has_draft == 'true'
runs-on: ubuntu-latest
permissions:
issues: write
pull-requests: write
env:
PR_NUMBER: ${{ needs.triage.outputs.pr_number }}
GH_TOKEN: ${{ github.token }}
Expand All @@ -206,4 +206,3 @@ jobs:
run: |
printf '\n\n<sub>Triaged by the Playwright bot - [agent run](%s)</sub>\n<!-- playwright-ci-triage -->\n' "$WORKFLOW_URL" >> output/triage.md
gh issue comment "$PR_NUMBER" --repo "${{ github.repository }}" --body-file output/triage.md

3 changes: 3 additions & 0 deletions .github/workflows/fix-flakes-prompt.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,9 @@ Turn CI test-results data into **one** concrete fix: pick a high-impact flaky-or
confirm nobody's on it, fix the root cause *or* scope a skip, pick a reviewer, and hand off a
single commit that becomes the PR. Fully autonomous — no approval stops.

The GitHub CLI (`gh`) is not authenticated in this job. Do not use it for GitHub API operations;
use GitHub MCP tools instead.

## 1. Pick one target

Query the DB following the patterns in `.claude/skills/playwright-test-results/SKILL.md`.
Expand Down
3 changes: 3 additions & 0 deletions .github/workflows/fix-flakes.yml
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,9 @@ jobs:
an expensive runner to fix a flaky or red test is most worthwhile right now, and hand
off only that runner label. Do NOT fix anything.

The GitHub CLI is not authenticated in this job. Do not run gh; use GitHub MCP tools
for all GitHub reads.

Optimise for the OS with the highest-impact actionable flakiness/reds that isn't
already being worked on OR already fixed. A candidate is dead if a fix PR touches it
(open, or recently merged/closed) — the DB window still holds the failing runs from
Expand Down
3 changes: 3 additions & 0 deletions .github/workflows/triage.yml
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,9 @@ jobs:
PROMPT=$(cat <<EOF
Triage https://github.com/${{ github.repository }}/issues/$ISSUE using the playwright-triage skill.

The GitHub CLI is not authenticated in this job. Do not run gh; use GitHub MCP tools
for all GitHub reads.

Do not post anything yourself. Write the comment to output/triage.md and a later step posts it.
EOF
)
Expand Down
6 changes: 6 additions & 0 deletions docs/src/api/class-locatorassertions.md
Original file line number Diff line number Diff line change
Expand Up @@ -2008,6 +2008,9 @@ Snapshot name. Must have a `.png` or `.webp` extension, the screenshot is captur
### option: LocatorAssertions.toHaveScreenshot#1.timeout = %%-js-assertions-timeout-%%
* since: v1.23

### option: LocatorAssertions.toHaveScreenshot#1.signal = %%-js-assertions-signal-%%
* since: v1.62

### option: LocatorAssertions.toHaveScreenshot#1.animations = %%-screenshot-option-animations-default-disabled-%%
* since: v1.23

Expand Down Expand Up @@ -2059,6 +2062,9 @@ Note that screenshot assertions only work with Playwright test runner.
### option: LocatorAssertions.toHaveScreenshot#2.timeout = %%-js-assertions-timeout-%%
* since: v1.23

### option: LocatorAssertions.toHaveScreenshot#2.signal = %%-js-assertions-signal-%%
* since: v1.62

### option: LocatorAssertions.toHaveScreenshot#2.animations = %%-screenshot-option-animations-default-disabled-%%
* since: v1.23

Expand Down
6 changes: 6 additions & 0 deletions docs/src/api/class-pageassertions.md
Original file line number Diff line number Diff line change
Expand Up @@ -258,6 +258,9 @@ Snapshot name. Must have a `.png` or `.webp` extension, the screenshot is captur
### option: PageAssertions.toHaveScreenshot#1.timeout = %%-js-assertions-timeout-%%
* since: v1.23

### option: PageAssertions.toHaveScreenshot#1.signal = %%-js-assertions-signal-%%
* since: v1.62

### option: PageAssertions.toHaveScreenshot#1.animations = %%-screenshot-option-animations-default-disabled-%%
* since: v1.23

Expand Down Expand Up @@ -314,6 +317,9 @@ Note that screenshot assertions only work with Playwright test runner.
### option: PageAssertions.toHaveScreenshot#2.timeout = %%-js-assertions-timeout-%%
* since: v1.23

### option: PageAssertions.toHaveScreenshot#2.signal = %%-js-assertions-signal-%%
* since: v1.62

### option: PageAssertions.toHaveScreenshot#2.animations = %%-screenshot-option-animations-default-disabled-%%
* since: v1.23

Expand Down
5 changes: 2 additions & 3 deletions packages/playwright-core/src/client/android.ts
Original file line number Diff line number Diff line change
Expand Up @@ -272,11 +272,10 @@ export class AndroidDevice extends ChannelOwner<channels.AndroidDeviceChannel> i

async waitForEvent(event: string, optionsOrPredicate: types.WaitForEventOptions = {}): Promise<any> {
return await this._wrapApiCall(async () => {
const { timeout, signal } = this._timeoutSettings.timeout(typeof optionsOrPredicate === 'function' ? {} : optionsOrPredicate);
const timeoutOptions = this._timeoutSettings.timeout(typeof optionsOrPredicate === 'function' ? {} : optionsOrPredicate);
const predicate = typeof optionsOrPredicate === 'function' ? optionsOrPredicate : optionsOrPredicate.predicate;
const waiter = Waiter.createForEvent(this, event);
waiter.rejectOnTimeout(timeout, `Timeout ${timeout}ms exceeded while waiting for event "${event}"`);
waiter.rejectOnSignal(signal);
waiter.rejectOnTimeout(timeoutOptions, `Timeout ${timeoutOptions.timeout}ms exceeded while waiting for event "${event}"`);
if (event !== Events.AndroidDevice.Close)
waiter.rejectOnEvent(this, Events.AndroidDevice.Close, () => new TargetClosedError());
const result = await waiter.waitForEvent(this, event, predicate as any);
Expand Down
5 changes: 2 additions & 3 deletions packages/playwright-core/src/client/browserContext.ts
Original file line number Diff line number Diff line change
Expand Up @@ -449,11 +449,10 @@ export class BrowserContext extends ChannelOwner<channels.BrowserContextChannel>

async waitForEvent(event: string, optionsOrPredicate: WaitForEventOptions = {}): Promise<any> {
return await this._wrapApiCall(async () => {
const { timeout, signal } = this._timeoutSettings.timeout(typeof optionsOrPredicate === 'function' ? {} : optionsOrPredicate);
const timeoutOptions = this._timeoutSettings.timeout(typeof optionsOrPredicate === 'function' ? {} : optionsOrPredicate);
const predicate = typeof optionsOrPredicate === 'function' ? optionsOrPredicate : optionsOrPredicate.predicate;
const waiter = Waiter.createForEvent(this, event);
waiter.rejectOnTimeout(timeout, `Timeout ${timeout}ms exceeded while waiting for event "${event}"`);
waiter.rejectOnSignal(signal);
waiter.rejectOnTimeout(timeoutOptions, `Timeout ${timeoutOptions.timeout}ms exceeded while waiting for event "${event}"`);
if (event !== Events.BrowserContext.Close)
waiter.rejectOnEvent(this, Events.BrowserContext.Close, () => new TargetClosedError(this._effectiveCloseReason()));
const result = await waiter.waitForEvent(this, event, predicate as any);
Expand Down
5 changes: 2 additions & 3 deletions packages/playwright-core/src/client/electron.ts
Original file line number Diff line number Diff line change
Expand Up @@ -139,11 +139,10 @@ export class ElectronApplication extends ChannelOwner<channels.ElectronApplicati

async waitForEvent(event: string, optionsOrPredicate: WaitForEventOptions = {}): Promise<any> {
return await this._wrapApiCall(async () => {
const { timeout, signal } = this._timeoutSettings.timeout(typeof optionsOrPredicate === 'function' ? {} : optionsOrPredicate);
const timeoutOptions = this._timeoutSettings.timeout(typeof optionsOrPredicate === 'function' ? {} : optionsOrPredicate);
const predicate = typeof optionsOrPredicate === 'function' ? optionsOrPredicate : optionsOrPredicate.predicate;
const waiter = Waiter.createForEvent(this, event);
waiter.rejectOnTimeout(timeout, `Timeout ${timeout}ms exceeded while waiting for event "${event}"`);
waiter.rejectOnSignal(signal);
waiter.rejectOnTimeout(timeoutOptions, `Timeout ${timeoutOptions.timeout}ms exceeded while waiting for event "${event}"`);
if (event !== Events.ElectronApplication.Close)
waiter.rejectOnEvent(this, Events.ElectronApplication.Close, () => new TargetClosedError());
const result = await waiter.waitForEvent(this, event, predicate as any);
Expand Down
9 changes: 4 additions & 5 deletions packages/playwright-core/src/client/frame.ts
Original file line number Diff line number Diff line change
Expand Up @@ -129,9 +129,8 @@ export class Frame extends ChannelOwner<channels.FrameChannel> implements api.Fr
waiter.rejectOnEvent(this._page!, Events.Page.Close, () => this._page!._closeErrorWithReason());
waiter.rejectOnEvent(this._page!, Events.Page.Crash, new Error('Navigation failed because page crashed!'));
waiter.rejectOnEvent<Frame>(this._page!, Events.Page.FrameDetached, new Error('Navigating frame was detached!'), frame => frame === this);
const { timeout } = this._page!._timeoutSettings.navigationTimeout(options);
waiter.rejectOnTimeout(timeout, `Timeout ${timeout}ms exceeded.`);
waiter.rejectOnSignal(options.signal);
const timeoutOptions = this._page!._timeoutSettings.navigationTimeout(options);
waiter.rejectOnTimeout(timeoutOptions, `Timeout ${timeoutOptions.timeout}ms exceeded.`);
return waiter;
}

Expand Down Expand Up @@ -496,8 +495,8 @@ export class Frame extends ChannelOwner<channels.FrameChannel> implements api.Fr
return (await this._channel.title({}, kNoTimeout)).value;
}

async _expect(expression: string, options: Omit<channels.FrameExpectParams, 'expression'> & { timeout: number }, signal: AbortSignal | undefined): Promise<ExpectResult> {
const { timeout, ...rest } = options;
async _expect(expression: string, options: Omit<channels.FrameExpectParams, 'expression'> & { timeout: number, signal?: AbortSignal }): Promise<ExpectResult> {
const { timeout, signal, ...rest } = options;
const params: channels.FrameExpectParams = { expression, ...rest, isNot: !!rest.isNot };
params.expectedValue = serializeArgument(rest.expectedValue);
try {
Expand Down
4 changes: 2 additions & 2 deletions packages/playwright-core/src/client/locator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -404,11 +404,11 @@ export class Locator implements api.Locator {
}


async _expect(expression: string, options: FrameExpectParams, signal: AbortSignal | undefined): Promise<ExpectResult> {
async _expect(expression: string, options: FrameExpectParams): Promise<ExpectResult> {
return this._frame._expect(expression, {
...options,
selector: this._selector,
}, signal);
});
}

private _inspect() {
Expand Down
5 changes: 2 additions & 3 deletions packages/playwright-core/src/client/network.ts
Original file line number Diff line number Diff line change
Expand Up @@ -804,11 +804,10 @@ export class WebSocket extends ChannelOwner<channels.WebSocketChannel> implement

async waitForEvent(event: string, optionsOrPredicate: WaitForEventOptions = {}): Promise<any> {
return await this._wrapApiCall(async () => {
const { timeout, signal } = this._page._timeoutSettings.timeout(typeof optionsOrPredicate === 'function' ? {} : optionsOrPredicate);
const timeoutOptions = this._page._timeoutSettings.timeout(typeof optionsOrPredicate === 'function' ? {} : optionsOrPredicate);
const predicate = typeof optionsOrPredicate === 'function' ? optionsOrPredicate : optionsOrPredicate.predicate;
const waiter = Waiter.createForEvent(this, event);
waiter.rejectOnTimeout(timeout, `Timeout ${timeout}ms exceeded while waiting for event "${event}"`);
waiter.rejectOnSignal(signal);
waiter.rejectOnTimeout(timeoutOptions, `Timeout ${timeoutOptions.timeout}ms exceeded while waiting for event "${event}"`);
if (event !== Events.WebSocket.Error)
waiter.rejectOnEvent(this, Events.WebSocket.Error, new Error('Socket error'));
if (event !== Events.WebSocket.Close)
Expand Down
15 changes: 9 additions & 6 deletions packages/playwright-core/src/client/page.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import fs from 'fs';
import * as inspector from 'inspector';
import path from 'path';

import { assertionAbortedMessage } from '@isomorphic/abortSignal';
import { assert } from '@isomorphic/assert';
import { headersObjectToArray } from '@isomorphic/headers';
import { trimStringWithEllipsis } from '@isomorphic/stringUtils';
Expand All @@ -32,7 +33,7 @@ import { Coverage } from './coverage';
import { DisposableObject, DisposableStub } from './disposable';
import { Download } from './download';
import { ElementHandle, determineScreenshotType } from './elementHandle';
import { PlaywrightError, TargetClosedError, isTargetClosedError, parseError, serializeError } from './errors';
import { AbortError, PlaywrightError, TargetClosedError, isTargetClosedError, parseError, serializeError } from './errors';
import { Events } from './events';
import { FileChooser } from './fileChooser';
import { Frame, verifyLoadState } from './frame';
Expand Down Expand Up @@ -77,6 +78,7 @@ export type ExpectScreenshotOptions = Omit<channels.PageExpectScreenshotOptions,
expected?: Buffer,
locator?: api.Locator,
timeout: number,
signal?: AbortSignal,
isNot: boolean,
mask?: api.Locator[],
};
Expand Down Expand Up @@ -477,13 +479,12 @@ export class Page extends ChannelOwner<channels.PageChannel> implements api.Page

private async _waitForEvent(event: string, optionsOrPredicate: WaitForEventOptions, logLine?: string): Promise<any> {
return await this._wrapApiCall(async () => {
const { timeout, signal } = this._timeoutSettings.timeout(typeof optionsOrPredicate === 'function' ? {} : optionsOrPredicate);
const timeoutOptions = this._timeoutSettings.timeout(typeof optionsOrPredicate === 'function' ? {} : optionsOrPredicate);
const predicate = typeof optionsOrPredicate === 'function' ? optionsOrPredicate : optionsOrPredicate.predicate;
const waiter = Waiter.createForEvent(this, event);
if (logLine)
waiter.log(logLine);
waiter.rejectOnTimeout(timeout, `Timeout ${timeout}ms exceeded while waiting for event "${event}"`);
waiter.rejectOnSignal(signal);
waiter.rejectOnTimeout(timeoutOptions, `Timeout ${timeoutOptions.timeout}ms exceeded while waiting for event "${event}"`);
if (event !== Events.Page.Crash)
waiter.rejectOnEvent(this, Events.Page.Crash, new Error('Page crashed'));
if (event !== Events.Page.Close)
Expand Down Expand Up @@ -622,7 +623,7 @@ export class Page extends ChannelOwner<channels.PageChannel> implements api.Page
}

async _expectScreenshot(options: ExpectScreenshotOptions): Promise<{ actual?: Buffer, previous?: Buffer, diff?: Buffer, errorMessage?: string, log?: string[], timedOut?: boolean}> {
const { timeout, ...optionsWithoutTimeout } = options;
const { timeout, signal, ...optionsWithoutTimeout } = options;
const mask = options?.mask ? options?.mask.map(locator => ({
frame: (locator as Locator)._frame._channel,
selector: (locator as Locator)._selector,
Expand All @@ -637,9 +638,11 @@ export class Page extends ChannelOwner<channels.PageChannel> implements api.Page
isNot: !!options.isNot,
locator,
mask,
}, { signal: undefined, timeout });
}, { timeout, signal });
return { actual: result.actual };
} catch (e) {
if (e instanceof AbortError)
return { errorMessage: 'Error: ' + assertionAbortedMessage(e.cause) };
if (!(e instanceof PlaywrightError))
throw e;
const details = e.details as channels.PageExpectScreenshotErrorDetails;
Expand Down
2 changes: 1 addition & 1 deletion packages/playwright-core/src/client/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -147,4 +147,4 @@ export type AnnotateOptions = { duration?: number, position?: AnnotatePosition,
export type RemoteAddr = channels.RemoteAddr;
export type SecurityDetails = channels.SecurityDetails;

export type FrameExpectParams = Omit<channels.FrameExpectParams, 'selector'|'expression'|'expectedValue'> & { expectedValue?: any, timeout: number };
export type FrameExpectParams = Omit<channels.FrameExpectParams, 'selector'|'expression'|'expectedValue'> & { expectedValue?: any, timeout: number, signal?: AbortSignal };
30 changes: 13 additions & 17 deletions packages/playwright-core/src/client/waiter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -80,25 +80,21 @@ export class Waiter {
this._rejectOn(promise.then(() => { throw (typeof error === 'function' ? error() : error); }), dispose);
}

rejectOnTimeout(timeout: number, message: string) {
if (!timeout)
return;
const { promise, dispose } = waitForTimeout(timeout);
this._rejectOn(promise.then(() => { throw new TimeoutError(message); }), dispose);
}
rejectOnTimeout({ timeout, signal }: channels.TimeoutOptions, timeoutMessage: string) {
if (signal) {
if (signal.aborted)
return this.rejectImmediately(new AbortError(undefined, { cause: signal.reason }));
let rejectPromise: (e: any) => void;
const promise = new Promise<void>((_, reject) => { rejectPromise = reject; });
const listener = () => rejectPromise!(new AbortError(undefined, { cause: signal.reason }));
signal.addEventListener('abort', listener, { once: true });
this._rejectOn(promise, () => signal.removeEventListener('abort', listener));
}

rejectOnSignal(signal: AbortSignal | undefined) {
if (!signal)
return;
if (signal.aborted) {
this.rejectImmediately(new AbortError(undefined, { cause: signal.reason }));
return;
if (timeout) {
const { promise, dispose } = waitForTimeout(timeout);
this._rejectOn(promise.then(() => { throw new TimeoutError(timeoutMessage); }), dispose);
}
let rejectPromise: (e: any) => void;
const promise = new Promise<void>((_, reject) => { rejectPromise = reject; });
const listener = () => rejectPromise!(new AbortError(undefined, { cause: signal.reason }));
signal.addEventListener('abort', listener, { once: true });
this._rejectOn(promise, () => signal.removeEventListener('abort', listener));
}

rejectImmediately(error: Error) {
Expand Down
5 changes: 2 additions & 3 deletions packages/playwright-core/src/client/worker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -89,11 +89,10 @@ export class Worker extends ChannelOwner<channels.WorkerChannel> implements api.
async waitForEvent(event: string, optionsOrPredicate: WaitForEventOptions = {}): Promise<any> {
return await this._wrapApiCall(async () => {
const timeoutSettings = this._page?._timeoutSettings ?? this._context?._timeoutSettings ?? new TimeoutSettings();
const { timeout, signal } = timeoutSettings.timeout(typeof optionsOrPredicate === 'function' ? {} : optionsOrPredicate);
const timeoutOptions = timeoutSettings.timeout(typeof optionsOrPredicate === 'function' ? {} : optionsOrPredicate);
const predicate = typeof optionsOrPredicate === 'function' ? optionsOrPredicate : optionsOrPredicate.predicate;
const waiter = Waiter.createForEvent(this, event);
waiter.rejectOnTimeout(timeout, `Timeout ${timeout}ms exceeded while waiting for event "${event}"`);
waiter.rejectOnSignal(signal);
waiter.rejectOnTimeout(timeoutOptions, `Timeout ${timeoutOptions.timeout}ms exceeded while waiting for event "${event}"`);
if (event !== Events.Worker.Close)
waiter.rejectOnEvent(this, Events.Worker.Close, () => this._closeErrorWithReason());
const result = await waiter.waitForEvent(this, event, predicate as any);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -356,13 +356,10 @@ export class DispatcherConnection {
type: dispatcher._type,
method,
params: params || {},
timeout: validMetadata.timeout,
log: [],
};

// TODO(skn0tt): promote to top-level metadata instead of smuggling through params.
if (validMetadata.timeout)
callMetadata.params = { ...callMetadata.params, timeout: validMetadata.timeout };

const controller = dispatcher.createProgressController(callMetadata);
this._activeProgressControllers.set(callMetadata.id, controller);

Expand Down
11 changes: 6 additions & 5 deletions packages/playwright-core/src/server/ebml.ts
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,7 @@ function element(id: Buffer, payload: Buffer): Buffer {

// Emits the Matroska header: EBML head, an unknown-size (streaming) Segment, stream Info with a
// 1ms timestamp scale, and a single MJPEG video track. Frames follow as Clusters via writeClusterHeader.
export function writeHeader(width: number, height: number): Buffer {
export function writeHeader(): Buffer {
const ebml = element(kEBML, Buffer.concat([
element(kEBMLVersion, uint(1)),
element(kEBMLReadVersion, uint(1)),
Expand All @@ -112,11 +112,12 @@ export function writeHeader(width: number, height: number): Buffer {
element(kTrackType, uint(1)), // 1 = video.
element(kFlagLacing, uint(0)),
element(kCodecID, Buffer.from('V_MJPEG')),
// PixelWidth/PixelHeight are advisory: ffmpeg's mjpeg decoder uses the dimensions encoded in
// each JPEG frame, and the output video filters normalize to the requested size.
// PixelWidth/PixelHeight are mandatory, but the actual JPEG dimensions are not known yet.
// A larger placeholder can make ffmpeg mistake a short progressive JPEG for an interlaced
// field, so use 1x1 and let the MJPEG decoder read the real dimensions from each frame.
element(kVideo, Buffer.concat([
element(kPixelWidth, uint(width)),
element(kPixelHeight, uint(height)),
element(kPixelWidth, uint(1)),
element(kPixelHeight, uint(1)),
])),
]));
const tracks = element(kTracks, track);
Expand Down
1 change: 1 addition & 0 deletions packages/playwright-core/src/server/instrumentation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,7 @@ export type CallMetadata = {
type: string;
method: string;
params: any;
timeout?: number;
title?: string;
// Client is making an internal call that should not show up in
// the inspector or trace.
Expand Down
Loading
Loading