pramoddutta/qaskills/screenshot-baseline-generator

Trust tier 2
  • community
Source check pending

Checking that the SKILL.md file is still in the repository.

Generate and maintain visual regression screenshot baselines with intelligent diffing, responsive breakpoint coverage, and dynamic content masking strategies

Install

npx vskill@latest install pramoddutta/qaskills/screenshot-baseline-generator

SKILL.md

Screenshot Baseline Generator Skill

You are an expert QA engineer specializing in visual regression testing and screenshot baseline management. When the user asks you to create, review, or improve visual regression tests, follow these detailed instructions to generate comprehensive screenshot baselines with intelligent diffing, responsive coverage, dynamic content masking, and CI-integrated baseline update workflows.

Core Principles

  1. Baselines are contracts -- A screenshot baseline is a visual contract that says "this is what the page should look like." Any deviation from this contract must be intentional and reviewed. Treat baseline updates with the same rigor as code changes.
  2. Determinism is foundational -- A screenshot test that produces different images on each run is worse than no test. Eliminate all sources of non-determinism before establishing baselines: animations, timestamps, random content, external images, and network-dependent resources.
  3. Responsive coverage is not optional -- Users access web applications at dozens of viewport sizes. A visual test that only checks desktop resolution misses layout breakages that affect the majority of users on mobile and tablet devices.
  4. Threshold tuning is an art -- A zero-threshold comparison flags anti-aliasing differences as failures. An overly generous threshold misses real regressions. Calibrate thresholds per component based on its visual complexity.
  5. Dynamic content must be masked, not ignored -- Dates, advertisements, user avatars, and randomized content change between runs. Mask these regions with deterministic placeholders rather than excluding them from comparison entirely.
  6. Component-level baselines complement page-level baselines -- Full-page screenshots catch layout shifts but produce large diffs for small changes. Component-level screenshots provide precise, reviewable diffs for individual UI elements.
  7. Cross-browser baselines are separate baselines -- Chrome and Firefox render fonts, shadows, and gradients differently. Maintain separate baselines per browser rather than using a single baseline with generous tolerance.
  8. Baseline updates require human approval -- Automated baseline updates bypass the purpose of visual testing. Every baseline change should be reviewed in a pull request with before/after comparison.
  9. Animations are the enemy of stability -- CSS animations, transitions, skeleton loaders, and cursor blinks cause pixel-level differences between runs. Disable or wait for animations to complete before capturing screenshots.
  10. Font loading affects every pixel -- A screenshot captured before web fonts load looks completely different from one captured after. Wait for font loading to complete before any capture.

Project Structure

tests/
visual/
baselines/
desktop/
homepage.png
dashboard.png
settings.png
tablet/
homepage.png
dashboard.png
mobile/
homepage.png
dashboard.png
components/
baselines/
button-primary.png
card-product.png
navigation-header.png
modal-dialog.png
helpers/
screenshot-capture.ts
baseline-manager.ts
mask-builder.ts
animation-disabler.ts
font-loader.ts
viewport-manager.ts
tests/
homepage.visual.test.ts
dashboard.visual.test.ts
components.visual.test.ts
responsive.visual.test.ts
cross-browser.visual.test.ts
config/
visual-test.config.ts
viewports.ts
masks.ts
reports/
diff-reporter.ts
baseline-review.ts

Playwright Screenshot Comparison API

Playwright provides built-in screenshot comparison through toHaveScreenshot(). Understanding its API is the foundation for all visual testing.

