import { expect, test } from "@playwright/test"; /** * Regression coverage for the four mobile-blog layout bugs: * * 1. Hero text too close to the header. * 2. Gap between title and intro too big (80px instead of ~24px). * 3. Intro runs to the viewport edge on the left. * 4. Post card runs to the viewport edge on the right. * * Tests run against the local Herd site where the rebuilt theme.css is * served. The live deployment (solowebdesigns.net) will pick up the * rebuilt CSS when the user deploys. */ const blogUrl = "http://community-works-collaborative.test/blog/"; test.describe("blog page mobile layout", () => { 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); }); });