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 package.json
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@
"test-e2e-testplane": "npm run test-e2e-testplane:generate-fixtures && npm run test-e2e-testplane:run-tests",
"test-e2e-testplane:run-tests": "node bin/testplane --config test/e2e/testplane.config.ts",
"test-e2e-testplane:generate-fixtures": "node bin/testplane --config test/e2e/fixtures/basic-report/testplane.config.ts || true",
"test-e2e:gui": "node bin/testplane --config test/e2e/testplane.config.ts gui",
"test-e2e-testplane:gui": "node bin/testplane --config test/e2e/testplane.config.ts gui",
"test-browser-env": "TS_NODE_PROJECT=./test/browser-env/tsconfig.json node bin/testplane -r tsconfig-paths/register --config test/browser-env/testplane.config.ts",
"test-browser-env:gui": "TS_NODE_PROJECT=./test/browser-env/tsconfig.json node bin/testplane gui -r tsconfig-paths/register --config test/browser-env/testplane.config.ts",
"toc": "doctoc docs --title '### Contents'",
Expand Down
16 changes: 13 additions & 3 deletions src/browser/client-scripts/screen-shooter/operations.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@ import {
fromBcrToRect,
getBottom,
getCoveringRect,
getHeight,
getIntersection
} from "@isomorphic";
import { OutsideOfViewportError } from "./errors/outside-of-viewport";
Expand Down Expand Up @@ -48,6 +47,16 @@ export function computeScrollOffset(element: Element): Coord<"page", "css", "y">
}

