114 lines
3.7 KiB
TypeScript
114 lines
3.7 KiB
TypeScript
import type { Page, Locator } from "@playwright/test"
|
|
import { test as base, expect as pwExpect } from "@playwright/test"
|
|
|
|
export { expect, type Page, API_BASE, clickSpaLink } from "../e2e/fixtures"
|
|
import { expect } from "../e2e/fixtures"
|
|
|
|
type VisualFixtures = {}
|
|
|
|
export const test = base.extend<VisualFixtures>({
|
|
/**
|
|
* Override page fixture: BrowserSync domcontentloaded workaround.
|
|
*/
|
|
page: async ({ page }, use) => {
|
|
const origGoto = page.goto.bind(page)
|
|
const origReload = page.reload.bind(page)
|
|
|
|
page.goto = ((url: string, opts?: any) =>
|
|
origGoto(url, { waitUntil: "domcontentloaded", ...opts })) as typeof page.goto
|
|
page.reload = ((opts?: any) => origReload({ waitUntil: "domcontentloaded", ...opts })) as typeof page.reload
|
|
|
|
await use(page)
|
|
},
|
|
})
|
|
|
|
/**
|
|
* Wait for the SPA to be fully rendered and stable for visual comparison.
|
|
* Waits for skeleton loaders to disappear and CSS to settle.
|
|
*/
|
|
export async function waitForVisualReady(page: Page, opts?: { timeout?: number }): Promise<void> {
|
|
const timeout = opts?.timeout ?? 15000
|
|
await page.waitForLoadState("domcontentloaded")
|
|
await expect(page.locator("#appContainer")).not.toBeEmpty({ timeout })
|
|
try {
|
|
await page.waitForFunction(() => document.querySelectorAll(".animate-pulse").length === 0, { timeout: 10000 })
|
|
} catch {
|
|
// Skeleton loaders may not exist on every page
|
|
}
|
|
await page.waitForTimeout(800)
|
|
}
|
|
|
|
/**
|
|
* Navigate to a route with visual readiness wait.
|
|
*/
|
|
export async function navigateToRoute(page: Page, routePath: string): Promise<void> {
|
|
const url = page.url()
|
|
const match = url.match(/\/([a-z]{2})(\/|$)/)
|
|
const lang = match?.[1] || "de"
|
|
const fullPath = routePath === "/" ? `/${lang}` : `/${lang}${routePath}`
|
|
await page.goto(fullPath, { waitUntil: "domcontentloaded" })
|
|
await waitForVisualReady(page)
|
|
}
|
|
|
|
/**
|
|
* Hide dynamic content that would cause screenshot flakiness:
|
|
* - BrowserSync overlay
|
|
* - All animations and transitions
|
|
* - Blinking cursor
|
|
*/
|
|
export async function hideDynamicContent(page: Page): Promise<void> {
|
|
await page.addStyleTag({
|
|
content: `
|
|
#__bs_notify__, #__bs_notify__:before, #__bs_notify__:after { display: none !important; }
|
|
*, *::before, *::after {
|
|
animation-duration: 0s !important;
|
|
animation-delay: 0s !important;
|
|
transition-duration: 0s !important;
|
|
transition-delay: 0s !important;
|
|
}
|
|
* { caret-color: transparent !important; }
|
|
`,
|
|
})
|
|
}
|
|
|
|
/**
|
|
* Returns locators for non-deterministic elements that should be masked
|
|
* in screenshots (e.g. cart badges, timestamps).
|
|
* Customize this for your project.
|
|
*/
|
|
export function getDynamicMasks(page: Page): Locator[] {
|
|
return [
|
|
// Add project-specific dynamic element selectors here:
|
|
// page.locator('[data-testid="cart-badge"]'),
|
|
]
|
|
}
|
|
|
|
/**
|
|
* Prepare the page for a screenshot: hide dynamic content and scroll to top.
|
|
*/
|
|
export async function prepareForScreenshot(page: Page): Promise<void> {
|
|
await hideDynamicContent(page)
|
|
await page.evaluate(() => window.scrollTo(0, 0))
|
|
await page.waitForTimeout(300)
|
|
}
|
|
|
|
/**
|
|
* Take a screenshot and compare against baseline.
|
|
*/
|
|
export async function expectScreenshot(
|
|
page: Page,
|
|
name: string,
|
|
opts?: {
|
|
fullPage?: boolean
|
|
mask?: Locator[]
|
|
maxDiffPixelRatio?: number
|
|
}
|
|
): Promise<void> {
|
|
const masks = [...getDynamicMasks(page), ...(opts?.mask ?? [])]
|
|
await pwExpect(page).toHaveScreenshot(name, {
|
|
fullPage: opts?.fullPage ?? false,
|
|
mask: masks,
|
|
maxDiffPixelRatio: opts?.maxDiffPixelRatio ?? 0.02,
|
|
})
|
|
}
|