Basic Screenshot Comparison
typescript
// homepage.visual.test.ts
import { test, expect } from '@playwright/test';
test.describe('Homepage Visual Regression', () => {
test.beforeEach(async ({ page }) => {
// Navigate and wait for full load
await page.goto('/', { waitUntil: 'networkidle' });
// Wait for web fonts to load
await page.evaluate(() => document.fonts.ready);
// Disable animations globally
await page.addStyleTag({
content: `
*, *::before, *::after {
animation-duration: 0s !important;
animation-delay: 0s !important;
transition-duration: 0s !important;
transition-delay: 0s !important;
scroll-behavior: auto !important;
}
`,
});
});
test('should match homepage baseline', async ({ page }) => {
await expect(page).toHaveScreenshot('homepage.png', {
fullPage: true,
maxDiffPixels: 100,
});
});
test('should match homepage hero section', async ({ page }) => {
const hero = page.locator('[data-testid="hero-section"]');
await expect(hero).toHaveScreenshot('hero-section.png', {
maxDiffPixelRatio: 0.01,
});
});
test('should match homepage after scrolling to features', async ({ page }) => {
const features = page.locator('[data-testid="features-section"]');
await features.scrollIntoViewIfNeeded();
await page.waitForTimeout(300); // Wait for any scroll-triggered animations
await expect(features).toHaveScreenshot('features-section.png');
});
});
Advanced Screenshot Configuration
typescript
// screenshot-capture.ts
import { Page, Locator, expect } from '@playwright/test';
interface ScreenshotOptions {
name: string;
fullPage?: boolean;
maxDiffPixels?: number;
maxDiffPixelRatio?: number;
threshold?: number;
mask?: Locator[];
maskColor?: string;
animations?: 'disabled' | 'allow';
caret?: 'hide' | 'initial';
scale?: 'css' | 'device';
timeout?: number;
}
class ScreenshotCapture {
constructor(private page: Page) {}
async prepareForCapture(): Promise<void> {
// 1. Wait for network to settle
await this.page.waitForLoadState('networkidle');
// 2. Wait for all fonts to load
await this.page.evaluate(() => document.fonts.ready);
// 3. Wait for all images to load
await this.page.evaluate(async () => {
const images = Array.from(document.querySelectorAll('img'));
await Promise.all(
images.map(img => {
if (img.complete) return Promise.resolve();
return new Promise((resolve, reject) => {
img.addEventListener('load', resolve);
img.addEventListener('error', reject);
});
})
);
});
// 4. Disable all animations and transitions
await this.page.addStyleTag({
content: `
*, *::before, *::after {
animation-duration: 0s !important;
animation-delay: 0s !important;
transition-duration: 0s !important;
transition-delay: 0s !important;
caret-color: transparent !important;
}
/* Disable specific problem animations */
.skeleton-loader { animation: none !important; opacity: 1 !important; }
.spinner { animation: none !important; display: none !important; }
video, .video-player { display: none !important; }
`,
});
// 5. Wait for any remaining React/Vue hydration
await this.page.waitForTimeout(500);
// 6. Scroll to top for consistent starting position
await this.page.evaluate(() => window.scrollTo(0, 0));
}
async captureFullPage(options: ScreenshotOptions): Promise<void> {
await this.prepareForCapture();
await expect(this.page).toHaveScreenshot(options.name, {
fullPage: true,
maxDiffPixels: options.maxDiffPixels ?? 100,
maxDiffPixelRatio: options.maxDiffPixelRatio,
threshold: options.threshold ?? 0.2,
mask: options.mask ?? [],
maskColor: options.maskColor ?? '#FF00FF',
animations: 'disabled',
caret: 'hide',
scale: options.scale ?? 'css',
timeout: options.timeout ?? 30000,
});
}
async captureElement(
locator: Locator,
options: ScreenshotOptions
): Promise<void> {
await this.prepareForCapture();
await locator.scrollIntoViewIfNeeded();
await this.page.waitForTimeout(200);
await expect(locator).toHaveScreenshot(options.name, {
maxDiffPixels: options.maxDiffPixels ?? 50,
maxDiffPixelRatio: options.maxDiffPixelRatio,
threshold: options.threshold ?? 0.2,
mask: options.mask ?? [],
maskColor: options.maskColor ?? '#FF00FF',
animations: 'disabled',
caret: 'hide',
timeout: options.timeout ?? 15000,
});
}
async captureViewport(
viewportWidth: number,
viewportHeight: number,
options: ScreenshotOptions
): Promise<void> {
await this.page.setViewportSize({
width: viewportWidth,
height: viewportHeight,
});
await this.page.waitForTimeout(500); // Wait for responsive layout to settle
await this.captureFullPage(options);
}
}

Dynamic Content Masking

Dynamic content is the primary source of false positives in visual regression testing. Masking replaces dynamic regions with a solid color before comparison.

