- styles/blocks/page-hero.css: add blue-to-teal gradient to .page-hero (matches homepage-hero). Fix image mask direction (right-to-left fade) so the image is visible on the right and fades to transparent on the left. Reposition the vector to span the full bottom of the hero at z-index 2 (above the media layer, below content). Add .page-hero__intro and .page-hero__intro-inner grid rules for the new two-column heading + body block. - views/partials/page-hero.php: drop bg-dark / bg-cover / bg-no-repeat utilities (the gradient comes from CSS now). Simplify the heading render. Load the page-intro partial inside .page-hero__content, below the heading. - views/partials/page-intro.php: rewrite as a fragment for embedding inside the hero - a two-column heading (hard-coded 'Our Work') + body grid. Returns early when the intro field is empty. - page.php: remove the page-intro partial call (it's now loaded from page-hero.php). - tests/inner-page.spec.js: switch the selector from .page-intro to .page-hero__intro. Update the order assertions to verify the intro is a descendant of the hero (not a sibling below it). Co-Authored-By: Claude <noreply@anthropic.com>
193 lines
7.1 KiB
JavaScript
193 lines
7.1 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();
|
|
|
|
// 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/);
|
|
});
|
|
});
|