Files
CWC/tests/inner-page.spec.js
Keith SolomonandClaude 2bda7f21eb Restructure hero: image at natural size overflowing, intro as text, no hard-coded copy
- views/partials/page-hero.php: removed the page-intro partial call. The
  intro is now rendered inline as a single .page-hero__intro div
  containing the editor-supplied intro text only (no hard-coded
  heading). The H1 is rendered with a dedicated .page-hero__heading
  class for typography control. Breadcrumbs + H1 + intro are in a
  single .page-hero__content container.
- views/partials/page-intro.php: deleted (no longer used).
- styles/blocks/page-hero.css: rewritten layout. The hero has a min-height
  of clamp(31rem, 38vw, 36rem). .page-hero__media is a foreground
  absolutely-positioned img on the right (no mask, no cover, no
  object-fit), with bottom: -10% so the image's bottom hangs over
  the bottom edge of the hero. The vector is positioned at
  bottom: clamp(1.5rem, 4vw, 3rem) with some spacing from the bottom
  edge, full-width on the left. Both elements stay above the content
  layer (z-index 10) and below the breadcrumb/title text. The H1 uses
  Quincy serif at clamp(2.5rem, 6vw, 4.5rem). The intro text is
  capped at 36rem to sit alongside the H1 without spanning the full
  hero width.
- tests/inner-page.spec.js: removed the now-obsolete
  .page-hero__intro-heading / .page-hero__intro-text assertions (the
  intro is a single text block now).

Co-Authored-By: Claude <noreply@anthropic.com>
2026-07-01 16:11:28 -05:00

189 lines
6.8 KiB
JavaScript

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();
// 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/);
});
});