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
8 changes: 6 additions & 2 deletions src/browser/client-scripts/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,7 @@ function prepareScreenshotUnsafe(areas, opts) {
width: viewportWidth,
height: viewportHeight
}),
pixelRatio = configurePixelRatio(opts.usePixelRatio),
pixelRatio = configurePixelRatio(opts.usePixelRatio, opts.preferredPixelRatio),
rect,
selectors = [];

Expand Down Expand Up @@ -252,11 +252,15 @@ function getCaptureRect(selectors, opts) {
};
}

function configurePixelRatio(usePixelRatio) {
function configurePixelRatio(usePixelRatio, preferredPixelRatio) {
if (usePixelRatio === false) {
return 1;
}

if (preferredPixelRatio) {
return preferredPixelRatio;
}

if (window.devicePixelRatio) {
return window.devicePixelRatio;
}
Expand Down
37 changes: 35 additions & 2 deletions src/browser/commands/assert-view/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,18 @@ const AssertViewResults = require("./assert-view-results");
const { BaseStateError } = require("./errors/base-state-error");
const { addTestplaneSelectivityPngDependency } = require("../../cdp/selectivity/testplane-selectivity");

const HEADLESS_CHROME_ARG_RE = /^-{0,2}headless(?:=|$)/;

const isHeadlessBrowser = chromeOptions => (chromeOptions?.args || []).some(arg => HEADLESS_CHROME_ARG_RE.test(arg));

const isPixelRatioEmulated = chromeOptions => Boolean(chromeOptions?.mobileEmulation);

const getEmulatedPixelRatio = chromeOptions => {
const pixelRatio = _.get(chromeOptions, "mobileEmulation.deviceMetrics.pixelRatio");

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Derive DPR for named mobile-emulation profiles

When headful Chrome is configured using the supported mobileEmulation: { deviceName: "..." } form, deviceMetrics is absent, so this returns undefined and the code never installs the preferred-DPR retry path. On an OOPIF page exhibiting the mismatch addressed by this change, prepareScreenshot may use the pre-capture DPR while the screenshot is produced after Chrome switches DPR, leaving named-device users with incorrectly scaled and cropped screenshots. Obtain the initial DPR from the browser or otherwise resolve the named profile rather than requiring explicit deviceMetrics.pixelRatio.

Useful? React with 👍 / 👎.


return _.isFinite(pixelRatio) && pixelRatio > 0 ? pixelRatio : undefined;
};

const getIgnoreDiffPixelCountRatio = value => {
const percent = _.isString(value) && value.endsWith("%") ? parseFloat(value.slice(0, -1)) : false;

Expand All @@ -29,6 +41,8 @@ const getIgnoreDiffPixelCountRatio = value => {
module.exports.default = browser => {
const screenShooter = ScreenShooter.create(browser);
const { publicAPI: session, config } = browser;
const chromeOptions = session.requestedCapabilities?.["goog:chromeOptions"];
const emulatedPixelRatio = getEmulatedPixelRatio(chromeOptions);
const {
assertViewOpts,
compareOpts,
Expand Down Expand Up @@ -68,13 +82,20 @@ module.exports.default = browser => {
const handleCaptureProcessorError = e =>
e instanceof BaseStateError ? testplaneCtx.assertViewResults.add(e) : Promise.reject(e);

const page = await browser.prepareScreenshot([].concat(selectors), {
const shouldValidatePixelRatio =
browser.shouldUsePixelRatio && !isHeadlessBrowser(chromeOptions) && isPixelRatioEmulated(chromeOptions);
const preferredPixelRatio = shouldValidatePixelRatio ? emulatedPixelRatio : undefined;

const screenshotSelectors = [].concat(selectors);
const prepareScreenshotOpts = {
ignoreSelectors: [].concat(opts.ignoreElements),
allowViewportOverflow: opts.allowViewportOverflow,
captureElementFromTop: opts.captureElementFromTop,
selectorToScroll: opts.selectorToScroll,
disableAnimation: opts.disableAnimation,
});
preferredPixelRatio,
};
const page = await browser.prepareScreenshot(screenshotSelectors, prepareScreenshotOpts);

const { tempOpts, updateRefs: isUpdatingRefs } = RuntimeConfig.getInstance();
temp.attach(tempOpts);
Expand All @@ -85,6 +106,18 @@ module.exports.default = browser => {
"screenshotDelay",
"selectorToScroll",
]);
if (shouldValidatePixelRatio) {
if (preferredPixelRatio) {
screenshoterOpts.preferredPixelRatio = preferredPixelRatio;
}

screenshoterOpts.reprepareScreenshot = currentPixelRatio =>
browser.prepareScreenshot(screenshotSelectors, {
...prepareScreenshotOpts,
disableAnimation: false,
preferredPixelRatio: currentPixelRatio,
});
}
const currImgInst = await screenShooter
.capture(page, screenshoterOpts)
.finally(() => browser.cleanupScreenshot(opts));
Expand Down
6 changes: 5 additions & 1 deletion src/browser/existing-browser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -172,7 +172,7 @@ export class ExistingBrowser extends Browser {
// Running this fragment with history causes rrweb snapshots to break on pages with iframes
return runWithoutHistory({ callstack: this._callstackHistory! }, async () => {
opts = _.extend(opts, {
usePixelRatio: this._calibration ? this._calibration.usePixelRatio : true,
usePixelRatio: this.shouldUsePixelRatio,
});

ensure(this._clientBridge, CLIENT_BRIDGE_HINT);
Expand Down Expand Up @@ -212,6 +212,10 @@ export class ExistingBrowser extends Browser {
return this._session.execute(`return ${script}`);
}

get shouldUsePixelRatio(): boolean {
return this._calibration ? this._calibration.usePixelRatio : true;
}

injectScript(script: string): Promise<unknown> {
ensure(this._session, BROWSER_SESSION_HINT);

Expand Down
21 changes: 20 additions & 1 deletion src/browser/screen-shooter/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,30 @@ module.exports = class ScreenShooter {
}

async capture(page, opts = {}) {
const { allowViewportOverflow, compositeImage, screenshotDelay, selectorToScroll } = opts;
const {
allowViewportOverflow,
compositeImage,
screenshotDelay,
selectorToScroll,
preferredPixelRatio,
reprepareScreenshot,
} = opts;
const viewportOpts = { allowViewportOverflow, compositeImage };
const cropImageOpts = { screenshotDelay, compositeImage, selectorToScroll };

const capturedImage = await this._browser.captureViewportImage(page, screenshotDelay);
if (reprepareScreenshot) {
const currentPixelRatio = await this._browser.evalScript("window.devicePixelRatio");
Comment on lines +27 to +28

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Validate DPR before cropping the provisional screenshot

With screenshotMode: "fullpage", an emulated-DPR mismatch and a horizontally scrolled page, captureViewportImage(page) processes the first screenshot using the stale, preferred-DPR viewport coordinates before this check runs. Camera._cropAreaToViewport treats the image as full-page and can pass an offset beyond the actual lower-DPR image into Image.crop, which throws a RangeError, so execution never reaches evalScript or the retry. Capture the provisional image without the stale page geometry, or determine the current DPR before asking Camera to crop it.

Useful? React with 👍 / 👎.


if (currentPixelRatio !== (preferredPixelRatio ?? page.pixelRatio)) {
Object.assign(page, await reprepareScreenshot(currentPixelRatio));
delete opts.preferredPixelRatio;
delete opts.reprepareScreenshot;

return this.capture(page, opts);
}
}

const viewport = Viewport.create(page, capturedImage, viewportOpts);
await viewport.handleImage(capturedImage);

Expand Down
60 changes: 60 additions & 0 deletions test/src/browser/commands/assert-view/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -161,6 +161,66 @@ describe("assertView command", () => {
assert.calledOnceWith(browser.prepareScreenshot, [".selector1", ".selector2"]);
});

it("should use emulated pixel ratio from requested capabilities in headful browser", async () => {
const session = mkSessionStub_();
session.requestedCapabilities = {
"goog:chromeOptions": {
mobileEmulation: { deviceMetrics: { pixelRatio: 3 } },
},
};
const browser = await initBrowser_({ session });
sandbox.stub(browser, "cleanupScreenshot").resolves();

await browser.publicAPI.assertView("plain", ".selector", { disableAnimation: true });

assert.calledOnceWith(
browser.prepareScreenshot,
[".selector"],
sinon.match({ disableAnimation: true, preferredPixelRatio: 3 }),
);
assert.calledOnceWith(
ScreenShooter.prototype.capture,
sinon.match.any,
sinon.match({ preferredPixelRatio: 3 }),
);

const reprepareScreenshot = ScreenShooter.prototype.capture.lastCall.args[1].reprepareScreenshot;

await reprepareScreenshot(1);

assert.calledWith(
browser.prepareScreenshot,
[".selector"],
sinon.match({ disableAnimation: false, preferredPixelRatio: 1 }),
);
});

it("should validate pixel ratio when mobile emulation uses a device name", async () => {
const session = mkSessionStub_();
session.requestedCapabilities = {
"goog:chromeOptions": {
mobileEmulation: { deviceName: "Pixel 7" },
},
};
const browser = await initBrowser_({ session });
sandbox.stub(browser, "cleanupScreenshot").resolves();

await browser.publicAPI.assertView("plain", ".selector");

const screenShooterOpts = ScreenShooter.prototype.capture.lastCall.args[1];

assert.notProperty(screenShooterOpts, "preferredPixelRatio");
assert.isFunction(screenShooterOpts.reprepareScreenshot);

await screenShooterOpts.reprepareScreenshot(3);

assert.calledWith(
browser.prepareScreenshot,
[".selector"],
sinon.match({ disableAnimation: false, preferredPixelRatio: 3 }),
);
});

it("should screenshot the viewport if selector is not provided", async () => {
const browser = await initBrowser_();

Expand Down
62 changes: 62 additions & 0 deletions test/src/browser/screen-shooter/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ describe("screen-shooter", () => {
browser = {
config: {},
captureViewportImage: sandbox.stub().resolves(imageStub),
evalScript: sandbox.stub().resolves(1),
scrollBy: sandbox.stub().resolves(),
};
});
Expand Down Expand Up @@ -94,6 +95,67 @@ describe("screen-shooter", () => {
assert.calledWithMatch(browser.captureViewportImage, sinon.match.any, 2000);
});

it("should retry capture using pixel ratio from browser if it differs from prepared", async () => {
const preparedPage = {
captureArea: { left: 1, top: 2, width: 10, height: 20 },
viewport: { left: 0, top: 0, width: 100, height: 200 },
ignoreAreas: [{ left: 3, top: 4, width: 5, height: 6 }],
documentHeight: 300,
documentWidth: 200,
pixelRatio: 3,
};
const reprepareScreenshot = sandbox.stub().resolves(preparedPage);
const opts = { reprepareScreenshot };
browser.evalScript.resolves(3);

await capture(
{
captureArea: { left: 3, top: 6, width: 30, height: 60 },
viewport: { left: 0, top: 0, width: 300, height: 600 },
ignoreAreas: [{ left: 9, top: 12, width: 15, height: 18 }],
documentHeight: 900,
documentWidth: 600,
pixelRatio: 1,
},
opts,
);

assert.calledTwice(browser.captureViewportImage);
assert.calledOnceWith(browser.evalScript, "window.devicePixelRatio");
assert.calledOnceWith(reprepareScreenshot, 3);
assert.calledOnceWith(Viewport.create, preparedPage, imageStub, sinon.match.any);
assert.notProperty(opts, "preferredPixelRatio");
assert.notProperty(opts, "reprepareScreenshot");
});

it("should recompute fractional-DPR geometry instead of rescaling rounded bounds", async () => {
const preparedPage = {
captureArea: { left: 1, top: 2, width: 10, height: 20 },
viewport: { left: 50, top: 0, width: 101, height: 201 },
ignoreAreas: [{ left: 3, top: 4, width: 6, height: 7 }],
documentHeight: 201,
documentWidth: 101,
pixelRatio: 1,
};
const reprepareScreenshot = sandbox.stub().resolves(preparedPage);
const opts = { preferredPixelRatio: 2.625, reprepareScreenshot };

await capture(
{
captureArea: { left: 3, top: 6, width: 28, height: 54 },
viewport: { left: 131, top: 0, width: 266, height: 528 },
ignoreAreas: [{ left: 9, top: 12, width: 15, height: 18 }],
documentHeight: 528,
documentWidth: 266,
pixelRatio: 2.625,
},
opts,
);

assert.calledOnceWith(reprepareScreenshot, 1);
assert.calledOnceWith(Viewport.create, preparedPage, imageStub, sinon.match.any);
});

it("should extract image of passed size", async () => {
await capture({ captureArea: { foo: "bar" } });

Expand Down