🐞 fix: Adjust mobile layout for blog page and add regression tests
This commit is contained in:
@@ -31,7 +31,7 @@ get_header();
|
|||||||
<?php if ( have_posts() ) : ?>
|
<?php if ( have_posts() ) : ?>
|
||||||
<div class="blog-posts <?php echo esc_attr( $clsEntry ); ?>">
|
<div class="blog-posts <?php echo esc_attr( $clsEntry ); ?>">
|
||||||
<div class="post-list">
|
<div class="post-list">
|
||||||
<div class="post-list__posts grid grid-cols-[repeat(auto-fit,minmax(26rem,1fr))] gap-12">
|
<div class="post-list__posts grid grid-cols-[repeat(auto-fit,minmax(20rem,1fr))] gap-12">
|
||||||
<?php
|
<?php
|
||||||
while ( have_posts() ) :
|
while ( have_posts() ) :
|
||||||
the_post();
|
the_post();
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
@apply text-cwc-blue-01;
|
@apply text-cwc-blue-01;
|
||||||
|
|
||||||
line-height: 1;
|
line-height: 1;
|
||||||
margin: 0 0 1.5rem;
|
margin: 0;
|
||||||
text-align: left;
|
text-align: left;
|
||||||
|
|
||||||
em {
|
em {
|
||||||
|
|||||||
@@ -0,0 +1,189 @@
|
|||||||
|
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);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -25,8 +25,8 @@ if ( ! $heading ) {
|
|||||||
$wrapperStyle = $bgColor ? 'background-color: ' . esc_attr( $bgColor ) . ';' : '';
|
$wrapperStyle = $bgColor ? 'background-color: ' . esc_attr( $bgColor ) . ';' : '';
|
||||||
?>
|
?>
|
||||||
|
|
||||||
<div class="page-hero <?php echo $isDark ? 'dark' : ''; ?> lg:px-0 lg:mt-30 lg:mb-4" style="<?php echo esc_attr( $wrapperStyle ); ?>">
|
<div class="page-hero <?php echo $isDark ? 'dark' : ''; ?> pt-4 lg:px-0 lg:pt-30 lg:mb-4" style="<?php echo esc_attr( $wrapperStyle ); ?>">
|
||||||
<div class="page-hero__content container flex flex-col lg:flex-row justify-between gap-20 mx-auto px-0!">
|
<div class="page-hero__content container flex flex-col lg:flex-row justify-between gap-6 lg:gap-20 mx-auto lg:px-0!">
|
||||||
<!-- <div id="breadcrumbs">
|
<!-- <div id="breadcrumbs">
|
||||||
<?php Breadcrumbs::render(); ?>
|
<?php Breadcrumbs::render(); ?>
|
||||||
</div> -->
|
</div> -->
|
||||||
@@ -41,7 +41,7 @@ $wrapperStyle = $bgColor ? 'background-color: ' . esc_attr( $bgColor ) . ';' : '
|
|||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
|
|
||||||
<?php if ( $intro ) : ?>
|
<?php if ( $intro ) : ?>
|
||||||
<div class="page-hero__intro grow text-18px pt-3">
|
<div class="page-hero__intro grow text-18px mb-6">
|
||||||
<?php echo wp_kses_post( $intro ); ?>
|
<?php echo wp_kses_post( $intro ); ?>
|
||||||
</div>
|
</div>
|
||||||
<?php
|
<?php
|
||||||
|
|||||||
Reference in New Issue
Block a user