export function computeViewportSize(): Size<"css"> {
const visualViewport = window.visualViewport;

// Visual viewport occasionally returns more correct values than innerWidth/Height, but may not be available in older browsers
if (visualViewport && visualViewport.width > 0 && visualViewport.height > 0) {
return {
width: visualViewport.width as Length<"css", "x">,
height: visualViewport.height as Length<"css", "y">
Comment on lines +55 to +56

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 Normalize visual viewport dimensions before returning

When visualViewport exposes fractional CSS pixels, such as under pinch/page zoom, these raw dimensions flow through fromCssToDevice unchanged whenever DPR is an integer. Later screenshot cropping and PNG encoding assume integer image sizes (Image.crop stores actualWidth/actualHeight directly, and PNG IHDR writes integer widths), so these captures can fail or produce malformed buffers; round/floor the visual viewport size before returning it.

Useful? React with 👍 / 👎.

};
}

return {
width: window.innerWidth as Length<"css", "x">,
height: window.innerHeight as Length<"css", "y">
Expand Down Expand Up @@ -333,7 +342,8 @@ export function computeSafeArea(
const scrollParentBcr = scrollParent.getBoundingClientRect();
topValue += isRootLikeElement(scrollParent) ? 0 : scrollParentBcr.top;
shouldSkipZIndexCheck =
scrollParent === scrollEl || (isRootLikeElement(scrollParent) && isRootLikeElement(scrollEl));
captureElements.some(capEl => capEl === el || capEl.contains(el)) &&
(scrollParent === scrollEl || (isRootLikeElement(scrollParent) && isRootLikeElement(scrollEl)));

if (!isNaN(topValue)) {
adjustedRect = {
Expand Down Expand Up @@ -396,7 +406,7 @@ export function computeSafeArea(

if (shrinkTop < shrinkBottom) {
resultingTop = Math.max(brBottom, safeTop) as Coord<"viewport", "css", "y">;
resultingHeight = getHeight(safeBottom, resultingTop);
resultingHeight = Math.max(0, safeBottom - resultingTop) as Length<"css", "y">;
logger?.("decided to shrink top");
} else if (shrinkBottom) {
resultingHeight = Math.min(safeHeight, br.top - safeTop) as Length<"css", "y">;
Expand Down
12 changes: 7 additions & 5 deletions src/browser/isomorphic/types.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
export enum DisableHoverMode {
Always = "always",
WhenScrollingNeeded = "when-scrolling-needed",
Never = "never",
}
export const DisableHoverMode = {
Always: "always",
WhenScrollingNeeded: "when-scrolling-needed",
Never: "never",
} as const;

export type DisableHoverMode = (typeof DisableHoverMode)[keyof typeof DisableHoverMode];

export enum BrowserSideErrorCode {
JS = "JS",
Expand Down
8 changes: 6 additions & 2 deletions src/browser/screen-shooter/composite-image/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -106,7 +106,11 @@ export class CompositeImage {
}

if (this._captureAreaSize.width <= 0 || this._captureAreaSize.height <= 0) {
throw new Error("Capture area size cannot be zero or negative. Got: " + prettySize(this._captureAreaSize));
throw new Error(
"Capture area size cannot be zero or negative. Got: " +
prettySize(this._captureAreaSize) +
"\nMost likely this means that you are trying to capture an area that is completely clipped by parent block (e.g. with overflow: hidden), or the element is zero-sized on its own.",
);
}

const imageSize = viewportImage.getSize() as Size<"device">;
Expand Down Expand Up @@ -166,8 +170,8 @@ export class CompositeImage {

debug("Candidates: %O", candidates);

this._chooseBestCandidates(candidates);
this._expandCandidatesToFullArea(candidates);
this._chooseBestCandidates(candidates);

const commonHorizontalArea = this._computeCommonHorizontalAreaIfNeeded(candidates, this._captureAreaSize.width);
const captureWidth = commonHorizontalArea?.width ?? this._captureAreaSize.width;
Expand Down
10 changes: 10 additions & 0 deletions src/browser/screen-shooter/elements-screen-shooter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -631,6 +631,16 @@ export class ElementsScreenShooter {
opts,
async currentState => {
if (currentState.captureSpecs.length === 0) {
if (iterations > 0) {

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 Require complete coverage before accepting disappeared targets

For a target that needs another scroll chunk and hides after the first scroll, iterations > 0 is true even though hasCapturedTheWholeArea is still false. Returning here makes _scrollThroughCaptureArea treat the empty spec list as finished, so CompositeImage renders only previously registered chunks and the lower part of the requested selector is never captured or compared; this can turn an incomplete capture into a passing/truncated baseline instead of an error.

Useful? React with 👍 / 👎.

debug(
"Capture area disappeared after %d chunk(s), rendering already captured data for selectors: %s",
iterations,
selectorsToCapture.join("; "),
);

return;
}

throw new Error(getEmptyCaptureSpecsErrorMessage(selectorsToCapture));
}

Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Original file line number Diff line number Diff line change
Expand Up @@ -336,6 +336,19 @@ describe("computeSafeArea", () => {
await browser.assertView("compute-safe-area-stacking-context-filter-in-front");
});

it("should not shrink when root sticky element is behind absolute popup target", async ({ browser }) => {
const { default: html } = await import("./fixtures/safe-areas/root-sticky-behind-absolute-popup.html?raw");
document.body.innerHTML = html;

const selectors = [".popup"];
const safeArea = computeSafeArea(selectors);
const captureSpecs = computeCaptureSpecs(selectors);

visualizeCaptureSpecs(captureSpecs);
visualizeSafeArea(safeArea.top, safeArea.height);
await browser.assertView("compute-safe-area-root-sticky-behind-absolute-popup");
});

it("should shrink for fixed header that creates stacking context via filter and is in front", async ({
browser,
}) => {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
<style>
html,
body {
margin: 0;
padding: 0;
width: 100%;
min-height: 1600px;
font-family: Arial, sans-serif;
background: #eef3f7;
}

.listing {
position: relative;
z-index: 1;
width: 1000px;
min-height: 900px;
margin: 0 auto;
padding-top: 100px;
}

.sticky-filter {
position: sticky;
top: 112px;
z-index: 22;
display: flex;
align-items: center;
width: 1000px;
height: 60px;
box-sizing: border-box;
padding: 0 24px;
background: #304461;
color: #fff;
font-weight: 700;
}

.card {
width: 520px;
height: 260px;
margin-top: 48px;
padding: 28px;
box-sizing: border-box;
background: #fff;
border: 1px solid #b9c7d8;
}

.popup {
position: absolute;
left: 700px;
top: 120px;
z-index: 11010;
width: 360px;
height: 116px;
box-sizing: border-box;
padding: 18px;
background: #fff;
border: 2px solid #55a0ff;
box-shadow: 0 10px 28px rgba(20, 33, 61, 0.22);
font-weight: 700;
}
</style>

<div class="listing">
<div class="sticky-filter">Sticky filter behind popup</div>

<div class="card">
<h3>Listing card</h3>
<p>The sticky filter belongs to page content and is below the popup in stacking order.</p>
</div>
</div>

<div class="popup">Popup target above sticky filter</div>
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
86 changes: 86 additions & 0 deletions test/e2e/static/capture-area-disappears-after-scroll.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Capture Area Disappears After Scroll</title>
<style>
html,
body {
margin: 0;
min-height: 1800px;
font-family: Arial, sans-serif;
background: #eef1f5;
}

.bottom-panel {
position: fixed;
left: 0;
top: calc(100vh - 100px);
width: 520px;
height: 100px;
box-sizing: border-box;
padding: 24px;
border-top: 6px solid #6b1111;
background: #d84444;
color: #fff;
font-size: 24px;
font-weight: 700;
}

.capture-target {
position: absolute;
left: 40px;
top: calc(100vh - 350px);
width: 360px;
height: 300px;
box-sizing: border-box;
border: 6px solid #183153;
background: #fff;
color: #183153;
overflow: hidden;
}

body.hide-capture-target .capture-target {
display: none;
}

.stripe {
height: 100px;
box-sizing: border-box;
padding: 30px;
font-size: 26px;
font-weight: 700;
}

.stripe-top {
background: #8ed2ff;
}

.stripe-middle {
background: #fff0a3;
}

.stripe-bottom {
background: #b7edc2;
}
</style>
</head>
<body>
<div class="bottom-panel">Safe area interference</div>

<main class="capture-target" data-testid="scroll-sensitive-popup">
<div class="stripe stripe-top">Popup top</div>
<div class="stripe stripe-middle">Popup middle</div>
<div class="stripe stripe-bottom">Popup bottom</div>
</main>

<script>
window.addEventListener("scroll", function () {
if (window.scrollY > 0) {
document.body.classList.add("hide-capture-target");
}
});
</script>
</body>
</html>
84 changes: 84 additions & 0 deletions test/e2e/static/fixed-layer-in-transformed-page.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Fixed Layer in Transformed Page</title>
<style>
html,
body {
margin: 0;
min-height: 3800px;
font-family: Arial, sans-serif;
background: #f1f4f8;
}

.page {
position: relative;
min-height: 3800px;
transform: translateZ(0);
}

.fixed-layer {
position: fixed;
top: 0;
left: 0;
z-index: 2;
width: 100%;
height: 3800px;
pointer-events: none;
}

.capture-target {
position: relative;
z-index: 1;
left: 64px;
top: 80px;
width: 720px;
height: 3720px;
box-sizing: border-box;
border: 6px solid #1b2a41;
background: #fff;
}

.section {
height: 930px;
box-sizing: border-box;
padding: 32px;
border-bottom: 3px solid #1b2a41;
color: #1b2a41;
font-size: 32px;
font-weight: 700;
}

.section:nth-child(1) {
background: #bfe7ff;
}

.section:nth-child(2) {
background: #fff1a8;
}

.section:nth-child(3) {
background: #d0f4c7;
}

.section:nth-child(4) {
border-bottom: 0;
background: #ffc8b8;
}
</style>
</head>
<body>
<div class="page">
<div class="fixed-layer" aria-hidden="true"></div>

<main class="capture-target" data-testid="capture-target">
<div class="section">Target top</div>
<div class="section">Target upper middle</div>
<div class="section">Target lower middle</div>
<div class="section">Target bottom must be captured</div>
</main>
</div>
</body>
</html>
16 changes: 16 additions & 0 deletions test/e2e/tests/assert-view.testplane.js
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,22 @@ describe("assertView", () => {
});
});

it("should keep capturing when a fixed layer scrolls inside a transformed page", async ({ browser }) => {
await browser.url("fixed-layer-in-transformed-page.html");

await browser.assertView("fixed-layer-in-transformed-page", "[data-testid=capture-target]", {
captureElementFromTop: true,
});
});

it("should use captured chunk when capture area disappears after scrolling", async ({ browser }) => {
await browser.url("capture-area-disappears-after-scroll.html");

await browser.assertView("scroll-sensitive-popup", "[data-testid=scroll-sensitive-popup]", {
captureElementFromTop: true,
});
});

it("should treat sticky content inside capture target as interference", async ({ browser }) => {
await browser.url("sticky-interference-behind-capture-target.html");

Expand Down
Loading
Loading