/** * Viewport profile helpers for responsive testing */ export type ViewportSize = "mobile" | "tablet" | "desktop" | "widescreen"; export interface ViewportDimensions { width: number; height: number; } export const VIEWPORTS: Record = { mobile: { width: 375, height: 667 }, // iPhone SE / similar tablet: { width: 768, height: 1024 }, // iPad / similar desktop: { width: 1280, height: 720 }, // Standard desktop widescreen: { width: 1920, height: 1080 }, // Large desktop } as const; export const VIEWPORT_SIZES: ViewportSize[] = [ "mobile", "tablet", "desktop", "widescreen", ]; /** * Breakpoint definitions matching CSS media queries */ export const BREAKPOINTS = { sm: 640, // Small devices md: 768, // Medium devices (tablet) lg: 1024, // Large devices (desktop) xl: 1280, // Extra large "2xl": 1536, // 2X large } as const; /** * Set viewport size */ export async function setViewport( page: any, size: ViewportSize, ): Promise { const dimensions = VIEWPORTS[size]; await page.setViewportSize(dimensions); } /** * Get current viewport size */ export async function getViewport(page: any): Promise { return page.evaluate(() => ({ width: window.innerWidth, height: window.innerHeight, })); } /** * Assert viewport is at expected size */ export async function assertViewport( page: any, size: ViewportSize, ): Promise { const expected = VIEWPORTS[size]; const actual = await getViewport(page); if (actual.width !== expected.width || actual.height !== expected.height) { throw new Error( `Expected viewport ${size} (${expected.width}x${expected.height}) ` + `but got ${actual.width}x${actual.height}`, ); } } /** * Check if element has horizontal overflow */ export async function hasHorizontalOverflow( page: any, selector: string, ): Promise { return page.evaluate((sel: string) => { const element = document.querySelector(sel); if (!element) return false; return element.scrollWidth > element.clientWidth; }, selector); } /** * Check if element is clipped (overflow hidden with content exceeding bounds) */ export async function isClipped(page: any, selector: string): Promise { return page.evaluate((sel: string) => { const element = document.querySelector(sel); if (!element) return false; const style = window.getComputedStyle(element); const rect = element.getBoundingClientRect(); // Check if element has overflow hidden and children exceed bounds const children = element.children; for (const child of children) { const childRect = child.getBoundingClientRect(); if (childRect.right > rect.right || childRect.bottom > rect.bottom) { return ( style.overflow === "hidden" || style.overflowX === "hidden" || style.overflowY === "hidden" ); } } return false; }, selector); } /** * Run a test across all viewport sizes */ export function testAllViewports( name: string, testFn: (size: ViewportSize) => Promise | void, ): void { for (const size of VIEWPORT_SIZES) { test(`${name} - ${size}`, async () => { await testFn(size); }); } } /** * Check sticky element position */ export async function getStickyPosition( page: any, selector: string, ): Promise<{ top: number; isSticky: boolean }> { return page.evaluate((sel: string) => { const element = document.querySelector(sel); if (!element) return { top: 0, isSticky: false }; const style = window.getComputedStyle(element); const rect = element.getBoundingClientRect(); return { top: rect.top, isSticky: style.position === "sticky" || style.position === "fixed", }; }, selector); }