Skip to content

Instantly share code, notes, and snippets.

@evagoras
Last active April 26, 2025 08:33
Show Gist options
  • Save evagoras/4b6376af5e661ec98e8df4b196019940 to your computer and use it in GitHub Desktop.
Save evagoras/4b6376af5e661ec98e8df4b196019940 to your computer and use it in GitHub Desktop.
Keep Images Honest: Zero-Dependency Aspect Ratio Testing with Playwright
import { Locator } from '@playwright/test';
export interface AspectRatioResult {
withinTolerance: boolean;
deviationPct: number;
naturalRatio: number;
renderedRatio: number;
}
/**
* General image-related utilities for Playwright tests.
*/
export class ImageUtils {
/**
* Checks whether an image’s rendered aspect ratio matches its natural aspect ratio
* within a given tolerance (in percent).
*
* @param image Playwright Locator for the <img> element
* @param tolerancePct Allowed deviation in percent (e.g. 1 = 1%)
* @returns Object describing whether it’s within tolerance, plus metrics
*/
public static async checkAspectRatio(
image: Locator,
tolerancePct: number = 1
): Promise<AspectRatioResult> {
// Bundle all DOM reads into a single evaluate() round-trip
const data = await image.evaluate((img: HTMLImageElement) => {
// 1) Ensure the image is loaded
if (!img.complete || img.naturalWidth === 0 || img.naturalHeight === 0) {
return null;
}
// 2) Get on-screen size (includes CSS transforms & fractional pixels)
const { width: rectW, height: rectH } = img.getBoundingClientRect();
const style = window.getComputedStyle(img);
// 3) Subtract padding & border to get the true content box
const padX =
parseFloat(style.paddingLeft || '0') +
parseFloat(style.paddingRight || '0');
const padY =
parseFloat(style.paddingTop || '0') +
parseFloat(style.paddingBottom || '0');
const borderX =
parseFloat(style.borderLeftWidth || '0') +
parseFloat(style.borderRightWidth || '0');
const borderY =
parseFloat(style.borderTopWidth || '0') +
parseFloat(style.borderBottomWidth || '0');
return {
naturalW: img.naturalWidth,
naturalH: img.naturalHeight,
renderedW: rectW - padX - borderX,
renderedH: rectH - padY - borderY
};
});
if (!data) {
throw new Error('Image not loaded or has zero natural dimensions');
}
const { naturalW, naturalH, renderedW, renderedH } = data;
const naturalRatio = naturalW / naturalH;
const renderedRatio = renderedW / renderedH;
// 4) Compute deviation percentage
const deviationPct = Math.abs(renderedRatio / naturalRatio - 1) * 100;
const withinTolerance = deviationPct <= tolerancePct;
// build and return an explicit AspectRatioResult
const result: AspectRatioResult = {
withinTolerance,
deviationPct,
naturalRatio,
renderedRatio,
};
if (!withinTolerance) {
console.warn(
`Aspect-ratio mismatch: rendered=${renderedRatio.toFixed(4)}, ` +
`natural=${naturalRatio.toFixed(4)} → ` +
`deviation=${deviationPct.toFixed(2)}% ` +
`(tolerance=${tolerancePct}%)`
);
}
return result;
}
}
import { test, expect } from '@playwright/test';
import { AspectRatioResult, ImageUtils } from '../utils/ImageUtils';
test('product thumbnail maintains its aspect ratio', async ({ page }) => {
await page.goto('https://your-app.local/products/42');
const thumbnail = page.locator('.product-thumbnail img');
const {
withinTolerance,
deviationPct,
naturalRatio,
renderedRatio
}: AspectRatioResult = await ImageUtils.checkAspectRatio(thumbnail, 0.5);
expect(withinTolerance).toBeTruthy();
expect(deviationPct).toBeLessThanOrEqual(0.5);
console.log(
`Natural ratio: ${naturalRatio.toFixed(4)}, ` +
`Rendered ratio: ${renderedRatio.toFixed(4)}, ` +
`Deviation: ${deviationPct.toFixed(2)}%`
);
})
@evagoras
Copy link
Author

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment