Skip to content

Commit 74e61d6

Browse files
sserrataclaude
andauthored
fix(visual-diff): reduce false positives and cache prod screenshots (#1397)
- Freeze CSS animations/transitions via injected stylesheet before capturing to eliminate mid-animation pixel differences - Replace details expansion click+timeout with direct open attribute set and waitForFunction poll to avoid framework re-render races - Switch waitUntil from networkidle to load for faster, deterministic page readiness - Add --disable-font-subpixel-positioning and --disable-lcd-text Chromium flags for consistent text rasterization across captures - Skip prod screenshot when file already exists on disk; pair with a workflow cache keyed on the production sitemap SHA-256 hash so prod pages are only re-screenshotted when the live site changes Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent 19c671c commit 74e61d6

File tree

2 files changed

+43
-9
lines changed

2 files changed

+43
-9
lines changed

.github/workflows/deploy-preview.yml

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -208,6 +208,20 @@ jobs:
208208
- name: Install Playwright
209209
run: npx playwright install --with-deps chromium
210210

211+
- name: Get production sitemap hash
212+
id: sitemap-hash
213+
run: |
214+
hash=$(curl -fsSL https://docusaurus-openapi.tryingpan.dev/sitemap.xml | sha256sum | cut -d' ' -f1)
215+
echo "hash=$hash" >> "$GITHUB_OUTPUT"
216+
217+
- name: Restore cached production screenshots
218+
uses: actions/cache@v4
219+
with:
220+
path: visual_diffs/prod
221+
key: prod-screenshots-${{ steps.sitemap-hash.outputs.hash }}
222+
restore-keys: |
223+
prod-screenshots-
224+
211225
- name: Run visual diff
212226
run: yarn ts-node scripts/sitemap-visual-diff.ts --preview-url ${{ needs.deploy.outputs.preview_url }} --summary-file visual_diffs/results.json --concurrency 4 --paths "/tests/"
213227

scripts/sitemap-visual-diff.ts

Lines changed: 29 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -98,17 +98,31 @@ function parseUrlsFromSitemap(xml: string): string[] {
9898
}
9999

100100
async function screenshotFullPage(page: any, url: string, outputPath: string) {
101-
await page.goto(url, { waitUntil: "networkidle" });
101+
await page.goto(url, { waitUntil: "load" });
102+
// Freeze all CSS animations and transitions before screenshotting so that
103+
// mid-animation frames don't produce spurious pixel differences.
104+
await page.addStyleTag({
105+
content: `*, *::before, *::after {
106+
animation-duration: 0s !important;
107+
animation-delay: 0s !important;
108+
transition-duration: 0s !important;
109+
transition-delay: 0s !important;
110+
}`,
111+
});
112+
// Expand details elements by setting the attribute directly; avoid
113+
// simulating clicks, which can fire framework event handlers and cause
114+
// re-renders that race with the subsequent screenshot.
102115
await page.evaluate(() => {
103116
document.querySelectorAll("div.container details").forEach((el) => {
104-
const detail = el as HTMLDetailsElement;
105-
const summary = detail.querySelector("summary");
106-
if (!detail.open && summary) (summary as HTMLElement).click();
107-
detail.open = true;
108-
detail.setAttribute("data-collapsed", "false");
117+
(el as HTMLDetailsElement).open = true;
109118
});
110119
});
111-
await page.waitForTimeout(500);
120+
// Wait until every details element is confirmed open before capturing.
121+
await page.waitForFunction(
122+
() =>
123+
document.querySelectorAll("div.container details:not([open])").length ===
124+
0
125+
);
112126
await fs.promises.mkdir(path.dirname(outputPath), { recursive: true });
113127
const container = await page.$("div.container");
114128
if (container) {
@@ -201,7 +215,9 @@ async function run() {
201215
}
202216
console.log(`Found ${paths.length} paths.`);
203217

204-
const browser = await chromium.launch();
218+
const browser = await chromium.launch({
219+
args: ["--disable-font-subpixel-positioning", "--disable-lcd-text"],
220+
});
205221
const context = await browser.newContext({
206222
viewport: { width: opts.width, height: opts.viewHeight },
207223
});
@@ -220,7 +236,11 @@ async function run() {
220236
const diffImg = path.join(opts.outputDir, "diff", `${cleanPath}.png`);
221237
const page = await context.newPage();
222238
try {
223-
await screenshotFullPage(page, url, prodSnap);
239+
if (fs.existsSync(prodSnap)) {
240+
console.log(`CACHED prod: /${cleanPath}`);
241+
} else {
242+
await screenshotFullPage(page, url, prodSnap);
243+
}
224244
await screenshotFullPage(
225245
page,
226246
new URL(cleanPath, opts.previewUrl).toString(),

0 commit comments

Comments
 (0)