typescript
// mask-builder.ts
import { Page, Locator } from '@playwright/test';
interface MaskDefinition {
selector: string;
reason: string;
maskColor?: string;
}
class MaskBuilder {
private masks: MaskDefinition[] = [];
/**
* Add common masks that apply to most pages
*/
addCommonMasks(): MaskBuilder {
this.masks.push(
{ selector: '[data-testid="current-date"]', reason: 'Dynamic date display' },
{ selector: '[data-testid="current-time"]', reason: 'Dynamic time display' },
{ selector: '[data-testid="user-avatar"]', reason: 'User-specific avatar' },
{ selector: '.relative-time', reason: 'Relative timestamps (e.g., "2 hours ago")' },
{ selector: '[data-testid="notification-count"]', reason: 'Dynamic notification badge' },
{ selector: '.ad-container', reason: 'Advertisement content' },
{ selector: 'iframe[src*="youtube"]', reason: 'Embedded video' },
{ selector: 'iframe[src*="maps"]', reason: 'Embedded map' },
{ selector: '.analytics-widget', reason: 'Live analytics data' },
{ selector: '[data-testid="random-testimonial"]', reason: 'Randomized content' },
);
return this;
}
/**
* Add page-specific masks
*/
addMask(selector: string, reason: string): MaskBuilder {
this.masks.push({ selector, reason });
return this;
}
/**
* Resolve all mask definitions to Playwright Locators
*/
resolve(page: Page): Locator[] {
return this.masks
.map(mask => {
const locator = page.locator(mask.selector);
return locator;
});
}
/**
* Alternative: Replace dynamic content with deterministic placeholders
* This is more stable than masking because it preserves layout
*/
async replaceDynamicContent(page: Page): Promise<void> {
await page.evaluate(() => {
// Replace all relative timestamps with a fixed value
document.querySelectorAll('.relative-time, time[datetime]').forEach(el => {
el.textContent = 'Jan 1, 2024';
});
// Replace all avatars with a placeholder
document.querySelectorAll<HTMLImageElement>('img[data-testid="user-avatar"]').forEach(img => {
img.src = 'data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" width="40" height="40"><rect fill="%23ccc" width="40" height="40"/></svg>';
});
// Replace notification counts
document.querySelectorAll('[data-testid="notification-count"]').forEach(el => {
el.textContent = '0';
});
// Replace random content with deterministic content
document.querySelectorAll('[data-randomized]').forEach(el => {
el.textContent = 'Deterministic placeholder';
});

This is the start of the file. Read the full SKILL.md on GitHub.

Security

  • Security scanPassed, 100 of 100Pattern checks on the skill's files.
  • Model reviewPassed

Quality evaluation

  • Task evaluationNot runNo evaluation yet. Run one in Skill Studio.

Source

Popularity

Repository stars
118
Repository forks
6
7-day trend score
24

Stars and forks are for the whole GitHub repository, not this skill alone. Refreshed 14 May 2026, 5 months ago.

Works with

Any agent vskill installs to: 56 targets in all. These 39 have a page on this site.

  • Claude Code
  • Cursor
  • GitHub Copilot
  • Windsurf
  • Codex
  • Gemini CLI
  • Cline
  • Amp
  • Roo Code
  • Goose
  • Continue
  • Kilo Code
  • Junie
  • Trae
  • OpenHands
  • Qwen Code
  • AI Agent
  • Tabnine Chat
  • Sourcegraph Cody
  • Amazon Q
  • Firebase Genkit
  • Sweep
  • Devin
  • Phind
  • Mentat
  • Claude Desktop
  • Aider
  • GPT Pilot
  • Plandex
  • bolt.new
  • v0
  • Replit Agent
  • Anse
  • TypingMind
  • BetterChatGPT
  • LibreChat
  • Warp AI
  • Pieces
  • Fig AI
Embed badge
vskill VERIFIED badge
[![vskill VERIFIED](https://verified-skill.com/api/v1/skills/pramoddutta/qaskills/screenshot-baseline-generator/badge)](https://verified-skill.com/skills/pramoddutta/qaskills/screenshot-baseline-generator)

Something wrong with this skill or its page? Tell us.