Skip to content
Closed
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
28 changes: 26 additions & 2 deletions packages/playwright-core/src/server/frames.ts
Original file line number Diff line number Diff line change
Expand Up @@ -510,6 +510,28 @@ export type FrameEventMap = {
[FrameEvent.RemoveLifecycle]: [event: types.LifecycleEvent];
};

// How long the one-shot expect check may keep running after the call has been aborted.
const kOneShotGracePeriod = 1000;

async function raceWithGracePeriod<T>(progress: Progress, promise: Promise<T>, gracePeriod: number): Promise<T> {
let timer: ReturnType<typeof setTimeout> | undefined;
const onAbort = () => {
timer = setTimeout(() => rejectOnAbort(progress.signal.reason), gracePeriod);
};
let rejectOnAbort: (reason: any) => void = () => {};
const aborted = new Promise<never>((_, reject) => rejectOnAbort = reject);
if (progress.signal.aborted)
onAbort();
else
progress.signal.addEventListener('abort', onAbort, { once: true });
try {
return await Promise.race([promise, aborted]);
} finally {
clearTimeout(timer);
progress.signal.removeEventListener('abort', onAbort);
}
}

export class Frame extends SdkObject<FrameEventMap> {
static Events = FrameEvent;

Expand Down Expand Up @@ -1483,11 +1505,13 @@ export class Frame extends SdkObject<FrameEventMap> {
progress.log(`waiting for ${this._asLocator(selector)}`);
await this._page.performActionPreChecks(progress);

// Step 2: perform one-shot expect check without a timeout.
// Step 2: perform one-shot expect check that may run past the deadline.
// Supports the case of `expect(locator).toBeVisible({ timeout: 1 })`
// that should succeed when the locator is already visible.
// The grace period prevents an unresponsive page (e.g. a blocked event loop)
// from holding the call past its timeout.
try {
const resultOneShot = await this._expectInternal(progress, selector, options, lastIntermediateResult, true);
const resultOneShot = await raceWithGracePeriod(progress, this._expectInternal(progress, selector, options, lastIntermediateResult, true), kOneShotGracePeriod);
if (resultOneShot.matches !== options.isNot)
return;
} catch (e) {
Expand Down
13 changes: 13 additions & 0 deletions tests/page/expect-timeout.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -211,3 +211,16 @@ Error: The assertion was aborted: stop it
`);
}
});

test('should respect timeout when the page is unresponsive', { annotation: { type: 'issue', description: 'https://github.com/microsoft/playwright/issues/42880' } }, async ({ page }) => {
test.setTimeout(10000);
await page.setContent('<div>hello</div>');
await page.evaluate(() => {
setTimeout(() => {
while (true) {}
}, 0);
});
await new Promise(f => setTimeout(f, 100));
const error = await expect(page.locator('div')).toBeVisible({ timeout: 1000 }).catch(e => e);
expect(stripAnsi(error.message)).toContain('Timeout: 1000ms');
});
Loading