diff --git a/index.php b/index.php index 87541f2..eaf81e1 100644 --- a/index.php +++ b/index.php @@ -31,7 +31,7 @@ get_header();
-
+
{ + test.use({ viewport: { width: 402, height: 874 } }); + + test("hero has top padding so it isn't glued to the header", async ({ + page, + }) => { + await page.goto(blogUrl, { waitUntil: "networkidle" }); + + const data = await page.evaluate(() => { + const header = document.querySelector(".site-header"); + const hero = document.querySelector(".page-hero"); + const heading = document.querySelector(".page-hero__heading"); + const cs = hero ? getComputedStyle(hero) : null; + return { + headerBottom: header ? Math.round(header.getBoundingClientRect().bottom) : null, + heroTop: hero ? Math.round(hero.getBoundingClientRect().top) : null, + heroPaddingTop: cs ? cs.paddingTop : null, + headingTop: heading ? Math.round(heading.getBoundingClientRect().top) : null, + }; + }); + + // The hero must have some top padding (>= 8px) on mobile. + const pt = parseFloat(data.heroPaddingTop) || 0; + expect( + pt, + `hero paddingTop should be >= 8px on mobile, got ${pt}`, + ).toBeGreaterThanOrEqual(8); + + // The heading shouldn't be touching the bottom of the header. + const gap = data.headingTop - data.headerBottom; + expect( + gap, + `heading should sit at least 8px below the header, got ${gap}px`, + ).toBeGreaterThanOrEqual(8); + }); + + test("title-to-intro gap is ~24px (not 80px) on mobile", async ({ + page, + }) => { + await page.goto(blogUrl, { waitUntil: "networkidle" }); + + const gap = await page.evaluate(() => { + const heading = document.querySelector(".page-hero__heading"); + const intro = document.querySelector(".page-hero__intro"); + if (!heading || !intro) return null; + const hRect = heading.getBoundingClientRect(); + const iRect = intro.getBoundingClientRect(); + return Math.round(iRect.top - hRect.bottom); + }); + + expect(gap, `title-to-intro gap should be ~24px, got ${gap}px`).toBeLessThanOrEqual(40); + }); + + test("hero intro is padded from the left edge", async ({ page }) => { + await page.goto(blogUrl, { waitUntil: "networkidle" }); + + const data = await page.evaluate(() => { + const intro = document.querySelector(".page-hero__intro"); + const heading = document.querySelector(".page-hero__heading"); + if (!intro || !heading) return null; + return { + introX: Math.round(intro.getBoundingClientRect().x), + headingX: Math.round(heading.getBoundingClientRect().x), + winW: window.innerWidth, + }; + }); + + // The intro should have at least 16px of left padding from the viewport edge. + expect( + data.introX, + `intro x=${data.introX} should be >= 16`, + ).toBeGreaterThanOrEqual(16); + expect( + data.headingX, + `heading x=${data.headingX} should be >= 16`, + ).toBeGreaterThanOrEqual(16); + }); + + test("intro has bottom margin so it doesn't sit on the first card", async ({ + page, + }) => { + await page.goto(blogUrl, { waitUntil: "networkidle" }); + + const data = await page.evaluate(() => { + const intro = document.querySelector(".page-hero__intro"); + const firstCard = document.querySelector(".post-list__post"); + if (!intro || !firstCard) return null; + const cs = intro ? getComputedStyle(intro) : null; + const iRect = intro.getBoundingClientRect(); + const cRect = firstCard.getBoundingClientRect(); + return { + introMarginBottom: cs ? cs.marginBottom : null, + introBottom: Math.round(iRect.bottom), + cardTop: Math.round(cRect.top), + gap: Math.round(cRect.top - iRect.bottom), + }; + }); + + // The intro must have a non-zero bottom margin. + const mb = parseFloat(data.introMarginBottom) || 0; + expect( + mb, + `intro marginBottom should be >= 16px, got ${mb}`, + ).toBeGreaterThanOrEqual(16); + + // The intro should sit at least 16px above the first card. + expect( + data.gap, + `intro-to-card gap should be >= 16px, got ${data.gap}px`, + ).toBeGreaterThanOrEqual(16); + }); + + test("post card fits within the viewport on mobile", async ({ page }) => { + await page.goto(blogUrl, { waitUntil: "networkidle" }); + + const data = await page.evaluate(() => { + const card = document.querySelector(".post-list__post"); + if (!card) return null; + const r = card.getBoundingClientRect(); + return { + x: Math.round(r.x), + w: Math.round(r.width), + right: Math.round(r.right), + winW: window.innerWidth, + }; + }); + + // The card must not overflow the viewport on either side. + expect( + data.x, + `card x=${data.x} should be >= 0`, + ).toBeGreaterThanOrEqual(0); + expect( + data.right, + `card right=${data.right} should be <= ${data.winW}`, + ).toBeLessThanOrEqual(data.winW); + }); + + test("no horizontal overflow on the blog page at 402", async ({ + page, + }) => { + await page.goto(blogUrl, { waitUntil: "networkidle" }); + + const data = await page.evaluate(() => ({ + docW: document.documentElement.scrollWidth, + winW: window.innerWidth, + })); + + expect( + data.docW, + `docW=${data.docW} exceeds winW=${data.winW}`, + ).toBeLessThanOrEqual(data.winW + 1); + }); +}); + +test.describe("blog page mobile layout (xs 360)", () => { + test.use({ viewport: { width: 360, height: 800 } }); + + test("post card fits within 360px viewport", async ({ page }) => { + await page.goto(blogUrl, { waitUntil: "networkidle" }); + + const data = await page.evaluate(() => { + const card = document.querySelector(".post-list__post"); + if (!card) return null; + const r = card.getBoundingClientRect(); + return { x: Math.round(r.x), w: Math.round(r.width), right: Math.round(r.right), winW: window.innerWidth }; + }); + + expect(data.x).toBeGreaterThanOrEqual(0); + expect(data.right).toBeLessThanOrEqual(data.winW); + }); +}); diff --git a/views/partials/page-hero.php b/views/partials/page-hero.php index 8271494..ceffa4b 100644 --- a/views/partials/page-hero.php +++ b/views/partials/page-hero.php @@ -25,8 +25,8 @@ if ( ! $heading ) { $wrapperStyle = $bgColor ? 'background-color: ' . esc_attr( $bgColor ) . ';' : ''; ?> -
-
+
+
@@ -41,7 +41,7 @@ $wrapperStyle = $bgColor ? 'background-color: ' . esc_attr( $bgColor ) . ';' : ' -
+