Skip to content

Commit 1c2ed33

Browse files
committed
feat: stabilize image sizes
When the viewport is changed, the browser fails to re-calculate the proper image size. With that new stabilization we ensure images are stable with `viewports` option.
1 parent 523719e commit 1c2ed33

File tree

4 files changed

+94
-5
lines changed

4 files changed

+94
-5
lines changed

packages/browser/src/global/stabilization.ts

+91-5
Original file line numberDiff line numberDiff line change
@@ -175,6 +175,79 @@ function restoreElementPositions(document: Document) {
175175
});
176176
}
177177

178+
/**
179+
* Stabilize all image sizes.
180+
* - Reload all images to ensure the dimensions are correctly calculated.
181+
* - Set the width and height to the rounded values.
182+
*/
183+
function stabilizeImageSizes(document: Document) {
184+
const images = Array.from(document.images);
185+
186+
const results = images.map((img) => {
187+
// Force sync decoding
188+
if (img.decoding !== "sync") {
189+
img.decoding = "sync";
190+
}
191+
192+
// Force eager loading
193+
if (img.loading !== "eager") {
194+
img.loading = "eager";
195+
}
196+
197+
if (!img.complete) {
198+
// If the image is not loaded yet, we wait.
199+
return false;
200+
}
201+
202+
if (img.dataset.argosStabilized) {
203+
// If the image is already stabilized, we skip it.
204+
return true;
205+
}
206+
207+
// Force the re-rendering of the image by removing the src and srcset attributes
208+
// and then restoring them.
209+
// This will recalculate the dimensions of the image and make it right.
210+
const originalSrcSet = img.srcset;
211+
const originalSrc = img.src;
212+
img.srcset = "";
213+
img.src = "";
214+
img.srcset = originalSrcSet;
215+
img.src = originalSrc;
216+
217+
// Mark it as stabilized
218+
img.dataset.argosStabilized = "true";
219+
220+
// Preserve the original width and height
221+
img.dataset.argosBckWidth = img.style.width ?? "";
222+
img.dataset.argosBckHeight = img.style.height ?? "";
223+
224+
// Set the width and height to the rounded values
225+
const rect = img.getBoundingClientRect();
226+
img.style.width = `${Math.round(rect.width)}px`;
227+
img.style.height = `${Math.round(rect.height)}px`;
228+
229+
return true;
230+
});
231+
232+
return results.every((x) => x);
233+
}
234+
235+
/**
236+
* Restore images to their original size.
237+
*/
238+
function restoreImageSizes(document: Document) {
239+
const images = Array.from(document.images);
240+
images.forEach((img) => {
241+
if (img.dataset.argosStabilized) {
242+
img.style.width = img.dataset.argosBckWidth ?? "";
243+
img.style.height = img.dataset.argosBckHeight ?? "";
244+
delete img.dataset.argosBckWidth;
245+
delete img.dataset.argosBckHeight;
246+
delete img.dataset.argosStabilized;
247+
}
248+
});
249+
}
250+
178251
function addGlobalClass(document: Document, className: string) {
179252
document.documentElement.classList.add(className);
180253
}
@@ -221,6 +294,7 @@ export function teardown(
221294
if (fullPage) {
222295
restoreElementPositions(document);
223296
}
297+
restoreImageSizes(document);
224298
}
225299

