Visual regression tests refined
This commit is contained in:
@@ -0,0 +1,353 @@
|
|||||||
|
import Button from "../app/components/Button.js";
|
||||||
|
import { within, userEvent } from "@storybook/testing-library";
|
||||||
|
|
||||||
|
export default {
|
||||||
|
title: "Components/Button/Visual Regression",
|
||||||
|
component: Button,
|
||||||
|
parameters: {
|
||||||
|
// Chromatic configuration for visual testing
|
||||||
|
chromatic: {
|
||||||
|
viewports: [320, 640, 1024, 1280],
|
||||||
|
delay: 200,
|
||||||
|
modes: {
|
||||||
|
light: {},
|
||||||
|
dark: {
|
||||||
|
colorScheme: "dark",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
argTypes: {
|
||||||
|
size: {
|
||||||
|
control: { type: "select" },
|
||||||
|
options: ["xsmall", "small", "medium", "large", "xlarge"],
|
||||||
|
},
|
||||||
|
variant: {
|
||||||
|
control: { type: "select" },
|
||||||
|
options: ["default", "home"],
|
||||||
|
},
|
||||||
|
disabled: {
|
||||||
|
control: { type: "boolean" },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
// Default button states
|
||||||
|
export const Default = {
|
||||||
|
args: {
|
||||||
|
children: "Default Button",
|
||||||
|
},
|
||||||
|
parameters: {
|
||||||
|
docs: {
|
||||||
|
description: {
|
||||||
|
story: "Default button state for visual regression testing.",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export const Hover = {
|
||||||
|
args: {
|
||||||
|
children: "Hover Button",
|
||||||
|
},
|
||||||
|
parameters: {
|
||||||
|
docs: {
|
||||||
|
description: {
|
||||||
|
story: "Button in hover state for visual regression testing.",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
play: async ({ canvasElement }) => {
|
||||||
|
const canvas = within(canvasElement);
|
||||||
|
const button = canvas.getByRole("button");
|
||||||
|
await userEvent.hover(button);
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, 100));
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export const Focus = {
|
||||||
|
args: {
|
||||||
|
children: "Focus Button",
|
||||||
|
},
|
||||||
|
parameters: {
|
||||||
|
docs: {
|
||||||
|
description: {
|
||||||
|
story: "Button in focus state for visual regression testing.",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
play: async ({ canvasElement }) => {
|
||||||
|
const canvas = within(canvasElement);
|
||||||
|
const button = canvas.getByRole("button");
|
||||||
|
button.focus();
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, 100));
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export const Active = {
|
||||||
|
args: {
|
||||||
|
children: "Active Button",
|
||||||
|
},
|
||||||
|
parameters: {
|
||||||
|
docs: {
|
||||||
|
description: {
|
||||||
|
story: "Button in active/pressed state for visual regression testing.",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
play: async ({ canvasElement }) => {
|
||||||
|
const canvas = within(canvasElement);
|
||||||
|
const button = canvas.getByRole("button");
|
||||||
|
await userEvent.click(button);
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, 100));
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export const Disabled = {
|
||||||
|
args: {
|
||||||
|
children: "Disabled Button",
|
||||||
|
disabled: true,
|
||||||
|
},
|
||||||
|
parameters: {
|
||||||
|
docs: {
|
||||||
|
description: {
|
||||||
|
story: "Disabled button state for visual regression testing.",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
// Size variants
|
||||||
|
export const XSmall = {
|
||||||
|
args: {
|
||||||
|
children: "XSmall Button",
|
||||||
|
size: "xsmall",
|
||||||
|
},
|
||||||
|
parameters: {
|
||||||
|
docs: {
|
||||||
|
description: {
|
||||||
|
story: "Extra small button size for visual regression testing.",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export const Small = {
|
||||||
|
args: {
|
||||||
|
children: "Small Button",
|
||||||
|
size: "small",
|
||||||
|
},
|
||||||
|
parameters: {
|
||||||
|
docs: {
|
||||||
|
description: {
|
||||||
|
story: "Small button size for visual regression testing.",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export const Medium = {
|
||||||
|
args: {
|
||||||
|
children: "Medium Button",
|
||||||
|
size: "medium",
|
||||||
|
},
|
||||||
|
parameters: {
|
||||||
|
docs: {
|
||||||
|
description: {
|
||||||
|
story: "Medium button size for visual regression testing.",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export const Large = {
|
||||||
|
args: {
|
||||||
|
children: "Large Button",
|
||||||
|
size: "large",
|
||||||
|
},
|
||||||
|
parameters: {
|
||||||
|
docs: {
|
||||||
|
description: {
|
||||||
|
story: "Large button size for visual regression testing.",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export const XLarge = {
|
||||||
|
args: {
|
||||||
|
children: "XLarge Button",
|
||||||
|
size: "xlarge",
|
||||||
|
},
|
||||||
|
parameters: {
|
||||||
|
docs: {
|
||||||
|
description: {
|
||||||
|
story: "Extra large button size for visual regression testing.",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
// Variant styles
|
||||||
|
export const HomeVariant = {
|
||||||
|
args: {
|
||||||
|
children: "Home Button",
|
||||||
|
variant: "home",
|
||||||
|
},
|
||||||
|
parameters: {
|
||||||
|
docs: {
|
||||||
|
description: {
|
||||||
|
story: "Home variant button for visual regression testing.",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
// Button with icon/content
|
||||||
|
export const WithIcon = {
|
||||||
|
args: {
|
||||||
|
children: (
|
||||||
|
<>
|
||||||
|
<span>Button with Icon</span>
|
||||||
|
<svg width="16" height="16" viewBox="0 0 16 16" fill="currentColor">
|
||||||
|
<path d="M8 4a.5.5 0 0 1 .5.5v3h3a.5.5 0 0 1 0 1h-3v3a.5.5 0 0 1-1 0v-3h-3a.5.5 0 0 1 0-1h3v-3A.5.5 0 0 1 8 4z" />
|
||||||
|
</svg>
|
||||||
|
</>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
parameters: {
|
||||||
|
docs: {
|
||||||
|
description: {
|
||||||
|
story: "Button with icon for visual regression testing.",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export const LongText = {
|
||||||
|
args: {
|
||||||
|
children:
|
||||||
|
"This is a button with very long text content that might wrap or overflow",
|
||||||
|
},
|
||||||
|
parameters: {
|
||||||
|
docs: {
|
||||||
|
description: {
|
||||||
|
story: "Button with long text for visual regression testing.",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
// Button grid for comparison
|
||||||
|
export const SizeComparison = {
|
||||||
|
render: () => (
|
||||||
|
<div className="space-y-4 p-4">
|
||||||
|
<div className="flex flex-wrap gap-4 items-center">
|
||||||
|
<Button size="xsmall">XSmall</Button>
|
||||||
|
<Button size="small">Small</Button>
|
||||||
|
<Button size="medium">Medium</Button>
|
||||||
|
<Button size="large">Large</Button>
|
||||||
|
<Button size="xlarge">XLarge</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
parameters: {
|
||||||
|
docs: {
|
||||||
|
description: {
|
||||||
|
story: "All button sizes for comparison and visual regression testing.",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export const StateComparison = {
|
||||||
|
render: () => (
|
||||||
|
<div className="space-y-4 p-4">
|
||||||
|
<div className="flex flex-wrap gap-4 items-center">
|
||||||
|
<Button>Default</Button>
|
||||||
|
<Button disabled>Disabled</Button>
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-wrap gap-4 items-center">
|
||||||
|
<Button variant="home">Home Default</Button>
|
||||||
|
<Button variant="home" disabled>
|
||||||
|
Home Disabled
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
parameters: {
|
||||||
|
docs: {
|
||||||
|
description: {
|
||||||
|
story: "Button states for comparison and visual regression testing.",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
// Interactive states
|
||||||
|
export const InteractiveStates = {
|
||||||
|
render: () => (
|
||||||
|
<div className="space-y-4 p-4">
|
||||||
|
<div className="flex flex-wrap gap-4 items-center">
|
||||||
|
<Button id="hover-test">Hover Me</Button>
|
||||||
|
<Button id="focus-test">Focus Me</Button>
|
||||||
|
<Button id="click-test">Click Me</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
parameters: {
|
||||||
|
docs: {
|
||||||
|
description: {
|
||||||
|
story: "Interactive button states for visual regression testing.",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
play: async ({ canvasElement, step }) => {
|
||||||
|
const canvas = within(canvasElement);
|
||||||
|
|
||||||
|
await step("Test hover state", async () => {
|
||||||
|
const hoverButton = canvas.getByRole("button", { name: "Hover Me" });
|
||||||
|
await userEvent.hover(hoverButton);
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, 100));
|
||||||
|
});
|
||||||
|
|
||||||
|
await step("Test focus state", async () => {
|
||||||
|
const focusButton = canvas.getByRole("button", { name: "Focus Me" });
|
||||||
|
focusButton.focus();
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, 100));
|
||||||
|
});
|
||||||
|
|
||||||
|
await step("Test click state", async () => {
|
||||||
|
const clickButton = canvas.getByRole("button", { name: "Click Me" });
|
||||||
|
await userEvent.click(clickButton);
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, 100));
|
||||||
|
});
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
// Edge cases
|
||||||
|
export const EdgeCases = {
|
||||||
|
render: () => (
|
||||||
|
<div className="space-y-4 p-4">
|
||||||
|
<div className="flex flex-wrap gap-4 items-center">
|
||||||
|
<Button size="xsmall">Very Small</Button>
|
||||||
|
<Button size="xlarge">Very Large</Button>
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-wrap gap-4 items-center">
|
||||||
|
<Button>Normal</Button>
|
||||||
|
<Button disabled>Disabled</Button>
|
||||||
|
<Button variant="home">Home</Button>
|
||||||
|
<Button variant="home" disabled>
|
||||||
|
Home Disabled
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
parameters: {
|
||||||
|
docs: {
|
||||||
|
description: {
|
||||||
|
story: "Edge cases for button visual regression testing.",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
@@ -0,0 +1,335 @@
|
|||||||
|
import Footer from "../app/components/Footer.js";
|
||||||
|
import { within, userEvent } from "@storybook/testing-library";
|
||||||
|
|
||||||
|
export default {
|
||||||
|
title: "Components/Footer/Responsive",
|
||||||
|
component: Footer,
|
||||||
|
parameters: {
|
||||||
|
// Chromatic configuration for responsive testing
|
||||||
|
chromatic: {
|
||||||
|
viewports: [320, 360, 480, 640, 768, 1024, 1280, 1440, 1920],
|
||||||
|
// Capture screenshots at each breakpoint
|
||||||
|
delay: 200, // Increased delay to ensure layout is stable
|
||||||
|
// Capture both light and dark themes if available
|
||||||
|
modes: {
|
||||||
|
light: {},
|
||||||
|
dark: {
|
||||||
|
// This will be used if dark mode is implemented
|
||||||
|
colorScheme: "dark",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
// Storybook viewport configuration
|
||||||
|
viewport: {
|
||||||
|
viewports: {
|
||||||
|
xs: {
|
||||||
|
name: "Extra Small (xs)",
|
||||||
|
styles: {
|
||||||
|
width: "360px",
|
||||||
|
height: "700px",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
sm: {
|
||||||
|
name: "Small (sm)",
|
||||||
|
styles: {
|
||||||
|
width: "640px",
|
||||||
|
height: "700px",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
md: {
|
||||||
|
name: "Medium (md)",
|
||||||
|
styles: {
|
||||||
|
width: "768px",
|
||||||
|
height: "700px",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
lg: {
|
||||||
|
name: "Large (lg)",
|
||||||
|
styles: {
|
||||||
|
width: "1024px",
|
||||||
|
height: "700px",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
xl: {
|
||||||
|
name: "Extra Large (xl)",
|
||||||
|
styles: {
|
||||||
|
width: "1280px",
|
||||||
|
height: "700px",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
xxl: {
|
||||||
|
name: "2XL (xxl)",
|
||||||
|
styles: {
|
||||||
|
width: "1440px",
|
||||||
|
height: "700px",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
full: {
|
||||||
|
name: "Full HD (full)",
|
||||||
|
styles: {
|
||||||
|
width: "1920px",
|
||||||
|
height: "700px",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
// Default story - will be captured at all viewports by Chromatic
|
||||||
|
export const Default = {
|
||||||
|
parameters: {
|
||||||
|
docs: {
|
||||||
|
description: {
|
||||||
|
story:
|
||||||
|
"Footer component at different breakpoints. Chromatic will capture screenshots at 320px, 360px, 480px, 640px, 768px, 1024px, 1280px, 1440px, and 1920px viewport widths to test responsive behavior.",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
// Story for each breakpoint to make testing easier
|
||||||
|
export const ExtraSmall = {
|
||||||
|
parameters: {
|
||||||
|
viewport: {
|
||||||
|
defaultViewport: "xs",
|
||||||
|
},
|
||||||
|
docs: {
|
||||||
|
description: {
|
||||||
|
story:
|
||||||
|
"Footer at extra small breakpoint (360px). Tests mobile layout and stacking behavior.",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export const Small = {
|
||||||
|
parameters: {
|
||||||
|
viewport: {
|
||||||
|
defaultViewport: "sm",
|
||||||
|
},
|
||||||
|
docs: {
|
||||||
|
description: {
|
||||||
|
story:
|
||||||
|
"Footer at small breakpoint (640px). Tests tablet layout and responsive behavior.",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export const Medium = {
|
||||||
|
parameters: {
|
||||||
|
viewport: {
|
||||||
|
defaultViewport: "md",
|
||||||
|
},
|
||||||
|
docs: {
|
||||||
|
description: {
|
||||||
|
story:
|
||||||
|
"Footer at medium breakpoint (768px). Tests small desktop layout.",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export const Large = {
|
||||||
|
parameters: {
|
||||||
|
viewport: {
|
||||||
|
defaultViewport: "lg",
|
||||||
|
},
|
||||||
|
docs: {
|
||||||
|
description: {
|
||||||
|
story: "Footer at large breakpoint (1024px). Tests desktop layout.",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export const ExtraLarge = {
|
||||||
|
parameters: {
|
||||||
|
viewport: {
|
||||||
|
defaultViewport: "xl",
|
||||||
|
},
|
||||||
|
docs: {
|
||||||
|
description: {
|
||||||
|
story:
|
||||||
|
"Footer at extra large breakpoint (1280px). Tests large desktop layout.",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export const TwoXL = {
|
||||||
|
parameters: {
|
||||||
|
viewport: {
|
||||||
|
defaultViewport: "xxl",
|
||||||
|
},
|
||||||
|
docs: {
|
||||||
|
description: {
|
||||||
|
story:
|
||||||
|
"Footer at 2XL breakpoint (1440px). Tests very large desktop layout.",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export const FullHD = {
|
||||||
|
parameters: {
|
||||||
|
viewport: {
|
||||||
|
defaultViewport: "full",
|
||||||
|
},
|
||||||
|
docs: {
|
||||||
|
description: {
|
||||||
|
story:
|
||||||
|
"Footer at Full HD breakpoint (1920px). Tests maximum desktop layout.",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
// Interactive story for testing user interactions
|
||||||
|
export const Interactive = {
|
||||||
|
parameters: {
|
||||||
|
docs: {
|
||||||
|
description: {
|
||||||
|
story:
|
||||||
|
"Interactive footer for testing user interactions. Check the Actions panel to see triggered events.",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
play: async ({ canvasElement, step }) => {
|
||||||
|
const canvas = within(canvasElement);
|
||||||
|
|
||||||
|
await step("Click footer links", async () => {
|
||||||
|
const useCasesLink = canvas.getByRole("link", { name: /use cases/i });
|
||||||
|
await userEvent.click(useCasesLink);
|
||||||
|
|
||||||
|
const learnLink = canvas.getByRole("link", { name: /learn/i });
|
||||||
|
await userEvent.click(learnLink);
|
||||||
|
|
||||||
|
const aboutLink = canvas.getByRole("link", { name: /about/i });
|
||||||
|
await userEvent.click(aboutLink);
|
||||||
|
|
||||||
|
const privacyLink = canvas.getByRole("link", { name: /privacy policy/i });
|
||||||
|
await userEvent.click(privacyLink);
|
||||||
|
|
||||||
|
const termsLink = canvas.getByRole("link", { name: /terms of service/i });
|
||||||
|
await userEvent.click(termsLink);
|
||||||
|
});
|
||||||
|
|
||||||
|
await step("Click social media links", async () => {
|
||||||
|
const blueskyLink = canvas.getByRole("link", {
|
||||||
|
name: /follow us on bluesky/i,
|
||||||
|
});
|
||||||
|
await userEvent.click(blueskyLink);
|
||||||
|
|
||||||
|
const gitlabLink = canvas.getByRole("link", {
|
||||||
|
name: /follow us on gitlab/i,
|
||||||
|
});
|
||||||
|
await userEvent.click(gitlabLink);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
// Story for testing hover states
|
||||||
|
export const HoverStates = {
|
||||||
|
parameters: {
|
||||||
|
docs: {
|
||||||
|
description: {
|
||||||
|
story:
|
||||||
|
"Footer with hover states visible. This story captures the visual appearance when elements are hovered.",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
play: async ({ canvasElement, step }) => {
|
||||||
|
const canvas = within(canvasElement);
|
||||||
|
|
||||||
|
await step("Hover over footer links", async () => {
|
||||||
|
const useCasesLink = canvas.getByRole("link", { name: /use cases/i });
|
||||||
|
await userEvent.hover(useCasesLink);
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, 100));
|
||||||
|
|
||||||
|
const learnLink = canvas.getByRole("link", { name: /learn/i });
|
||||||
|
await userEvent.hover(learnLink);
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, 100));
|
||||||
|
|
||||||
|
const aboutLink = canvas.getByRole("link", { name: /about/i });
|
||||||
|
await userEvent.hover(aboutLink);
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, 100));
|
||||||
|
});
|
||||||
|
|
||||||
|
await step("Hover over social media links", async () => {
|
||||||
|
const blueskyLink = canvas.getByRole("link", {
|
||||||
|
name: /follow us on bluesky/i,
|
||||||
|
});
|
||||||
|
await userEvent.hover(blueskyLink);
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, 100));
|
||||||
|
|
||||||
|
const gitlabLink = canvas.getByRole("link", {
|
||||||
|
name: /follow us on gitlab/i,
|
||||||
|
});
|
||||||
|
await userEvent.hover(gitlabLink);
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, 100));
|
||||||
|
});
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
// Story for testing with long content above
|
||||||
|
export const WithLongContent = {
|
||||||
|
render: () => (
|
||||||
|
<div className="min-h-screen bg-[var(--color-surface-default-primary)]">
|
||||||
|
<main className="p-8">
|
||||||
|
<div className="max-w-4xl mx-auto">
|
||||||
|
<h1 className="text-2xl font-bold text-white mb-4">
|
||||||
|
Footer with Long Content Above
|
||||||
|
</h1>
|
||||||
|
<p className="text-white mb-4">
|
||||||
|
This story tests how the footer looks with a lot of content above
|
||||||
|
it. This helps ensure the footer maintains its visual integrity in
|
||||||
|
real-world scenarios.
|
||||||
|
</p>
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
|
||||||
|
{Array.from({ length: 20 }, (_, i) => (
|
||||||
|
<div
|
||||||
|
key={i}
|
||||||
|
className="bg-[var(--color-surface-default-secondary)] p-4 rounded-lg"
|
||||||
|
>
|
||||||
|
<h3 className="text-white font-semibold mb-2">
|
||||||
|
Content Block {i + 1}
|
||||||
|
</h3>
|
||||||
|
<p className="text-[var(--color-content-default-secondary)] text-sm">
|
||||||
|
This is example content to show how the footer integrates with
|
||||||
|
page content. This block contains enough text to test layout
|
||||||
|
behavior.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
<Footer />
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
parameters: {
|
||||||
|
docs: {
|
||||||
|
description: {
|
||||||
|
story:
|
||||||
|
"Footer with long content above to test visual integration and layout stability.",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
// Story for testing edge cases
|
||||||
|
export const EdgeCases = {
|
||||||
|
parameters: {
|
||||||
|
viewport: {
|
||||||
|
defaultViewport: "xs",
|
||||||
|
},
|
||||||
|
docs: {
|
||||||
|
description: {
|
||||||
|
story:
|
||||||
|
"Footer at the smallest breakpoint to test edge case behavior and ensure no layout issues.",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
@@ -7,9 +7,17 @@ export default {
|
|||||||
parameters: {
|
parameters: {
|
||||||
// Chromatic configuration for responsive testing
|
// Chromatic configuration for responsive testing
|
||||||
chromatic: {
|
chromatic: {
|
||||||
viewports: [360, 640, 768, 1024, 1280],
|
viewports: [320, 360, 480, 640, 768, 1024, 1280, 1440, 1920],
|
||||||
// Capture screenshots at each breakpoint
|
// Capture screenshots at each breakpoint
|
||||||
delay: 100, // Small delay to ensure layout is stable
|
delay: 200, // Increased delay to ensure layout is stable
|
||||||
|
// Capture both light and dark themes if available
|
||||||
|
modes: {
|
||||||
|
light: {},
|
||||||
|
dark: {
|
||||||
|
// This will be used if dark mode is implemented
|
||||||
|
colorScheme: "dark",
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
// Storybook viewport configuration
|
// Storybook viewport configuration
|
||||||
viewport: {
|
viewport: {
|
||||||
@@ -49,6 +57,20 @@ export default {
|
|||||||
height: "700px",
|
height: "700px",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
xxl: {
|
||||||
|
name: "2XL (xxl)",
|
||||||
|
styles: {
|
||||||
|
width: "1440px",
|
||||||
|
height: "700px",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
full: {
|
||||||
|
name: "Full HD (full)",
|
||||||
|
styles: {
|
||||||
|
width: "1920px",
|
||||||
|
height: "700px",
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -198,3 +220,163 @@ export const Interactive = {
|
|||||||
});
|
});
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Story for testing hover states
|
||||||
|
export const HoverStates = {
|
||||||
|
args: {
|
||||||
|
onToggle: () => console.log("Navigation toggled"),
|
||||||
|
},
|
||||||
|
parameters: {
|
||||||
|
docs: {
|
||||||
|
description: {
|
||||||
|
story:
|
||||||
|
"Header with hover states visible. This story captures the visual appearance when elements are hovered.",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
play: async ({ canvasElement, step }) => {
|
||||||
|
const canvas = within(canvasElement);
|
||||||
|
|
||||||
|
await step("Hover over navigation items", async () => {
|
||||||
|
const useCasesLink = canvas.getByRole("link", { name: /use cases/i });
|
||||||
|
await userEvent.hover(useCasesLink);
|
||||||
|
// Wait for hover state to be visible
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, 100));
|
||||||
|
|
||||||
|
const learnLink = canvas.getByRole("link", { name: /learn/i });
|
||||||
|
await userEvent.hover(learnLink);
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, 100));
|
||||||
|
|
||||||
|
const aboutLink = canvas.getByRole("link", { name: /about/i });
|
||||||
|
await userEvent.hover(aboutLink);
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, 100));
|
||||||
|
});
|
||||||
|
|
||||||
|
await step("Hover over authentication elements", async () => {
|
||||||
|
const loginLink = canvas.getByRole("link", {
|
||||||
|
name: /log in to your account/i,
|
||||||
|
});
|
||||||
|
await userEvent.hover(loginLink);
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, 100));
|
||||||
|
|
||||||
|
const createRuleButton = canvas.getByRole("button", {
|
||||||
|
name: /create a new rule with avatar decoration/i,
|
||||||
|
});
|
||||||
|
await userEvent.hover(createRuleButton);
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, 100));
|
||||||
|
});
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
// Story for testing focus states
|
||||||
|
export const FocusStates = {
|
||||||
|
args: {
|
||||||
|
onToggle: () => console.log("Navigation toggled"),
|
||||||
|
},
|
||||||
|
parameters: {
|
||||||
|
docs: {
|
||||||
|
description: {
|
||||||
|
story:
|
||||||
|
"Header with focus states visible. This story captures the visual appearance when elements are focused.",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
play: async ({ canvasElement, step }) => {
|
||||||
|
const canvas = within(canvasElement);
|
||||||
|
|
||||||
|
await step("Focus on navigation items", async () => {
|
||||||
|
const useCasesLink = canvas.getByRole("link", { name: /use cases/i });
|
||||||
|
useCasesLink.focus();
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, 100));
|
||||||
|
|
||||||
|
const learnLink = canvas.getByRole("link", { name: /learn/i });
|
||||||
|
learnLink.focus();
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, 100));
|
||||||
|
|
||||||
|
const aboutLink = canvas.getByRole("link", { name: /about/i });
|
||||||
|
aboutLink.focus();
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, 100));
|
||||||
|
});
|
||||||
|
|
||||||
|
await step("Focus on authentication elements", async () => {
|
||||||
|
const loginLink = canvas.getByRole("link", {
|
||||||
|
name: /log in to your account/i,
|
||||||
|
});
|
||||||
|
loginLink.focus();
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, 100));
|
||||||
|
|
||||||
|
const createRuleButton = canvas.getByRole("button", {
|
||||||
|
name: /create a new rule with avatar decoration/i,
|
||||||
|
});
|
||||||
|
createRuleButton.focus();
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, 100));
|
||||||
|
});
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
// Story for testing with long content
|
||||||
|
export const WithLongContent = {
|
||||||
|
args: {
|
||||||
|
onToggle: () => console.log("Navigation toggled"),
|
||||||
|
},
|
||||||
|
render: () => (
|
||||||
|
<div className="min-h-screen bg-[var(--color-surface-default-primary)]">
|
||||||
|
<Header onToggle={() => console.log("Navigation toggled")} />
|
||||||
|
<main className="p-8">
|
||||||
|
<div className="max-w-4xl mx-auto">
|
||||||
|
<h1 className="text-2xl font-bold text-white mb-4">
|
||||||
|
Header with Long Content
|
||||||
|
</h1>
|
||||||
|
<p className="text-white mb-4">
|
||||||
|
This story tests how the header looks with a lot of content below
|
||||||
|
it. This helps ensure the header maintains its visual integrity in
|
||||||
|
real-world scenarios.
|
||||||
|
</p>
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
|
||||||
|
{Array.from({ length: 12 }, (_, i) => (
|
||||||
|
<div
|
||||||
|
key={i}
|
||||||
|
className="bg-[var(--color-surface-default-secondary)] p-4 rounded-lg"
|
||||||
|
>
|
||||||
|
<h3 className="text-white font-semibold mb-2">
|
||||||
|
Content Block {i + 1}
|
||||||
|
</h3>
|
||||||
|
<p className="text-[var(--color-content-default-secondary)] text-sm">
|
||||||
|
This is example content to show how the header integrates with
|
||||||
|
page content. This block contains enough text to test layout
|
||||||
|
behavior.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
parameters: {
|
||||||
|
docs: {
|
||||||
|
description: {
|
||||||
|
story:
|
||||||
|
"Header with long content below to test visual integration and layout stability.",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
// Story for testing edge cases
|
||||||
|
export const EdgeCases = {
|
||||||
|
args: {
|
||||||
|
onToggle: () => console.log("Navigation toggled"),
|
||||||
|
},
|
||||||
|
parameters: {
|
||||||
|
viewport: {
|
||||||
|
defaultViewport: "xs",
|
||||||
|
},
|
||||||
|
docs: {
|
||||||
|
description: {
|
||||||
|
story:
|
||||||
|
"Header at the smallest breakpoint to test edge case behavior and ensure no layout issues.",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|||||||
@@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -1,11 +1,15 @@
|
|||||||
import { test, expect } from "@playwright/test";
|
import { test, expect } from "@playwright/test";
|
||||||
|
|
||||||
const breakpoints = [
|
const breakpoints = [
|
||||||
{ name: "xs", width: 360, height: 700 },
|
{ name: "xs", width: 320, height: 700 },
|
||||||
{ name: "sm", width: 640, height: 700 },
|
{ name: "sm", width: 360, height: 700 },
|
||||||
{ name: "md", width: 768, height: 700 },
|
{ name: "md", width: 480, height: 700 },
|
||||||
{ name: "lg", width: 1024, height: 700 },
|
{ name: "lg", width: 640, height: 700 },
|
||||||
{ name: "xl", width: 1280, 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) {
|
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
|
// Additional responsive behavior tests
|
||||||
test.describe("Header responsive behavior", () => {
|
test.describe("Header responsive behavior", () => {
|
||||||
test("header maintains proper layout across breakpoints", async ({
|
test("header maintains proper layout across breakpoints", async ({
|
||||||
|
|||||||
@@ -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,
|
||||||
|
};
|
||||||
Reference in New Issue
Block a user