Visual regression tests refined

This commit is contained in:
adilallo
2025-08-29 13:42:25 -06:00
parent e8691efac4
commit f7621d2086
6 changed files with 1421 additions and 7 deletions
+242
View File
@@ -0,0 +1,242 @@
import { test, expect } from "@playwright/test";
const breakpoints = [
{ name: "xs", width: 320, height: 700 },
{ name: "sm", width: 360, height: 700 },
{ name: "md", width: 480, height: 700 },
{ name: "lg", width: 640, height: 700 },
{ name: "xl", width: 768, height: 700 },
{ name: "2xl", width: 1024, height: 700 },
{ name: "3xl", width: 1280, height: 700 },
{ name: "4xl", width: 1440, height: 700 },
{ name: "full", width: 1920, height: 700 },
];
for (const bp of breakpoints) {
test.describe(`Footer responsive behavior at ${bp.name} breakpoint`, () => {
test.beforeEach(async ({ page }) => {
await page.setViewportSize({ width: bp.width, height: bp.height });
await page.goto("/");
});
test(`footer layout at ${bp.name}`, async ({ page }) => {
const footer = page.getByRole("contentinfo");
await expect(footer).toBeVisible();
// Check that footer content is visible
const footerContent = page.locator("footer");
await expect(footerContent).toBeVisible();
});
test(`footer navigation items visibility at ${bp.name}`, async ({
page,
}) => {
// All breakpoints should have navigation items
await expect(
page.getByRole("link", { name: /use cases/i })
).toBeVisible();
await expect(page.getByRole("link", { name: /learn/i })).toBeVisible();
await expect(page.getByRole("link", { name: /about/i })).toBeVisible();
});
test(`footer legal links visibility at ${bp.name}`, async ({ page }) => {
// All breakpoints should have legal links
await expect(
page.getByRole("link", { name: /privacy policy/i })
).toBeVisible();
await expect(
page.getByRole("link", { name: /terms of service/i })
).toBeVisible();
});
test(`footer social links visibility at ${bp.name}`, async ({ page }) => {
// All breakpoints should have social links
await expect(
page.getByRole("link", { name: /follow us on bluesky/i })
).toBeVisible();
await expect(
page.getByRole("link", { name: /follow us on gitlab/i })
).toBeVisible();
});
test(`footer logo visibility at ${bp.name}`, async ({ page }) => {
// Logo should be visible at all breakpoints
const logo = page.locator('[data-testid="logo-wrapper"]').first();
await expect(logo).toBeVisible();
});
// Breakpoint-specific tests
if (bp.name === "xs") {
test("xs breakpoint specific behavior", async ({ page }) => {
// At xs, footer should stack vertically
const footer = page.locator("footer");
await expect(footer).toBeVisible();
// Check that content is properly stacked
const footerContent = page.locator("footer > div");
await expect(footerContent).toBeVisible();
});
}
if (bp.name === "md") {
test("md breakpoint specific behavior", async ({ page }) => {
// At md, footer should have proper spacing
const footer = page.locator("footer");
await expect(footer).toBeVisible();
});
}
if (bp.name === "xl") {
test("xl breakpoint specific behavior", async ({ page }) => {
// At xl, footer should have full layout
const footer = page.locator("footer");
await expect(footer).toBeVisible();
});
}
});
}
// Visual regression tests
test.describe("Footer visual regression", () => {
test("footer visual consistency across breakpoints", async ({ page }) => {
// Test visual consistency at all breakpoints
for (const bp of breakpoints) {
await page.setViewportSize({ width: bp.width, height: bp.height });
await page.goto("/");
// Scroll to footer
await page.evaluate(() => window.scrollTo(0, document.body.scrollHeight));
await page.waitForTimeout(500);
// Take a screenshot for visual regression testing
await expect(page.locator("footer").first()).toHaveScreenshot(
`footer-${bp.name}.png`
);
}
});
test("footer hover states visual consistency", async ({ page }) => {
// Test hover states at key breakpoints
const keyBreakpoints = [
{ name: "xs", width: 320, height: 700 },
{ name: "md", width: 768, height: 700 },
{ name: "xl", width: 1280, height: 700 },
];
for (const bp of keyBreakpoints) {
await page.setViewportSize({ width: bp.width, height: bp.height });
await page.goto("/");
// Scroll to footer
await page.evaluate(() => window.scrollTo(0, document.body.scrollHeight));
await page.waitForTimeout(500);
// Test hover on navigation items
const useCasesLink = page.getByRole("link", { name: /use cases/i });
await useCasesLink.hover();
await page.waitForTimeout(200);
await expect(page.locator("footer").first()).toHaveScreenshot(
`footer-${bp.name}-hover-nav.png`
);
// Test hover on social links
const blueskyLink = page.getByRole("link", {
name: /follow us on bluesky/i,
});
await blueskyLink.hover();
await page.waitForTimeout(200);
await expect(page.locator("footer").first()).toHaveScreenshot(
`footer-${bp.name}-hover-social.png`
);
}
});
test("footer focus states visual consistency", async ({ page }) => {
// Test focus states at key breakpoints
const keyBreakpoints = [
{ name: "xs", width: 320, height: 700 },
{ name: "md", width: 768, height: 700 },
{ name: "xl", width: 1280, height: 700 },
];
for (const bp of keyBreakpoints) {
await page.setViewportSize({ width: bp.width, height: bp.height });
await page.goto("/");
// Scroll to footer
await page.evaluate(() => window.scrollTo(0, document.body.scrollHeight));
await page.waitForTimeout(500);
// Test focus on navigation items
const useCasesLink = page.getByRole("link", { name: /use cases/i });
await useCasesLink.focus();
await page.waitForTimeout(200);
await expect(page.locator("footer").first()).toHaveScreenshot(
`footer-${bp.name}-focus-nav.png`
);
// Test focus on social links
const blueskyLink = page.getByRole("link", {
name: /follow us on bluesky/i,
});
await blueskyLink.focus();
await page.waitForTimeout(200);
await expect(page.locator("footer").first()).toHaveScreenshot(
`footer-${bp.name}-focus-social.png`
);
}
});
});
// Additional responsive behavior tests
test.describe("Footer responsive behavior", () => {
test("footer maintains proper layout across breakpoints", async ({
page,
}) => {
// Test that footer doesn't break at edge cases
const edgeCases = [
{ width: 320, height: 700 }, // Very small
{ width: 1920, height: 700 }, // Very large
];
for (const viewport of edgeCases) {
await page.setViewportSize(viewport);
await page.goto("/");
// Scroll to footer
await page.evaluate(() => window.scrollTo(0, document.body.scrollHeight));
const footer = page.getByRole("contentinfo");
await expect(footer).toBeVisible();
}
});
test("footer elements are properly accessible across breakpoints", async ({
page,
}) => {
// Test accessibility at different breakpoints
for (const bp of breakpoints) {
await page.setViewportSize({ width: bp.width, height: bp.height });
await page.goto("/");
// Scroll to footer
await page.evaluate(() => window.scrollTo(0, document.body.scrollHeight));
// Check that all interactive elements are accessible
const interactiveElements = [
page.getByRole("link", { name: /use cases/i }),
page.getByRole("link", { name: /learn/i }),
page.getByRole("link", { name: /about/i }),
page.getByRole("link", { name: /privacy policy/i }),
page.getByRole("link", { name: /terms of service/i }),
page.getByRole("link", { name: /follow us on bluesky/i }),
page.getByRole("link", { name: /follow us on gitlab/i }),
];
for (const element of interactiveElements) {
await expect(element).toBeVisible();
await expect(element).toBeEnabled();
}
}
});
});
+92 -5
View File
@@ -1,11 +1,15 @@
import { test, expect } from "@playwright/test";
const breakpoints = [
{ name: "xs", width: 360, height: 700 },
{ name: "sm", width: 640, height: 700 },
{ name: "md", width: 768, height: 700 },
{ name: "lg", width: 1024, height: 700 },
{ name: "xl", width: 1280, height: 700 },
{ name: "xs", width: 320, height: 700 },
{ name: "sm", width: 360, height: 700 },
{ name: "md", width: 480, height: 700 },
{ name: "lg", width: 640, height: 700 },
{ name: "xl", width: 768, height: 700 },
{ name: "2xl", width: 1024, height: 700 },
{ name: "3xl", width: 1280, height: 700 },
{ name: "4xl", width: 1440, height: 700 },
{ name: "full", width: 1920, height: 700 },
];
for (const bp of breakpoints) {
@@ -132,6 +136,89 @@ for (const bp of breakpoints) {
});
}
// Visual regression tests
test.describe("Header visual regression", () => {
test("header visual consistency across breakpoints", async ({ page }) => {
// Test visual consistency at all breakpoints
for (const bp of breakpoints) {
await page.setViewportSize({ width: bp.width, height: bp.height });
await page.goto("/");
// Wait for layout to stabilize
await page.waitForTimeout(500);
// Take a screenshot for visual regression testing
await expect(page.locator("header").first()).toHaveScreenshot(
`header-${bp.name}.png`
);
}
});
test("header hover states visual consistency", async ({ page }) => {
// Test hover states at key breakpoints
const keyBreakpoints = [
{ name: "xs", width: 320, height: 700 },
{ name: "md", width: 768, height: 700 },
{ name: "xl", width: 1280, height: 700 },
];
for (const bp of keyBreakpoints) {
await page.setViewportSize({ width: bp.width, height: bp.height });
await page.goto("/");
// Test hover on navigation items
const useCasesLink = page.getByRole("link", { name: /use cases/i });
await useCasesLink.hover();
await page.waitForTimeout(200);
await expect(page.locator("header").first()).toHaveScreenshot(
`header-${bp.name}-hover-nav.png`
);
// Test hover on create rule button
const createRuleButton = page.getByRole("button", {
name: /create a new rule with avatar decoration/i,
});
await createRuleButton.hover();
await page.waitForTimeout(200);
await expect(page.locator("header").first()).toHaveScreenshot(
`header-${bp.name}-hover-button.png`
);
}
});
test("header focus states visual consistency", async ({ page }) => {
// Test focus states at key breakpoints
const keyBreakpoints = [
{ name: "xs", width: 320, height: 700 },
{ name: "md", width: 768, height: 700 },
{ name: "xl", width: 1280, height: 700 },
];
for (const bp of keyBreakpoints) {
await page.setViewportSize({ width: bp.width, height: bp.height });
await page.goto("/");
// Test focus on navigation items
const useCasesLink = page.getByRole("link", { name: /use cases/i });
await useCasesLink.focus();
await page.waitForTimeout(200);
await expect(page.locator("header").first()).toHaveScreenshot(
`header-${bp.name}-focus-nav.png`
);
// Test focus on create rule button
const createRuleButton = page.getByRole("button", {
name: /create a new rule with avatar decoration/i,
});
await createRuleButton.focus();
await page.waitForTimeout(200);
await expect(page.locator("header").first()).toHaveScreenshot(
`header-${bp.name}-focus-button.png`
);
}
});
});
// Additional responsive behavior tests
test.describe("Header responsive behavior", () => {
test("header maintains proper layout across breakpoints", async ({
+215
View File
@@ -0,0 +1,215 @@
/**
* Visual Regression Testing Configuration
*
* This file defines the configuration for visual regression testing across
* different breakpoints, components, and scenarios.
*/
// Breakpoint definitions for responsive testing
export const breakpoints = {
// Mobile breakpoints
xs: { width: 320, height: 700, name: "Extra Small" },
sm: { width: 360, height: 700, name: "Small" },
md: { width: 480, height: 700, name: "Medium" },
// Tablet breakpoints
lg: { width: 640, height: 700, name: "Large" },
xl: { width: 768, height: 700, name: "Extra Large" },
// Desktop breakpoints
"2xl": { width: 1024, height: 700, name: "2XL" },
"3xl": { width: 1280, height: 700, name: "3XL" },
"4xl": { width: 1440, height: 700, name: "4XL" },
full: { width: 1920, height: 700, name: "Full HD" },
};
// Key breakpoints for focused testing
export const keyBreakpoints = [
breakpoints.xs, // Mobile
breakpoints.md, // Tablet
breakpoints.xl, // Desktop
];
// Visual testing scenarios
export const visualScenarios = {
// Component states
states: {
default: "Default state",
hover: "Hover state",
focus: "Focus state",
active: "Active/pressed state",
disabled: "Disabled state",
},
// Interactive states
interactions: {
hover: "Element hovered",
focus: "Element focused",
click: "Element clicked",
loading: "Loading state",
error: "Error state",
},
// Content variations
content: {
short: "Short content",
long: "Long content",
empty: "Empty state",
loading: "Loading content",
error: "Error content",
},
// Layout scenarios
layout: {
compact: "Compact layout",
spacious: "Spacious layout",
stacked: "Stacked layout",
grid: "Grid layout",
list: "List layout",
},
};
// Chromatic configuration
export const chromaticConfig = {
// Viewports for Chromatic screenshots
viewports: Object.values(breakpoints).map((bp) => bp.width),
// Delay for layout stabilization
delay: 200,
// Modes for different themes
modes: {
light: {},
dark: {
colorScheme: "dark",
},
},
// Storybook viewport configuration
storybookViewports: Object.entries(breakpoints).reduce((acc, [key, bp]) => {
acc[key] = {
name: bp.name,
styles: {
width: `${bp.width}px`,
height: `${bp.height}px`,
},
};
return acc;
}, {}),
};
// Playwright visual testing configuration
export const playwrightVisualConfig = {
// Screenshot options
screenshot: {
fullPage: false,
type: "png",
quality: 90,
},
// Visual comparison options
visualComparison: {
threshold: 0.1, // 10% difference threshold
maxDiffPixels: 100,
maxDiffPixelRatio: 0.1,
},
// Test timeouts
timeouts: {
navigation: 30000,
action: 5000,
assertion: 10000,
},
};
// Component-specific visual testing configurations
export const componentConfigs = {
Header: {
breakpoints: [breakpoints.xs, breakpoints.md, breakpoints.xl],
states: ["default", "hover", "focus"],
scenarios: ["navigation", "authentication", "responsive"],
},
Footer: {
breakpoints: [breakpoints.xs, breakpoints.md, breakpoints.xl],
states: ["default", "hover", "focus"],
scenarios: ["navigation", "social", "legal"],
},
Button: {
breakpoints: [breakpoints.sm, breakpoints.md, breakpoints.lg],
states: ["default", "hover", "focus", "active", "disabled"],
variants: ["default", "home"],
sizes: ["xsmall", "small", "medium", "large", "xlarge"],
},
Logo: {
breakpoints: [breakpoints.xs, breakpoints.md, breakpoints.xl],
states: ["default", "hover"],
variants: ["with-text", "icon-only"],
},
MenuBar: {
breakpoints: [breakpoints.xs, breakpoints.md, breakpoints.xl],
states: ["default", "hover", "focus"],
scenarios: ["navigation", "dropdown"],
},
};
// Visual regression test patterns
export const testPatterns = {
// Basic component testing
basic: {
description: "Basic component rendering",
steps: [
"Navigate to component",
"Wait for layout stabilization",
"Take screenshot",
],
},
// Interactive state testing
interactive: {
description: "Interactive state testing",
steps: [
"Navigate to component",
"Interact with element (hover/focus/click)",
"Wait for state change",
"Take screenshot",
],
},
// Responsive testing
responsive: {
description: "Responsive behavior testing",
steps: [
"Set viewport size",
"Navigate to component",
"Wait for layout stabilization",
"Take screenshot",
"Repeat for all breakpoints",
],
},
// Content variation testing
contentVariation: {
description: "Content variation testing",
steps: [
"Navigate to component with different content",
"Wait for layout stabilization",
"Take screenshot",
"Compare with baseline",
],
},
};
// Export all configurations
export default {
breakpoints,
keyBreakpoints,
visualScenarios,
chromaticConfig,
playwrightVisualConfig,
componentConfigs,
testPatterns,
};