import { expect, test } from "@playwright/test"; // Default rich fixture: all fields populated (hero_image, hero_vector, intro). const pageUrl = process.env.TEST_PAGE_URL || "http://community-works-collaborative.test/sample-page/"; // No-intro fixture: `intro` ACF field intentionally empty. const noIntroUrl = process.env.TEST_PAGE_URL_NO_INTRO || "http://community-works-collaborative.test/inner-page-no-intro/"; // No-asset fixture: `hero_image` and `hero_vector` ACF fields intentionally empty. const noAssetUrl = process.env.TEST_PAGE_URL_NO_ASSET || "http://community-works-collaborative.test/inner-page-no-asset/"; const getBox = async (locator) => { const box = await locator.boundingBox(); expect(box).not.toBeNull(); return box; }; const expectNear = (actual, expected, tolerance = 3) => { expect(Math.abs(actual - expected)).toBeLessThanOrEqual(tolerance); }; test.describe("inner page layout", () => { test("desktop: hero, intro, body render with intro inside hero", async ({ page, }) => { const pageErrors = []; page.on("pageerror", (err) => pageErrors.push(err.message)); await page.setViewportSize({ width: 1280, height: 900 }); await page.goto(pageUrl); const hero = page.locator(".page-hero").first(); const intro = page.locator(".page-hero__intro").first(); const content = page.locator(".entry-content").first(); await expect(hero).toBeVisible(); await expect(intro).toBeVisible(); await expect(content).toBeVisible(); // Intro lives INSIDE the hero. const heroBox = await getBox(hero); const introBox = await getBox(intro); const contentBox = await getBox(content); // Intro is a descendant of the hero (same x, y is between hero top and bottom). expect(introBox.x).toBeGreaterThanOrEqual(heroBox.x - 1); expect(introBox.y).toBeGreaterThanOrEqual(heroBox.y - 1); expect(introBox.y + introBox.height).toBeLessThanOrEqual( heroBox.y + heroBox.height + 1, ); // Body renders below the hero. expect(contentBox.y).toBeGreaterThanOrEqual(heroBox.y + heroBox.height - 1); // Heading is rendered inside the hero. await expect(hero.locator("h1").first()).toBeVisible(); // Intro contains a heading and a body paragraph. await expect(intro.locator(".page-hero__intro-heading").first()).toBeVisible(); await expect(intro.locator(".page-hero__intro-text").first()).toBeVisible(); // Page renders without JS errors. expect(pageErrors).toEqual([]); }); test("mobile 402: hero, intro, body order and intro width", async ({ page, }) => { const pageErrors = []; page.on("pageerror", (err) => pageErrors.push(err.message)); await page.setViewportSize({ width: 402, height: 874 }); await page.goto(pageUrl); const hero = page.locator(".page-hero").first(); const intro = page.locator(".page-hero__intro").first(); const content = page.locator(".entry-content").first(); await expect(hero).toBeVisible(); await expect(intro).toBeVisible(); await expect(content).toBeVisible(); const heroBox = await getBox(hero); const introBox = await getBox(intro); const contentBox = await getBox(content); // Intro inside the hero. expect(introBox.x).toBeGreaterThanOrEqual(heroBox.x - 1); expect(introBox.y).toBeGreaterThanOrEqual(heroBox.y - 1); expect(introBox.y + introBox.height).toBeLessThanOrEqual( heroBox.y + heroBox.height + 1, ); // Body below the hero. expect(contentBox.y).toBeGreaterThanOrEqual(heroBox.y + heroBox.height - 1); // Intro fits within the mobile viewport. expect(introBox.x + introBox.width).toBeLessThanOrEqual(402 + 3); // No JS errors. expect(pageErrors).toEqual([]); }); test("mobile 320: nothing clips horizontally", async ({ page }) => { await page.setViewportSize({ width: 320, height: 800 }); await page.goto(pageUrl); const hero = page.locator(".page-hero").first(); const intro = page.locator(".page-hero__intro").first(); await expect(hero).toBeVisible(); await expect(intro).toBeVisible(); const heroBox = await getBox(hero); const introBox = await getBox(intro); expect(heroBox.x + heroBox.width).toBeLessThanOrEqual(320 + 3); expect(introBox.x + introBox.width).toBeLessThanOrEqual(320 + 3); }); test("mobile 767: layout still holds before the desktop breakpoint", async ({ page, }) => { await page.setViewportSize({ width: 767, height: 900 }); await page.goto(pageUrl); const hero = page.locator(".page-hero").first(); const intro = page.locator(".page-hero__intro").first(); await expect(hero).toBeVisible(); await expect(intro).toBeVisible(); const heroBox = await getBox(hero); const introBox = await getBox(intro); expect(heroBox.x + heroBox.width).toBeLessThanOrEqual(767 + 3); expect(introBox.x + introBox.width).toBeLessThanOrEqual(767 + 3); }); // Spec line 113: "A page with no `intro` field renders hero + body with no empty intro wrapper." test("no-intro: hero and body render with no .page-hero__intro wrapper", async ({ page, }) => { await page.setViewportSize({ width: 1280, height: 900 }); await page.goto(noIntroUrl); const hero = page.locator(".page-hero").first(); const content = page.locator(".entry-content").first(); await expect(hero).toBeVisible(); await expect(content).toBeVisible(); // No intro wrapper rendered when the intro field is empty. expect(await page.locator(".page-hero__intro").count()).toBe(0); // Hero and body still render in the expected order. const heroBox = await getBox(hero); const contentBox = await getBox(content); expect(contentBox.y).toBeGreaterThanOrEqual(heroBox.y + heroBox.height - 1); // Heading is still inside the hero. await expect(hero.locator("h1").first()).toBeVisible(); }); // Spec line 114: "A page with no `hero_image` and no `hero_vector` renders the existing dark text-only hero unchanged." test("no-asset: dark text-only hero renders without image or vector", async ({ page, }) => { await page.setViewportSize({ width: 1280, height: 900 }); await page.goto(noAssetUrl); const hero = page.locator(".page-hero").first(); await expect(hero).toBeVisible(); // No media layers when both assets are empty. expect(await page.locator(".page-hero__media").count()).toBe(0); expect(await page.locator(".page-hero__vector").count()).toBe(0); // Heading still renders inside the hero. await expect(hero.locator("h1").first()).toBeVisible(); // Hero still exposes the dark text-only class hook. await expect(hero).toHaveClass(/page-hero/); }); });