diff --git a/CLAUDE.md b/CLAUDE.md index 04e887e..1317ff3 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -6,7 +6,7 @@ Rules, gotchas, and entry points — the things that must change what you do *be |---|---| | How the site hangs together — stack, plugin roles, templates, partial contracts, data flows | [`docs/reference/architecture.md`](docs/reference/architecture.md) | | Domain vocabulary — Trip, Entry, Story, Active Trip | [`CONCEPTS.md`](CONCEPTS.md) | -| Doing something operational — posting, GPX, switching trips, local setup, deploying | [`docs/guides/`](docs/guides/) | +| Doing something operational — posting, writing stories, GPX, switching trips, local setup, deploying | [`docs/guides/`](docs/guides/) | | Test suite layout and conventions | [`docs/reference/testing.md`](docs/reference/testing.md) | | A bug or workflow trap already hit and written up | [`docs/solutions/`](docs/solutions/) — grep the `module`/`tags`/`problem_type` frontmatter; check when working in a documented area | | Folder map, prerequisites, the full `make` command list | [`README.md`](README.md) | @@ -20,7 +20,8 @@ The site is Grav (flat-file PHP CMS, no database) in Docker, with content and th - **Never SSH to a server directly** — use the `make remote-*` targets, since credentials live in `.env`. If no target covers what you need, ask the user to run it or propose a new target. - **Never hand-edit build output** — sources and outputs share folders under `user/themes/intotheeast/` (paths below are relative to it), so know which is which. Run `make build-assets` after editing any source. - Everything in `js/` is **generated** *except* `js/src/`, `js/maplibre-utils.js` and `js/nav.js`. - - `css-compiled/` and `fonts/` are generated (sources: `css/style.css`, `css/tokens.css`); so is `templates/partials/weather-icons.html.twig` (source: `scripts/gen-weather-icons.js`). + - `css-compiled/` and `fonts/` are generated by esbuild from the `js/src/` entrypoints' CSS and font imports (fontsource, photoswipe, maplibre-gl) — **not** from `css/`. `css/style.css` and `css/tokens.css` are hand-authored and served directly (`partials/base.html.twig`), so editing them needs no rebuild. + - `templates/partials/weather-icons.html.twig` is generated (source: `scripts/gen-weather-icons.js`). - **Never toggle dev↔prod mode mid-session.** If a caching or config issue appears, fix it at the application level (plugin, template logic) rather than flipping a mode flag — mode switches leave inconsistent state and make bugs harder to reproduce. ## Dev environment diff --git a/tests/ui/stories/stories.spec.js b/tests/ui/stories/stories.spec.js index b35f13f..369237a 100644 --- a/tests/ui/stories/stories.spec.js +++ b/tests/ui/stories/stories.spec.js @@ -1,5 +1,5 @@ // @ts-check -// Tests: S1–S7 — story mode rendering and navigation +// Tests: S1–S9 — story mode rendering and navigation // Requires demo data: run `make demo-load` before this suite. const { test, expect } = require('@playwright/test'); @@ -85,3 +85,74 @@ test('S7: story body back link has back-pill class', async ({ page }) => { await expect(bodyBack).toBeAttached(); await expect(bodyBack).toHaveText(/← Back/); }); + +// ── S8: Scrolly-section text panels actually render beside the pinned image ─── +// The server ships the panel text inside .scrolly__steps-content, which CSS hides +// (style.css: `display: none`). Only the inline Scrollama block in story.html.twig +// splits it into visible .scrolly-step divs — and it early-returns silently if the +// main.js bundle (which sets window.scrollama) hasn't executed yet. S3 asserted the +// image column exists; nothing asserted the text column was non-empty. +test('S8: scrolly-section builds visible step panels from its slot content', async ({ page }) => { + await page.goto(STORY_SCROLLY); + await expect(page.locator('.story-hero__img')).toBeVisible({ timeout: 8000 }); + + // The bundle must have published scrollama before the inline block ran + expect( + await page.evaluate(() => typeof window.scrollama !== 'undefined'), + 'window.scrollama published by main.js bundle' + ).toBe(true); + + // Every scrolly-section must have produced at least one step + const sections = page.locator('.scrolly'); + const sectionCount = await sections.count(); + expect(sectionCount, 'Two scrolly-sections').toBe(2); + + for (let i = 0; i < sectionCount; i++) { + const section = sections.nth(i); + const steps = section.locator('.scrolly-step'); + expect( + await steps.count(), + `scrolly-section ${i} split its slot content into steps` + ).toBeGreaterThan(0); + } + + // …and the text must be readable, not left hidden in the raw slot. + // Scroll each step into view so its reveal transition completes. + const firstStep = page.locator('.scrolly').first().locator('.scrolly-step').first(); + await firstStep.scrollIntoViewIfNeeded(); + await page.waitForTimeout(800); + await expect(firstStep.locator('.scrolly-step__inner')).toBeVisible(); + const text = (await firstStep.innerText()).trim(); + expect(text.length, 'First step panel renders non-empty text').toBeGreaterThan(20); +}); + +// ── S9: Back-to-top is wired once, by main.js, and pushes a history entry ───── +// The inline duplicate in story.html.twig was removed; initBackToTop() in +// js/src/main.js now solely owns #story-totop. That makes the button depend on +// the bundle having loaded, so assert the observable behaviour end to end. +test('S9: story back-to-top reveals on scroll, returns to top, and pushes history', async ({ page }) => { + await page.goto(STORY_SCROLLY); + await expect(page.locator('.story-hero__img')).toBeVisible({ timeout: 8000 }); + + const btn = page.locator('#story-totop'); + await expect(btn).toBeAttached(); + + // Hidden until scrolled past the 0.8 * viewport threshold + await expect(btn).not.toHaveClass(/is-visible/); + + const historyBefore = await page.evaluate(() => history.length); + + await page.evaluate(() => window.scrollTo(0, window.innerHeight * 2)); + await expect(btn).toHaveClass(/is-visible/, { timeout: 3000 }); + + await btn.click(); + await expect + .poll(() => page.evaluate(() => window.scrollY), { timeout: 3000 }) + .toBeLessThan(10); + + // main.js's variant pushes a history entry; the removed inline copy did not + expect( + await page.evaluate(() => history.length), + 'Back-to-top pushed a history entry' + ).toBeGreaterThan(historyBefore); +});