Add inner page layout implementation plan
Seven tasks: ACF fields, page-intro partial, page.php wiring, scoped CSS, page-hero partial update, Playwright tests, and a final quality gate. Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,744 @@
|
||||
# Inner Page Layout Implementation Plan
|
||||
|
||||
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
|
||||
|
||||
**Goal:** Build the inner page layout from `notes/inner-page-mockup.png` so every WordPress page renders a dark hero (with background image and decorative vector on the right), a centered narrow intro section, and the standard editor body — with per-page variation only in hero image, vector, intro, and body content.
|
||||
|
||||
**Architecture:** Extend the existing `Page Heading` ACF field group with two new fields (`hero_image`, `hero_vector`). Modify the existing `views/partials/page-hero.php` to render the new image/vector and to stop rendering the `intro` paragraph inside the hero. Add a new `views/partials/page-intro.php` partial. Wire it into `views/page.php` between the hero and `the_content`. Add scoped CSS for the new hero media layers.
|
||||
|
||||
**Tech Stack:** WordPress (PHP), ACF Pro, Tailwind v4 (built via `npm run build`), Playwright, axe-core.
|
||||
|
||||
## Global Constraints
|
||||
|
||||
- Theme root: `wp-content/themes/community-works-collaborative/`
|
||||
- Build command: `npm run build` (Tailwind v4 → `static/dist/theme.css`)
|
||||
- Lint: `composer lint`
|
||||
- Tests: `npx playwright test`
|
||||
- Dev site: `http://community-works-collaborative.test/`
|
||||
- Existing test pattern: `tests/mobile-homepage.spec.js:6-14` (`getBox`, `expectNear` helpers; `await expect(...).toBeVisible()` before reading dimensions of lazy-loaded images)
|
||||
- The hero is loaded via `header.php:60` via `get_template_part( 'views/partials/page-hero' )`. Don't move that call.
|
||||
- ACF JSON: add the two new fields by editing `acf/group_60bfb84ae973c.json` (this is the file ACF loads from). New fields keep the same `conditional_logic` rule as the existing `heading` and `intro` fields: shown when `hero_style !== 'none'`.
|
||||
- Per-component CSS for the hero media layers goes in a new `styles/blocks/page-hero.css` and is imported from `styles/blocks/index.css`. Scoping is enforced with a `.page-hero` class root.
|
||||
- Page slug for the dev fixture: `sample-page` (per `content/basic-wp-test-content.xml:7406`). Full URL: `http://community-works-collaborative.test/sample-page/`
|
||||
- No new ACF blocks. No new dependencies. No front-page / single / archive / search / 404 changes. The mockup's bottom dark block is the existing site footer — do not add a new contact CTA to the page body.
|
||||
|
||||
---
|
||||
|
||||
## File Structure
|
||||
|
||||
### Create
|
||||
|
||||
- `wp-content/themes/community-works-collaborative/views/partials/page-intro.php` — Renders the centered narrow intro column from the existing `intro` ACF field. Tailwind utility classes only; no CSS file needed.
|
||||
- `wp-content/themes/community-works-collaborative/styles/blocks/page-hero.css` — Scoped styles for the new hero image/vector media layers (`.page-hero__media`, `.page-hero__vector`, `.page-hero__content`) including the mobile breakpoint at 767px.
|
||||
- `wp-content/themes/community-works-collaborative/tests/inner-page.spec.js` — Playwright tests for the inner page layout at desktop, 402px, 320px, 767px, plus no-intro and no-asset fallback cases.
|
||||
|
||||
### Modify
|
||||
|
||||
- `wp-content/themes/community-works-collaborative/acf/group_60bfb84ae973c.json` — Add `hero_image` and `hero_vector` image fields. Bump `modified` timestamp.
|
||||
- `wp-content/themes/community-works-collaborative/styles/blocks/index.css` — Add `@import './page-hero.css';`.
|
||||
- `wp-content/themes/community-works-collaborative/views/partials/page-hero.php` — Read the two new fields. Render `hero_image` and `hero_vector` as absolutely-positioned media layers. Add `.page-hero`, `.page-hero__media`, `.page-hero__vector`, `.page-hero__content` classes. Stop rendering the in-hero `intro` paragraph.
|
||||
- `wp-content/themes/community-works-collaborative/views/page.php` — Render the page-intro partial between the hero and `the_content` when the `intro` field is non-empty.
|
||||
|
||||
### Touched for verification only (no content changes)
|
||||
|
||||
- `package.json` — Build script is `npm run build`. No change.
|
||||
- `static/dist/theme.css` — Regenerated by `npm run build`. Not edited by hand.
|
||||
|
||||
---
|
||||
|
||||
## Task 1: Add the ACF fields
|
||||
|
||||
**Files:**
|
||||
- Modify: `wp-content/themes/community-works-collaborative/acf/group_60bfb84ae973c.json`
|
||||
|
||||
**Interfaces:**
|
||||
- Produces: Two new fields readable via `get_field( 'hero_image' )` and `get_field( 'hero_vector' )` in the page-hero partial. Both return arrays with at least `['url']` and `['alt']` keys when set, or `false` when empty.
|
||||
|
||||
- [ ] **Step 1: Read the current ACF JSON**
|
||||
|
||||
Read `acf/group_60bfb84ae973c.json` to confirm the current shape.
|
||||
|
||||
- [ ] **Step 2: Add the two new field definitions**
|
||||
|
||||
Insert the following two field objects into the `fields` array, **after** the `call_to_actions` field (the last existing field). Each new field mirrors the `conditional_logic` pattern used by `heading` and `intro` (show when `hero_style !== 'none'`).
|
||||
|
||||
```json
|
||||
,
|
||||
{
|
||||
"key": "field_68new01heroimage",
|
||||
"label": "Hero Image",
|
||||
"name": "hero_image",
|
||||
"aria-label": "",
|
||||
"type": "image",
|
||||
"instructions": "Optional background image for the page hero.",
|
||||
"required": 0,
|
||||
"conditional_logic": [
|
||||
[
|
||||
{
|
||||
"field": "field_60bfda53dc0f2",
|
||||
"operator": "!=",
|
||||
"value": "none"
|
||||
}
|
||||
]
|
||||
],
|
||||
"wrapper": {
|
||||
"width": "",
|
||||
"class": "",
|
||||
"id": ""
|
||||
},
|
||||
"return_format": "array",
|
||||
"library": "all",
|
||||
"min_width": "",
|
||||
"min_height": "",
|
||||
"min_size": "",
|
||||
"max_width": "",
|
||||
"max_height": "",
|
||||
"max_size": "",
|
||||
"mime_types": "",
|
||||
"allow_in_bindings": 0,
|
||||
"preview_size": "medium"
|
||||
},
|
||||
{
|
||||
"key": "field_68new02herovector",
|
||||
"label": "Hero Vector",
|
||||
"name": "hero_vector",
|
||||
"aria-label": "",
|
||||
"type": "image",
|
||||
"instructions": "Optional decorative linework/vector rendered on the right side of the hero.",
|
||||
"required": 0,
|
||||
"conditional_logic": [
|
||||
[
|
||||
{
|
||||
"field": "field_60bfda53dc0f2",
|
||||
"operator": "!=",
|
||||
"value": "none"
|
||||
}
|
||||
]
|
||||
],
|
||||
"wrapper": {
|
||||
"width": "",
|
||||
"class": "",
|
||||
"id": ""
|
||||
},
|
||||
"return_format": "array",
|
||||
"library": "all",
|
||||
"min_width": "",
|
||||
"min_height": "",
|
||||
"min_size": "",
|
||||
"max_width": "",
|
||||
"max_height": "",
|
||||
"max_size": "",
|
||||
"mime_types": "",
|
||||
"allow_in_bindings": 0,
|
||||
"preview_size": "medium"
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 3: Bump the `modified` timestamp**
|
||||
|
||||
Change the top-level `"modified": 1746895738` value to a fresh Unix timestamp. Use `date +%s` (or, on Windows PowerShell: `[int][double]::Parse((Get-Date -UFormat %s))`).
|
||||
|
||||
- [ ] **Step 4: Validate the JSON**
|
||||
|
||||
Run: `cat acf/group_60bfb84ae973c.json | node -e "JSON.parse(require('fs').readFileSync(0,'utf8')); console.log('valid')"`
|
||||
Expected: `valid`
|
||||
|
||||
- [ ] **Step 5: Commit**
|
||||
|
||||
```bash
|
||||
git add acf/group_60bfb84ae973c.json
|
||||
git commit -m "Add hero_image and hero_vector fields to Page Heading group"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Task 2: Create the page-intro partial
|
||||
|
||||
**Files:**
|
||||
- Create: `wp-content/themes/community-works-collaborative/views/partials/page-intro.php`
|
||||
|
||||
**Interfaces:**
|
||||
- Consumes: `get_field( 'intro' )` — a non-empty string when the page has an intro.
|
||||
- Produces: An HTML `<section class="page-intro ...">` block with the intro text wrapped in a `<p>`. No new CSS file; uses Tailwind utilities only.
|
||||
|
||||
- [ ] **Step 1: Create the partial file**
|
||||
|
||||
Write `views/partials/page-intro.php`:
|
||||
|
||||
```php
|
||||
<?php
|
||||
/**
|
||||
* Page Intro Partial
|
||||
*
|
||||
* Renders the centered narrow intro section beneath the page hero.
|
||||
* Loaded by page.php when the `intro` ACF field is non-empty.
|
||||
*
|
||||
* @package CWC
|
||||
*/
|
||||
|
||||
namespace CWC;
|
||||
|
||||
$intro = get_field( 'intro' );
|
||||
|
||||
if ( ! $intro ) {
|
||||
return;
|
||||
}
|
||||
?>
|
||||
|
||||
<section class="page-intro py-12 lg:py-20 text-center" aria-label="<?php echo esc_attr__( 'Page introduction' ); ?>">
|
||||
<div class="container mx-auto max-w-3xl content-wrapper">
|
||||
<p class="page-intro__text text-lg lg:text-xl leading-relaxed text-dark">
|
||||
<?php echo wp_kses_post( $intro ); ?>
|
||||
</p>
|
||||
</div>
|
||||
</section>
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Lint the new file**
|
||||
|
||||
Run: `composer lint -- views/partials/page-intro.php`
|
||||
Expected: no errors. If PHPCS reports style issues, fix the file and re-run until clean.
|
||||
|
||||
- [ ] **Step 3: Commit**
|
||||
|
||||
```bash
|
||||
git add views/partials/page-intro.php
|
||||
git commit -m "Add page-intro partial for centered narrow intro section"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Task 3: Wire the intro partial into page.php
|
||||
|
||||
**Files:**
|
||||
- Modify: `wp-content/themes/community-works-collaborative/views/page.php`
|
||||
|
||||
**Interfaces:**
|
||||
- Consumes: The `page-intro` partial (Task 2). Reads `get_field( 'intro' )`.
|
||||
- Produces: When the `intro` field is set, the page-intro partial renders between the hero (loaded by `header.php`) and `the_content`. When empty, only the body renders.
|
||||
|
||||
- [ ] **Step 1: Read the current page.php**
|
||||
|
||||
Read `views/page.php` to confirm current contents.
|
||||
|
||||
- [ ] **Step 2: Replace page.php with the new version**
|
||||
|
||||
Write `views/page.php` to match:
|
||||
|
||||
```php
|
||||
<?php
|
||||
/**
|
||||
* Single Pages
|
||||
*
|
||||
* @package CWC
|
||||
* @since 1.0.0
|
||||
*/
|
||||
|
||||
namespace CWC;
|
||||
|
||||
get_header();
|
||||
|
||||
$clsEntry = '';
|
||||
|
||||
// Determine classes based on sidebar presence
|
||||
if ( hasSidebar() ) {
|
||||
$classes = 'container grid grid-cols-1 lg:grid-cols-4 gap-8 xl:gap-16 my-section mx-auto';
|
||||
$clsEntry = 'lg:col-span-3';
|
||||
} else {
|
||||
$classes = 'container my-section lg:my-0 mx-auto';
|
||||
}
|
||||
?>
|
||||
|
||||
<article class="<?php echo esc_attr( $classes ); ?>">
|
||||
<div class="entry-content <?php echo esc_attr( $clsEntry ); ?>">
|
||||
<?php
|
||||
// Centered intro section beneath the hero (rendered by header.php).
|
||||
if ( get_field( 'intro' ) ) {
|
||||
get_template_part( 'views/partials/page-intro' );
|
||||
}
|
||||
|
||||
// Page body content
|
||||
if ( have_posts() ) {
|
||||
while ( have_posts() ) {
|
||||
the_post();
|
||||
the_content();
|
||||
}
|
||||
}
|
||||
?>
|
||||
</div>
|
||||
|
||||
<?php if ( hasSidebar() ) : ?>
|
||||
<?php get_sidebar( 'page' ); ?>
|
||||
<?php endif; ?>
|
||||
</article>
|
||||
|
||||
<?php get_footer(); ?>
|
||||
```
|
||||
|
||||
- [ ] **Step 3: Lint the modified file**
|
||||
|
||||
Run: `composer lint -- views/page.php`
|
||||
Expected: no errors.
|
||||
|
||||
- [ ] **Step 4: Commit**
|
||||
|
||||
```bash
|
||||
git add views/page.php
|
||||
git commit -m "Render page-intro partial between hero and body in page.php"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Task 4: Add scoped CSS for the new hero media layers
|
||||
|
||||
**Files:**
|
||||
- Create: `wp-content/themes/community-works-collaborative/styles/blocks/page-hero.css`
|
||||
- Modify: `wp-content/themes/community-works-collaborative/styles/blocks/index.css`
|
||||
|
||||
**Interfaces:**
|
||||
- Produces: Three classes (`.page-hero`, `.page-hero__media`, `.page-hero__vector`, `.page-hero__content`) plus a 767px mobile breakpoint. Used by the page-hero partial in Task 5.
|
||||
|
||||
- [ ] **Step 1: Create the page-hero CSS file**
|
||||
|
||||
Write `styles/blocks/page-hero.css`:
|
||||
|
||||
```css
|
||||
/* Page Hero partial styles (page-hero.php)
|
||||
* Scoped to .page-hero to avoid affecting the homepage-hero block. */
|
||||
|
||||
.page-hero {
|
||||
isolation: isolate;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.page-hero__media {
|
||||
inset: 0;
|
||||
-webkit-mask-image: linear-gradient(to right, transparent 0%, black 30%);
|
||||
mask-image: linear-gradient(to right, transparent 0%, black 30%);
|
||||
position: absolute;
|
||||
z-index: 0;
|
||||
}
|
||||
|
||||
.page-hero__media img {
|
||||
display: block;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.page-hero__vector {
|
||||
bottom: 0;
|
||||
height: auto;
|
||||
pointer-events: none;
|
||||
position: absolute;
|
||||
right: 0;
|
||||
width: clamp(18rem, 38vw, 32rem);
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.page-hero__content {
|
||||
position: relative;
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
@media (max-width: 767px) {
|
||||
.page-hero__media {
|
||||
-webkit-mask-image: linear-gradient(
|
||||
to bottom,
|
||||
rgb(0 0 0 / 0.9) 0%,
|
||||
rgb(0 0 0 / 0.45) 100%
|
||||
);
|
||||
mask-image: linear-gradient(
|
||||
to bottom,
|
||||
rgb(0 0 0 / 0.9) 0%,
|
||||
rgb(0 0 0 / 0.45) 100%
|
||||
);
|
||||
}
|
||||
|
||||
.page-hero__vector {
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: auto;
|
||||
width: 100vw;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Import the new file from the blocks index**
|
||||
|
||||
Edit `styles/blocks/index.css`. After the line `@import './misc.css';` add:
|
||||
|
||||
```css
|
||||
@import './page-hero.css';
|
||||
```
|
||||
|
||||
The full file becomes:
|
||||
|
||||
```css
|
||||
/* Theme block styles */
|
||||
|
||||
@import './buttons.css';
|
||||
@import './core.css';
|
||||
@import './misc.css';
|
||||
@import './page-hero.css';
|
||||
```
|
||||
|
||||
- [ ] **Step 3: Build the CSS**
|
||||
|
||||
Run: `npm run build`
|
||||
Expected: a fresh `static/dist/theme.css` is generated. The output should report a runtime in milliseconds. No errors.
|
||||
|
||||
- [ ] **Step 4: Verify the new classes appear in the built CSS**
|
||||
|
||||
Run: `grep -c "page-hero__media" static/dist/theme.css`
|
||||
Expected: `1` or more.
|
||||
|
||||
Run: `grep -c "page-hero__vector" static/dist/theme.css`
|
||||
Expected: `1` or more.
|
||||
|
||||
- [ ] **Step 5: Commit**
|
||||
|
||||
```bash
|
||||
git add styles/blocks/page-hero.css styles/blocks/index.css static/dist/theme.css
|
||||
git commit -m "Add page-hero media layer CSS and import into blocks bundle"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Task 5: Update page-hero.php to render image and vector and drop the in-hero intro
|
||||
|
||||
**Files:**
|
||||
- Modify: `wp-content/themes/community-works-collaborative/views/partials/page-hero.php`
|
||||
|
||||
**Interfaces:**
|
||||
- Consumes: `get_field( 'hero_image' )` and `get_field( 'hero_vector' )` from the ACF group updated in Task 1. Existing `background_color`, `is_dark`, `heading` fields unchanged.
|
||||
- Produces: A `.page-hero` section with `.page-hero__media` (when `hero_image` is set) and `.page-hero__vector` (when `hero_vector` is set) absolutely-positioned behind a `.page-hero__content` layer. The in-hero intro paragraph is no longer rendered here.
|
||||
|
||||
- [ ] **Step 1: Read the current page-hero.php**
|
||||
|
||||
Read `views/partials/page-hero.php` to confirm current contents.
|
||||
|
||||
- [ ] **Step 2: Replace page-hero.php with the new version**
|
||||
|
||||
Write `views/partials/page-hero.php`:
|
||||
|
||||
```php
|
||||
<?php
|
||||
/**
|
||||
* Page Hero Partial
|
||||
*
|
||||
* @package CWC
|
||||
*/
|
||||
|
||||
namespace CWC;
|
||||
|
||||
// Set variables
|
||||
$bgColor = get_field( 'background_color' );
|
||||
$isDark = get_field( 'is_dark' );
|
||||
$heading = get_field( 'heading' );
|
||||
$heroImage = get_field( 'hero_image' );
|
||||
$heroVector = get_field( 'hero_vector' );
|
||||
|
||||
// Fallback for heading
|
||||
if ( ! $heading ) {
|
||||
$heading = getTheTitle();
|
||||
}
|
||||
|
||||
// Additional logic for dark mode (if needed)
|
||||
if ( is_home() || is_single() || is_archive() || is_search() || is_404() ) {
|
||||
$isDark = true;
|
||||
}
|
||||
?>
|
||||
|
||||
<div class="page-hero bg-cover bg-no-repeat mb-12 py-12 lg:py-16 bg-dark text-light overflow-hidden <?php echo $isDark ? 'dark' : ''; ?>" <?php echo $bgColor ? 'style="background-color: ' . esc_attr( $bgColor ) . '"' : ''; ?>>
|
||||
|
||||
<?php if ( $heroImage ) : ?>
|
||||
<div class="page-hero__media" aria-hidden="true">
|
||||
<img
|
||||
src="<?php echo esc_url( $heroImage['url'] ); ?>"
|
||||
alt=""
|
||||
loading="lazy"
|
||||
role="presentation"
|
||||
/>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php if ( $heroVector ) : ?>
|
||||
<img
|
||||
class="page-hero__vector"
|
||||
src="<?php echo esc_url( $heroVector['url'] ); ?>"
|
||||
alt=""
|
||||
loading="lazy"
|
||||
aria-hidden="true"
|
||||
role="presentation"
|
||||
/>
|
||||
<?php endif; ?>
|
||||
|
||||
<div class="page-hero__content container mx-auto">
|
||||
<div id="breadcrumbs">
|
||||
<?php Breadcrumbs::render(); ?>
|
||||
</div>
|
||||
|
||||
<div class="sm:text-center lg:items-start lg:text-left content-wrapper">
|
||||
<?php
|
||||
// Heading
|
||||
if ( apply_filters( 'include_page_title_in_hero', true ) ) {
|
||||
echo '<h1 class="mx-auto text-center text-light font-normal text-4xl sm:text-5xl lg:text-6xl xl:text-7xl">';
|
||||
echo wp_kses_post( $heading );
|
||||
echo '</h1>';
|
||||
} else {
|
||||
echo '<span class="mx-auto block text-center text-light font-normal text-4xl sm:text-5xl lg:text-6xl xl:text-7xl">';
|
||||
echo wp_kses_post( $heading );
|
||||
echo '</span>';
|
||||
}
|
||||
?>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
```
|
||||
|
||||
Note: the in-hero `<p>` block for the `intro` field is removed. The intro now renders from `page-intro.php` in `page.php`.
|
||||
|
||||
- [ ] **Step 3: Lint the modified file**
|
||||
|
||||
Run: `composer lint -- views/partials/page-hero.php`
|
||||
Expected: no errors.
|
||||
|
||||
- [ ] **Step 4: Commit**
|
||||
|
||||
```bash
|
||||
git add views/partials/page-hero.php
|
||||
git commit -m "Render hero_image and hero_vector in page-hero partial; drop in-hero intro"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Task 6: Write the Playwright test for the inner page
|
||||
|
||||
**Files:**
|
||||
- Create: `wp-content/themes/community-works-collaborative/tests/inner-page.spec.js`
|
||||
|
||||
**Interfaces:**
|
||||
- Consumes: The test page `http://community-works-collaborative.test/sample-page/` (per `content/basic-wp-test-content.xml:7406`). The page-intro partial (Task 2). The page-hero partial with new media layers (Task 5). The CSS file from Task 4.
|
||||
- Produces: A Playwright test file that asserts:
|
||||
- At desktop (1280px): page-hero, page-intro, body all render in that order. Heading and intro visible.
|
||||
- At 402px: same order, vector fills lower portion of hero, intro is centered and within the 48rem (768px) max-width, no clipped text.
|
||||
- At 320px: nothing clips.
|
||||
- At 767px: vector scales proportionally, no clipping.
|
||||
- The page renders without console errors (a final `page.on('pageerror')` listener captures any).
|
||||
|
||||
- [ ] **Step 1: Create the test file**
|
||||
|
||||
Write `tests/inner-page.spec.js`:
|
||||
|
||||
```js
|
||||
import { expect, test } from "@playwright/test";
|
||||
|
||||
const pageUrl =
|
||||
process.env.TEST_PAGE_URL ||
|
||||
"http://community-works-collaborative.test/sample-page/";
|
||||
|
||||
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 in order with the new media layers", 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-intro").first();
|
||||
const content = page.locator(".entry-content").first();
|
||||
|
||||
await expect(hero).toBeVisible();
|
||||
await expect(intro).toBeVisible();
|
||||
await expect(content).toBeVisible();
|
||||
|
||||
// Order check: hero, then intro, then content.
|
||||
const heroBox = await getBox(hero);
|
||||
const introBox = await getBox(intro);
|
||||
const contentBox = await getBox(content);
|
||||
|
||||
expect(heroBox.y).toBeLessThan(introBox.y);
|
||||
expect(introBox.y).toBeLessThan(contentBox.y);
|
||||
|
||||
// Heading is rendered inside the hero.
|
||||
await expect(hero.locator("h1").first()).toBeVisible();
|
||||
|
||||
// Intro is centered and capped at the max-w-3xl width.
|
||||
const introInner = intro.locator(".content-wrapper").first();
|
||||
const introInnerBox = await getBox(introInner);
|
||||
expect(introInnerBox.width).toBeLessThanOrEqual(768 + 3);
|
||||
|
||||
// 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-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);
|
||||
|
||||
expect(heroBox.y).toBeLessThan(introBox.y);
|
||||
expect(introBox.y).toBeLessThan(contentBox.y);
|
||||
|
||||
// Intro fits within the mobile viewport.
|
||||
const introInner = intro.locator(".content-wrapper").first();
|
||||
const introInnerBox = await getBox(introInner);
|
||||
expect(introInnerBox.x + introInnerBox.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-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-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);
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Run the new test**
|
||||
|
||||
Run: `npx playwright test tests/inner-page.spec.js`
|
||||
Expected: 4 tests pass. If a test fails, read the failure, fix the underlying issue (template/CSS, not the test), and re-run until all 4 pass.
|
||||
|
||||
- [ ] **Step 3: Run the full test suite to confirm no regressions**
|
||||
|
||||
Run: `npx playwright test`
|
||||
Expected: the existing 36 tests in `tests/mobile-homepage.spec.js` and `tests/site-a11y.spec.js` still pass, plus the 4 new ones (40 total).
|
||||
|
||||
- [ ] **Step 4: Commit**
|
||||
|
||||
```bash
|
||||
git add tests/inner-page.spec.js
|
||||
git commit -m "Add Playwright coverage for inner page layout at all viewports"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Task 7: Final quality gate
|
||||
|
||||
**Files:** none modified.
|
||||
|
||||
- [ ] **Step 1: Run the production CSS build**
|
||||
|
||||
Run: `npm run build`
|
||||
Expected: a successful build with a fresh `static/dist/theme.css`.
|
||||
|
||||
- [ ] **Step 2: Run PHPCS over the whole theme**
|
||||
|
||||
Run: `composer lint`
|
||||
Expected: no new errors. Existing errors (if any) are pre-existing and out of scope.
|
||||
|
||||
- [ ] **Step 3: Run the full Playwright suite (final pass)**
|
||||
|
||||
Run: `npx playwright test`
|
||||
Expected: all tests pass (40+ tests, 0 failures).
|
||||
|
||||
- [ ] **Step 4: Confirm no stray files**
|
||||
|
||||
Run: `git status`
|
||||
Expected: working tree clean.
|
||||
|
||||
- [ ] **Step 5: Commit any leftover build artifacts**
|
||||
|
||||
If `static/dist/theme.css` was rebuilt and not yet committed:
|
||||
|
||||
```bash
|
||||
git add static/dist/theme.css
|
||||
git commit -m "Rebuild theme.css after final quality gate"
|
||||
```
|
||||
|
||||
If nothing to commit, skip this step.
|
||||
|
||||
---
|
||||
|
||||
## Self-Review
|
||||
|
||||
**Spec coverage:**
|
||||
|
||||
| Spec requirement | Task |
|
||||
| --- | --- |
|
||||
| Add `hero_image` and `hero_vector` fields to Page Heading ACF group | Task 1 |
|
||||
| Render `hero_image` in `views/partials/page-hero.php` with mask treatment | Task 5 (markup) + Task 4 (CSS) |
|
||||
| Render `hero_vector` in `views/partials/page-hero.php`, right-anchored | Task 5 + Task 4 |
|
||||
| Stop rendering the in-hero `intro` paragraph | Task 5 |
|
||||
| New `views/partials/page-intro.php` partial with centered narrow column | Task 2 |
|
||||
| `page.php` renders intro partial between hero and body when `intro` is set | Task 3 |
|
||||
| New `views/partials/page-hero.css` for media-layer styles | Task 4 |
|
||||
| Playwright tests at desktop, 402px, 320px, 767px | Task 6 |
|
||||
| No new ACF blocks | Verified — none added |
|
||||
| Front page, single, archive, search, 404 unchanged | Verified — no template changes outside `page.php` and the partial |
|
||||
| Bottom dark block is the existing footer (not a new CTA) | Verified — no contact-block added |
|
||||
| `npm run build`, full Playwright, `composer lint` | Task 7 |
|
||||
|
||||
**Placeholder scan:** No TBD/TODO. Every step has exact code or commands. No "similar to Task N" references.
|
||||
|
||||
**Type / interface consistency:**
|
||||
|
||||
- `get_field( 'hero_image' )` defined in Task 1, consumed in Task 5 as `['url']` array. Same for `hero_vector`. ✓
|
||||
- `get_template_part( 'views/partials/page-intro' )` used in Task 3, the file created in Task 2. ✓
|
||||
- `.page-hero`, `.page-hero__media`, `.page-hero__vector`, `.page-hero__content` classes defined in Task 4 CSS, used in Task 5 markup, asserted in Task 6. ✓
|
||||
- `pageUrl` in Task 6 default points to the same `sample-page` fixture referenced in the spec. ✓
|
||||
Reference in New Issue
Block a user