Files
intotheeast-com/tests/ui/stories/stories.spec.js
T
m038andClaude Opus 5 143ec135c0 test(stories): cover scrolly step text and back-to-top; fix build-output note
S8 asserts scrolly-section splits its slot content into visible step panels
(the blank-text-column regression had no coverage: S3 only checked the image
column existed, and the silent early return threw nothing for S4 to catch).
S9 covers back-to-top now that main.js solely owns it, including the history
entry the removed inline copy lacked.

CLAUDE.md attributed css-compiled/ and fonts/ to css/style.css and
css/tokens.css. They are built from the js/src/ entrypoints' CSS and font
imports; css/ is hand-authored and served directly, needing no rebuild.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-29 23:16:22 +02:00

159 lines
8.3 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// @ts-check
// Tests: S1S9 — story mode rendering and navigation
// Requires demo data: run `make demo-load` before this suite.
const { test, expect } = require('@playwright/test');
const TRIP_URL = '/trips/italy-2026-demo'; // stories now surface in the trip feed
const STORY_GALLERY = '/trips/italy-2026-demo/stories/val-dorcia-at-dawn'; // gallery-led: snap-gallery × 2, chapter-break, text-only pull-quote
const STORY_SCROLLY = '/trips/italy-2026-demo/stories/sorano-rock-and-time'; // scrolly-led: scrolly-section × 2, chapter-break, pull-quote with image
const DEMO_STORY = '/trips/italy-2026-demo/stories/val-dorcia-at-dawn'; // used for cross-trip hero sanity check
// ── S1: Trip feed shows story cards ──────────────────────────────────────────
test('S1: trip feed renders at least 3 story cards', async ({ page }) => {
await page.goto(TRIP_URL);
const cards = page.locator('[data-type="story"]');
await expect(cards.first()).toBeVisible({ timeout: 5000 });
const count = await cards.count();
expect(count, 'At least 3 story cards').toBeGreaterThanOrEqual(3);
});
// ── S2: Gallery-led story — hero image + snap-gallery + chapter-break + text-only pull-quote ──
test('S2: gallery-led story renders hero, snap-gallery, chapter-break, text-only pull-quote', async ({ page }) => {
await page.goto(STORY_GALLERY);
// Hero: real image rendered, no placeholder
await expect(page.locator('.story-hero__img')).toBeVisible({ timeout: 8000 });
await expect(page.locator('.story-hero__img-placeholder')).toHaveCount(0);
// Snap-gallery (there are two in this story)
const galleries = page.locator('.pgallery');
await expect(galleries.first()).toBeVisible();
expect(await galleries.count(), 'Two snap-galleries').toBe(2);
// Chapter-break
await expect(page.locator('.chapter-break')).toBeVisible();
// Text-only pull-quote (no background image variant)
await expect(page.locator('.pull-quote__inner--no-image')).toBeVisible();
});
// ── S3: Scrolly-led story — two scrolly-sections + pull-quote with image ─────
test('S3: scrolly-led story renders two scrolly-sections and pull-quote with background image', async ({ page }) => {
await page.goto(STORY_SCROLLY);
await expect(page.locator('.story-hero__img')).toBeVisible({ timeout: 8000 });
// Two scrolly-sections
const scrollySections = page.locator('.scrolly');
await expect(scrollySections.first()).toBeVisible();
expect(await scrollySections.count(), 'Two scrolly-sections').toBe(2);
// Pull-quote with background image
await expect(page.locator('.pull-quote__bg')).toBeVisible();
});
// ── S4: Scrolly story loads without JS errors (Scrollama CDN) ────────────────
test('S4: scrolly story page loads without JS errors', async ({ page }) => {
const errors = [];
page.on('pageerror', e => errors.push(e.message));
await page.goto(STORY_SCROLLY);
await expect(page.locator('.story-hero__img')).toBeVisible({ timeout: 8000 });
await page.waitForTimeout(1000);
expect(errors, 'No JS errors on story page').toHaveLength(0);
});
// ── S5: Back button returns to the originating trip page ─────────────────────
test('S5: story back button navigates back to the trip page', async ({ page }) => {
// Establish history: trip page → story → back
await page.goto(TRIP_URL);
await page.locator('[data-type="story"]').first().click();
await expect(page.locator('.story-hero__img')).toBeVisible({ timeout: 8000 });
await page.locator('.story-escape').click();
// story-escape runs history.back() when history exists → back to the trip page
await expect(page).toHaveURL(/\/trips\/italy-2026-demo$/);
await expect(page.locator('.journal-post').first()).toBeVisible();
});
// ── S6: Demo story — hero image sanity check ─────────────────────────────────
test('S6: demo story renders hero image without placeholder', async ({ page }) => {
await page.goto(DEMO_STORY);
await expect(page.locator('.story-hero__img')).toBeVisible({ timeout: 8000 });
await expect(page.locator('.story-hero__img-placeholder')).toHaveCount(0);
});
// ── S7: Story body back link is styled as a back-pill ────────────────────────
test('S7: story body back link has back-pill class', async ({ page }) => {
await page.goto('/trips/italy-2026-demo/stories/val-dorcia-at-dawn');
await expect(page.locator('.story-hero__img')).toBeVisible({ timeout: 8000 });
// Scroll past the hero to reveal the story body
await page.evaluate(() => window.scrollBy(0, window.innerHeight * 1.5));
await page.waitForTimeout(300);
const bodyBack = page.locator('.story-footer .back-pill');
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);
});