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
66 changes: 31 additions & 35 deletions packages/playwright-core/src/server/frames.ts
Original file line number Diff line number Diff line change
Expand Up @@ -441,7 +441,7 @@ export class Frame extends SdkObject {
private _setContentCounter = 0;
readonly _detachedPromise: Promise<void>;
private _detachedCallback = () => {};
private _nonStallingEvaluations = new Set<(error: Error) => void>();
private _raceAgainstEvaluationStallingEventsPromises = new Set<ManualPromise<any>>();

constructor(page: Page, id: string, parentFrame: Frame | null) {
super(page, 'frame');
Expand Down Expand Up @@ -500,53 +500,47 @@ export class Frame extends SdkObject {
}

_invalidateNonStallingEvaluations(message: string) {
if (!this._nonStallingEvaluations)
if (!this._raceAgainstEvaluationStallingEventsPromises.size)
return;
const error = new Error(message);
for (const callback of this._nonStallingEvaluations)
callback(error);
for (const promise of this._raceAgainstEvaluationStallingEventsPromises)
promise.reject(error);
}

async nonStallingRawEvaluateInExistingMainContext(expression: string): Promise<any> {
async raceAgainstEvaluationStallingEvents<T>(cb: () => Promise<T>): Promise<T> {
if (this._pendingDocument)
throw new Error('Frame is currently attempting a navigation');
if (this._page._frameManager._openedDialogs.size)
throw new Error('Open JavaScript dialog prevents evaluation');
const context = this._existingMainContext();
if (!context)
throw new Error('Frame does not yet have a main execution context');

let callback = () => {};
const frameInvalidated = new Promise<void>((f, r) => callback = r);
this._nonStallingEvaluations.add(callback);
const promise = new ManualPromise<T>();
this._raceAgainstEvaluationStallingEventsPromises.add(promise);
try {
return await Promise.race([
context.rawEvaluateJSON(expression),
frameInvalidated
cb(),
promise
]);
} finally {
this._nonStallingEvaluations.delete(callback);
this._raceAgainstEvaluationStallingEventsPromises.delete(promise);
}
}

async nonStallingEvaluateInExistingContext(expression: string, isFunction: boolean|undefined, world: types.World): Promise<any> {
if (this._pendingDocument)
throw new Error('Frame is currently attempting a navigation');
const context = this._contextData.get(world)?.context;
if (!context)
throw new Error('Frame does not yet have the execution context');
nonStallingRawEvaluateInExistingMainContext(expression: string): Promise<any> {
return this.raceAgainstEvaluationStallingEvents(() => {
const context = this._existingMainContext();
if (!context)
throw new Error('Frame does not yet have a main execution context');
return context.rawEvaluateJSON(expression);
});
}

let callback = () => {};
const frameInvalidated = new Promise<void>((f, r) => callback = r);
this._nonStallingEvaluations.add(callback);
try {
return await Promise.race([
context.evaluateExpression(expression, isFunction),
frameInvalidated
]);
} finally {
this._nonStallingEvaluations.delete(callback);
}
nonStallingEvaluateInExistingContext(expression: string, isFunction: boolean|undefined, world: types.World): Promise<any> {
return this.raceAgainstEvaluationStallingEvents(() => {
const context = this._contextData.get(world)?.context;
if (!context)
throw new Error('Frame does not yet have the execution context');
return context.evaluateExpression(expression, isFunction);
});
}

private _recalculateLifecycle() {
Expand Down Expand Up @@ -1168,10 +1162,12 @@ export class Frame extends SdkObject {
}

async hideHighlight() {
const context = await this._utilityContext();
const injectedScript = await context.injectedScript();
return await injectedScript.evaluate(injected => {
return injected.hideHighlight();
return this.raceAgainstEvaluationStallingEvents(async () => {
const context = await this._utilityContext();
const injectedScript = await context.injectedScript();
return await injectedScript.evaluate(injected => {
return injected.hideHighlight();
});
});
}

Expand Down
9 changes: 7 additions & 2 deletions packages/playwright-core/src/server/screenshotter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -223,6 +223,9 @@ export class Screenshotter {
}

async _maskElements(progress: Progress, options: ScreenshotOptions) {
if (!options.mask || !options.mask.length)
return false;

const framesToParsedSelectors: MultiMap<Frame, ParsedSelector> = new MultiMap();
await Promise.all((options.mask || []).map(async ({ frame, selector }) => {
const pair = await frame.resolveFrameForSelectorNoWait(selector);
Expand All @@ -235,6 +238,7 @@ export class Screenshotter {
await frame.maskSelectors(framesToParsedSelectors.get(frame));
}));
progress.cleanupWhenAborted(() => this._page.hideHighlight());
return true;
}

private async _screenshot(progress: Progress, format: 'png' | 'jpeg', documentRect: types.Rect | undefined, viewportRect: types.Rect | undefined, fitsViewport: boolean | undefined, options: ScreenshotOptions): Promise<Buffer> {
Expand All @@ -248,13 +252,14 @@ export class Screenshotter {
}
progress.throwIfAborted(); // Avoid extra work.

await this._maskElements(progress, options);
const hasHighlight = await this._maskElements(progress, options);
progress.throwIfAborted(); // Avoid extra work.

const buffer = await this._page._delegate.takeScreenshot(progress, format, documentRect, viewportRect, options.quality, fitsViewport);
progress.throwIfAborted(); // Avoid restoring after failure - should be done by cleanup.

await this._page.hideHighlight();
if (hasHighlight)
await this._page.hideHighlight();
progress.throwIfAborted(); // Avoid restoring after failure - should be done by cleanup.

if (shouldSetDefaultBackground)
Expand Down
15 changes: 15 additions & 0 deletions tests/page/page-screenshot.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@

import { test as it, expect } from './pageTest';
import { verifyViewport, attachFrame } from '../config/utils';
import type { Route } from 'playwright-core';
import path from 'path';
import fs from 'fs';
import os from 'os';
Expand Down Expand Up @@ -429,6 +430,20 @@ it.describe('page screenshot', () => {
const screenshot2 = await page.screenshot();
expect(screenshot1.equals(screenshot2)).toBe(true);
});

it('should work when subframe has stalled navigation', async ({ page, server }) => {
let cb;
const routeReady = new Promise<Route>(f => cb = f);
await page.route('**/subframe.html', cb); // Stalling subframe.

await page.goto(server.EMPTY_PAGE);
const done = page.setContent(`<iframe src='/subframe.html'></iframe>`);
const route = await routeReady;

await page.screenshot({ mask: [ page.locator('non-existent') ] });
await route.fulfill({ body: '' });
await done;
});
});
});

Expand Down