diff --git a/tests/ui/trip-filter.spec.js b/tests/ui/trip-filter.spec.js new file mode 100644 index 0000000..88977cd --- /dev/null +++ b/tests/ui/trip-filter.spec.js @@ -0,0 +1,83 @@ +// @ts-check +// Tests: F1–F7 — trip page filter bar and inline stats toggle +const { test, expect } = require('@playwright/test'); + +const TRIP_URL = '/trips/japan-korea-2026'; + +// ── F1: filter bar renders with three buttons ───────────────────────────────── +test('F1: trip page shows All/Journal/Stories filter buttons', async ({ page }) => { + await page.goto(TRIP_URL); + await expect(page.locator('.trip-filter-btn[data-filter="all"]')).toBeVisible(); + await expect(page.locator('.trip-filter-btn[data-filter="journal"]')).toBeVisible(); + await expect(page.locator('.trip-filter-btn[data-filter="story"]')).toBeVisible(); +}); + +// ── F2: "All content" is active by default ──────────────────────────────────── +test('F2: "All content" filter button is active on load', async ({ page }) => { + await page.goto(TRIP_URL); + const allBtn = page.locator('.trip-filter-btn[data-filter="all"]'); + await expect(allBtn).toHaveClass(/is-active/); + await expect(page.locator('.trip-filter-btn[data-filter="journal"]')).not.toHaveClass(/is-active/); + await expect(page.locator('.trip-filter-btn[data-filter="story"]')).not.toHaveClass(/is-active/); +}); + +// ── F3: clicking Journal makes it active and deactivates All ───────────────── +test('F3: clicking Journal sets it as active filter', async ({ page }) => { + await page.goto(TRIP_URL); + await page.click('.trip-filter-btn[data-filter="journal"]'); + await expect(page.locator('.trip-filter-btn[data-filter="journal"]')).toHaveClass(/is-active/); + await expect(page.locator('.trip-filter-btn[data-filter="all"]')).not.toHaveClass(/is-active/); +}); + +// ── F4: Journal filter hides story cards ───────────────────────────────────── +test('F4: Journal filter hides story cards', async ({ page }) => { + await page.goto(TRIP_URL); + await page.click('.trip-filter-btn[data-filter="journal"]'); + const storyCards = page.locator('[data-type="story"]'); + const count = await storyCards.count(); + for (let i = 0; i < count; i++) { + await expect(storyCards.nth(i)).toBeHidden(); + } +}); + +// ── F5: All content filter shows journal cards ──────────────────────────────── +test('F5: All content filter makes journal cards visible', async ({ page }) => { + await page.goto(TRIP_URL); + await page.click('.trip-filter-btn[data-filter="journal"]'); + await page.click('.trip-filter-btn[data-filter="all"]'); + const journalCards = page.locator('[data-type="journal"]'); + await expect(journalCards.first()).toBeVisible(); +}); + +// ── F6: Stories filter shows empty state when no stories exist ──────────────── +test('F6: Stories filter shows empty message when no stories exist', async ({ page }) => { + await page.goto(TRIP_URL); + const storyCount = await page.locator('[data-type="story"]').count(); + if (storyCount === 0) { + await page.click('.trip-filter-btn[data-filter="story"]'); + await expect(page.locator('#feed-filter-empty')).toBeVisible(); + await expect(page.locator('#feed-filter-empty')).toContainText('No stories'); + } else { + test.skip(); + } +}); + +// ── F7: Stats button toggles inline stats block ─────────────────────────────── +test('F7: Stats button expands and collapses inline stats block', async ({ page }) => { + await page.goto(TRIP_URL); + const statsBlock = page.locator('#trip-stats-block'); + const statsBtn = page.locator('#trip-stats-toggle'); + + // hidden by default + await expect(statsBlock).toBeHidden(); + + // click to expand + await statsBtn.click(); + await expect(statsBlock).toBeVisible(); + await expect(statsBtn).toHaveClass(/is-active/); + + // click to collapse + await statsBtn.click(); + await expect(statsBlock).toBeHidden(); + await expect(statsBtn).not.toHaveClass(/is-active/); +});