226300
/**
@@ -236,8 +310,7 @@ function waitForFontsToLoad(document: Document) {
236310
function waitForImagesToLoad(document: Document) {
237311
const images = Array.from(document.images);
238312

239-
// Force eager loading
240-
images.forEach((img) => {
313+
return images.every((img) => {
241314
// Force sync decoding
242315
if (img.decoding !== "sync") {
243316
img.decoding = "sync";
@@ -247,9 +320,10 @@ function waitForImagesToLoad(document: Document) {
247320
if (img.loading !== "eager") {
248321
img.loading = "eager";
249322
}
250-
});
251323

252-
return images.every((img) => img.complete);
324+
// Check if the image is loaded
325+
return img.complete;
326+
});
253327
}
254328

255329
/**
@@ -283,6 +357,11 @@ export type StabilizationOptions = {
283357
* @default true
284358
*/
285359
images?: boolean;
360+
/**
361+
* Stabilize the images sizes.
362+
* @default true
363+
*/
364+
imageSizes?: boolean;
286365
/**
287366
* Wait for fonts to be loaded.
288367
* @default true
@@ -294,17 +373,24 @@ export type StabilizationOptions = {
294373
* Get the stabilization state of the document.
295374
*/
296375
function getStabilityState(document: Document, options?: StabilizationOptions) {
297-
const { ariaBusy = true, images = true, fonts = true } = options ?? {};
376+
const {
377+
ariaBusy = true,
378+
images = true,
379+
fonts = true,
380+
imageSizes = true,
381+
} = options ?? {};
298382
return {
299383
ariaBusy: ariaBusy ? waitForNoBusy(document) : true,
300384
images: images ? waitForImagesToLoad(document) : true,
385+
imageSizes: imageSizes ? stabilizeImageSizes(document) : true,
301386
fonts: fonts ? waitForFontsToLoad(document) : true,
302387
};
303388
}
304389

305390
const VALIDATION_ERRORS: Record<keyof StabilizationOptions, string> = {
306391
ariaBusy: "Some elements still have `aria-busy='true'`",
307392
images: "Some images are still loading",
393+
imageSizes: "Failed to stabilize image sizes",
308394
fonts: "Some fonts are still loading",
309395
};
310396

packages/cypress/docs/index.mdx

+1
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,7 @@ module.exports = defineConfig({
5959
- `options.stabilize.ariaBusy`: Wait for the `aria-busy` attribute to be removed from the document. Default to `true`.
6060
- `options.stabilize.fonts`: Wait for fonts to be loaded. Default to `true`.
6161
- `options.stabilize.images`: Wait for images to be loaded. Default to `true`.
62+
- `options.stabilize.imageSizes`: Stabilize all image sizes to avoid glitches. Default to `true`.
6263

6364
## Helper Attributes for Visual Testing
6465

packages/playwright/docs/index.mdx

+1
Original file line numberDiff line numberDiff line change
@@ -239,6 +239,7 @@ export default defineConfig({
239239
- `options.stabilize.ariaBusy`: Wait for the `aria-busy` attribute to be removed from the document. Default to `true`.
240240
- `options.stabilize.fonts`: Wait for fonts to be loaded. Default to `true`.
241241
- `options.stabilize.images`: Wait for images to be loaded. Default to `true`.
242+
- `options.stabilize.imageSizes`: Stabilize all image sizes to avoid glitches. Default to `true`.
242243
- `options.beforeScreenshot`: Run a function before taking the screenshot. When using viewports, this function will run before taking sreenshots on each viewport.
243244
- `options.afterScreenshot`: Run a function after taking the screenshot. When using viewports, this function will run after taking sreenshots on each viewport.
244245

packages/puppeteer/docs/index.mdx

+1
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,7 @@ Screenshots are stored in `screenshots/argos` folder, relative to current direct
6262
- `options.stabilize.ariaBusy`: Wait for the `aria-busy` attribute to be removed from the document. Default to `true`.
6363
- `options.stabilize.fonts`: Wait for fonts to be loaded. Default to `true`.
6464
- `options.stabilize.images`: Wait for images to be loaded. Default to `true`.
65+
- `options.stabilize.imageSizes`: Stabilize all image sizes to avoid glitches. Default to `true`.
6566

6667
Unlike [Puppeteer's `screenshot` method](https://playwright.dev/docs/api/class-page#page-screenshot), `argosScreenshot` set `fullPage` option to `true` by default. Feel free to override this option if you prefer partial screenshots of your pages.
6768

0 commit comments

Comments
 (0)