import { test, expect } from "@playwright/test"; import { whatsappUrl } from "../src/data.js"; const pages = [ ["/", "Built to develop."], ["/fabrics", "Different yarns."], ["/about", "Built around"], ["/contact", "Let’s talk"], ]; for (const width of [1440, 768, 390, 320]) { for (const [path, heading] of pages) { test(`${path} renders without errors at ${width}px`, async ({ page, }, testInfo) => { const errors = []; page.on("pageerror", (error) => errors.push(error.message)); page.on("console", (message) => { if (message.type() === "error") errors.push(message.text()); }); await page.setViewportSize({ width, height: 900 }); await page.goto(path); await expect(page.getByRole("heading", { level: 1 })).toContainText( heading, ); await page.evaluate(() => document.fonts.ready); for (const image of await page.locator("img").all()) { await image.scrollIntoViewIfNeeded(); await expect(image).toHaveJSProperty("complete", true); expect( await image.evaluate((element) => element.naturalWidth), ).toBeGreaterThan(0); } await page.evaluate(() => window.scrollTo(0, 0)); await expect(page.locator("h1")).toHaveCount(1); await expect(page.locator("[style]")).toHaveCount(0); await expect(page.locator("form")).toHaveCount(0); const metrics = await page.evaluate(() => ({ overflow: document.documentElement.scrollWidth > window.innerWidth, unloadedImages: [...document.images].filter( (image) => !image.complete || !image.naturalWidth, ).length, fontsLoaded: document.fonts.check('16px "Instrument Serif"') && document.fonts.check('16px "DM Sans"'), contentWidthRatio: document.querySelector("#main-content").getBoundingClientRect() .width / window.innerWidth, })); expect(metrics).toMatchObject({ overflow: false, unloadedImages: 0, fontsLoaded: true, }); expect(metrics.contentWidthRatio).toBeGreaterThan(0.85); expect(errors).toEqual([]); if (width === 1440 || width === 390) { await page.screenshot({ path: testInfo.outputPath("page.png"), fullPage: true, }); } }); } } test("desktop navigation, anchored CTA and browser back work", async ({ page, }) => { await page.goto("/"); await page .getByRole("link", { name: "Explore our fabrics", exact: true }) .click(); await expect(page).toHaveURL("/fabrics"); await expect( page .getByRole("navigation", { name: "Main navigation" }) .getByRole("link", { name: "Fabrics", exact: true }), ).toHaveAttribute("aria-current", "page"); await page .getByRole("link", { name: "Talk to us on WhatsApp", exact: true }) .click(); await expect(page).toHaveURL("/contact#whatsapp"); await expect(page.locator("#whatsapp")).toBeInViewport(); await expect( page.getByRole("button", { name: "Talk to us on WhatsApp", exact: true }), ).toBeDisabled(); await expect(page.locator("#whatsapp-status")).toContainText( "number to be supplied", ); await page.goBack(); await expect(page).toHaveURL("/fabrics"); await page.goto("/contact#whatsapp"); await page .getByRole("link", { name: "Talk to us on WhatsApp", exact: true }) .click(); await expect(page.locator("#whatsapp")).toBeInViewport(); }); test("mobile navigation closes on selection and Escape", async ({ page }) => { await page.setViewportSize({ width: 390, height: 844 }); await page.goto("/"); const toggle = page.locator("#menu-toggle"); await toggle.click(); await expect(toggle).toHaveAttribute("aria-expanded", "true"); await page.keyboard.press("Escape"); await expect(toggle).toHaveAttribute("aria-expanded", "false"); await expect(toggle).toBeFocused(); await toggle.click(); await page .getByRole("navigation", { name: "Main navigation" }) .getByRole("link", { name: "About", exact: true }) .click(); await expect(page).toHaveURL("/about"); await expect(toggle).toHaveAttribute("aria-expanded", "false"); await expect(page.locator(".owner")).toHaveCount(2); await toggle.click(); await page.getByRole("link", { name: "RRS home", exact: true }).click(); await expect(page).toHaveURL("/"); await expect(toggle).toHaveAttribute("aria-expanded", "false"); }); test("application disclosures work from the keyboard", async ({ page }) => { await page.goto("/fabrics"); const summary = page.locator("summary").filter({ hasText: "Textured knits" }); await summary.focus(); await page.keyboard.press("Enter"); await expect( page.getByText( "Knitted constructions where yarn and machine configuration are used to create distinctive surface character.", ), ).toBeVisible(); await page.keyboard.press("Enter"); await expect( page.getByText( "Knitted constructions where yarn and machine configuration are used to create distinctive surface character.", ), ).not.toBeVisible(); await expect(page.locator(".sample")).toHaveCount(7); }); test("conversation starter copies with accessible feedback", async ({ page, context, }) => { await context.grantPermissions(["clipboard-read", "clipboard-write"]); await page.goto("/contact"); await page.getByRole("button", { name: "Copy conversation starter" }).click(); await expect(page.getByRole("status")).toHaveText("Message copied."); expect(await page.evaluate(() => navigator.clipboard.readText())).toBe( "Hello RRS, I'd like to discuss a fabric requirement.", ); }); test("clipboard denial has a useful fallback", async ({ page }) => { await page.goto("/contact"); await page.evaluate(() => { Object.defineProperty(navigator, "clipboard", { value: { writeText: () => Promise.reject(new Error("Denied")) }, configurable: true, }); }); await page.getByRole("button", { name: "Copy conversation starter" }).click(); await expect(page.getByRole("status")).toContainText( "Please select and copy", ); }); test("WhatsApp requires a real international number and encodes the message", () => { for (const value of ["", "[Number]", "123", "+12345678901", "not-a-number"]) expect(whatsappUrl(value)).toBeNull(); const url = new URL(whatsappUrl("12345678901")); expect(url.origin).toBe("https://wa.me"); expect(url.searchParams.get("text")).toBe( "Hello RRS, I'd like to discuss a fabric requirement.", ); }); test("every page section unveils once when scrolled into view", async ({ page, }) => { for (const [path] of pages) { await page.goto(path); const sections = page.locator("#main-content > section"); expect(await sections.count()).toBeGreaterThan(0); for (const section of await sections.all()) { await section.scrollIntoViewIfNeeded(); await expect(section).toHaveClass(/reveal--visible/); await expect(section).toHaveCSS("opacity", "1"); } } }); test("homepage feature headings are centered semantic h2 elements", async ({ page, }) => { await page.goto("/"); const fabricsHeading = page.getByRole("heading", { level: 2, name: "Greige is where it begins.", }); const developmentsHeading = page.getByRole("heading", { level: 2, name: "Structure changes everything.", }); await expect(fabricsHeading).toHaveCSS("text-align", "center"); await expect(developmentsHeading.locator("..").locator("..")).toHaveClass( /section-heading--centered/, ); }); test("capacity values roll in opposite directions and settle at 60–70", async ({ page, }) => { for (const path of ["/", "/fabrics"]) { await page.goto(path); const capacity = page.getByLabel("60 to 70"); await expect(capacity).toHaveCount(1); await capacity.scrollIntoViewIfNeeded(); await expect .poll(() => capacity .locator(".capacity-value__reel--up") .evaluate((element) => element.getAnimations().length), ) .toBeGreaterThan(0); await expect(capacity.locator(".capacity-value__reel--up")).toHaveCSS( "transform", /matrix/, ); await expect(capacity.locator(".capacity-value__reel--down")).toHaveCSS( "transform", /matrix/, ); } }); test("section unveiling respects reduced-motion preferences", async ({ page, }) => { await page.emulateMedia({ reducedMotion: "reduce" }); await page.goto("/"); const sections = page.locator("#main-content > section"); await expect(sections.first()).toHaveClass(/reveal--visible/); for (const section of await sections.all()) { await expect(section).toHaveCSS("opacity", "1"); await expect(section).toHaveCSS("transform", "none"); } const capacity = page.getByLabel("60 to 70"); await capacity.scrollIntoViewIfNeeded(); expect( await capacity .locator(".capacity-value__reel--up") .evaluate((element) => element.getAnimations().length), ).toBe(0); }); test("unknown route provides a way home", async ({ page }) => { await page.goto("/missing-page"); await expect(page.getByRole("heading", { level: 1 })).toHaveText( "A loose thread.", ); await page.getByRole("link", { name: "Back to home", exact: true }).click(); await expect(page).toHaveURL("/"); });