import { test, expect } from './msw-setup' import type { StoredEvent } from '../src/composables/useEventStorage' const STORAGE_KEY = 'fete:events' const futureEvent1: StoredEvent = { eventToken: 'future-aaa', title: 'Summer BBQ', dateTime: '2027-06-15T18:00:00Z', organizerToken: 'org-token-1', } const futureEvent2: StoredEvent = { eventToken: 'future-bbb', title: 'Team Meeting', dateTime: '2027-01-10T09:00:00Z', rsvpToken: 'rsvp-token-1', rsvpName: 'Alice', } const pastEvent: StoredEvent = { eventToken: 'past-ccc', title: 'New Year Party', dateTime: '2025-01-01T00:00:00Z', } function seedEvents(events: StoredEvent[]): string { return `window.localStorage.setItem('${STORAGE_KEY}', ${JSON.stringify(JSON.stringify(events))})` } test.describe('US2: Empty State', () => { test('shows empty state when no events are stored', async ({ page }) => { await page.goto('/') await expect(page.getByText('No events yet')).toBeVisible() await expect(page.getByRole('link', { name: /Create Event/ })).toBeVisible() }) test('empty state links to create page', async ({ page }) => { await page.goto('/') const link = page.getByRole('link', { name: /Create Event/ }) await expect(link).toHaveAttribute('href', '/create') }) test('empty state is hidden when events exist', async ({ page }) => { await page.addInitScript(seedEvents([futureEvent1])) await page.goto('/') await expect(page.getByText('No events yet')).not.toBeVisible() }) }) test.describe('US4: Past Events Appear Faded', () => { test('past events have the faded modifier class', async ({ page }) => { await page.addInitScript(seedEvents([futureEvent1, pastEvent])) await page.goto('/') const cards = page.locator('.event-card') await expect(cards).toHaveCount(2) // Future event should NOT have past class const futureCard = cards.filter({ hasText: 'Summer BBQ' }) await expect(futureCard).not.toHaveClass(/event-card--past/) // Past event should have past class const pastCard = cards.filter({ hasText: 'New Year Party' }) await expect(pastCard).toHaveClass(/event-card--past/) }) test('past events remain clickable', async ({ page, network }) => { await page.addInitScript(seedEvents([pastEvent])) const { http, HttpResponse } = await import('msw') network.use( http.get('*/api/events/:token', () => { return HttpResponse.json({ eventToken: pastEvent.eventToken, title: pastEvent.title, dateTime: pastEvent.dateTime, description: '', location: '', timezone: 'UTC', attendeeCount: 0, }) }), ) await page.goto('/') await page.getByText('New Year Party').click() await expect(page).toHaveURL(`/events/${pastEvent.eventToken}`) }) }) test.describe('US3: Remove Event from List', () => { test('delete icon triggers confirmation dialog, confirm removes event', async ({ page }) => { await page.addInitScript(seedEvents([futureEvent1, futureEvent2])) await page.goto('/') // Both events visible await expect(page.getByText('Summer BBQ')).toBeVisible() await expect(page.getByText('Team Meeting')).toBeVisible() // Click delete on Summer BBQ await page.getByRole('button', { name: /Remove Summer BBQ/ }).click() // Confirmation dialog appears await expect(page.getByText('Remove event?')).toBeVisible() // Confirm removal await page.getByRole('button', { name: 'Remove', exact: true }).click() // Event is gone, other remains await expect(page.getByText('Summer BBQ')).not.toBeVisible() await expect(page.getByText('Team Meeting')).toBeVisible() }) test('cancel keeps the event in the list', async ({ page }) => { await page.addInitScript(seedEvents([futureEvent1])) await page.goto('/') await page.getByRole('button', { name: /Remove Summer BBQ/ }).click() await expect(page.getByText('Remove event?')).toBeVisible() // Cancel await page.getByRole('button', { name: 'Cancel' }).click() // Dialog gone, event still there await expect(page.getByText('Remove event?')).not.toBeVisible() await expect(page.getByText('Summer BBQ')).toBeVisible() }) }) test.describe('US5: Visual Distinction for Event Roles', () => { test('shows organizer badge for events with organizerToken', async ({ page }) => { await page.addInitScript(seedEvents([futureEvent1])) await page.goto('/') const card = page.locator('.event-card').filter({ hasText: 'Summer BBQ' }) const badge = card.locator('.event-card__badge') await expect(badge).toBeVisible() await expect(badge).toHaveText('Organizer') await expect(badge).toHaveClass(/event-card__badge--organizer/) }) test('shows attendee badge for events with rsvpToken only', async ({ page }) => { await page.addInitScript(seedEvents([futureEvent2])) await page.goto('/') const card = page.locator('.event-card').filter({ hasText: 'Team Meeting' }) const badge = card.locator('.event-card__badge') await expect(badge).toBeVisible() await expect(badge).toHaveText('Attendee') await expect(badge).toHaveClass(/event-card__badge--attendee/) }) test('shows watcher badge for events without organizerToken or rsvpToken', async ({ page }) => { await page.addInitScript(seedEvents([pastEvent])) await page.goto('/') const card = page.locator('.event-card').filter({ hasText: 'New Year Party' }) const badge = card.locator('.event-card__badge') await expect(badge).toBeVisible() await expect(badge).toHaveText('Watching') await expect(badge).toHaveClass(/event-card__badge--watcher/) }) }) test.describe('FAB: Create Event Button', () => { test('FAB is visible when events exist', async ({ page }) => { await page.addInitScript(seedEvents([futureEvent1])) await page.goto('/') const fab = page.getByRole('link', { name: 'Create event' }) await expect(fab).toBeVisible() }) test('FAB navigates to create page', async ({ page }) => { await page.addInitScript(seedEvents([futureEvent1])) await page.goto('/') const fab = page.getByRole('link', { name: 'Create event' }) await expect(fab).toHaveAttribute('href', '/create') }) test('FAB is not visible on empty state (empty state has its own CTA)', async ({ page }) => { await page.goto('/') await expect(page.locator('.fab')).toHaveCount(0) }) }) test.describe('Temporal Grouping: Section Headers', () => { test('events are distributed under correct section headers', async ({ page }) => { // Use dates relative to "now" to ensure correct section assignment const now = new Date() const todayEvent: StoredEvent = { eventToken: 'today-1', title: 'Today Standup', dateTime: new Date(now.getFullYear(), now.getMonth(), now.getDate(), 18, 0, 0).toISOString(), } const laterEvent: StoredEvent = { eventToken: 'later-1', title: 'Future Conference', dateTime: new Date(now.getFullYear() + 1, 0, 15, 10, 0, 0).toISOString(), } await page.addInitScript(seedEvents([todayEvent, laterEvent, pastEvent])) await page.goto('/') // Verify section headers appear await expect(page.getByRole('heading', { name: 'Today', level: 2 })).toBeVisible() await expect(page.getByRole('heading', { name: 'Later', level: 2 })).toBeVisible() await expect(page.getByRole('heading', { name: 'Past', level: 2 })).toBeVisible() // Events are in the correct sections const sections = page.locator('.event-section') const todaySection = sections.filter({ has: page.getByRole('heading', { name: 'Today', level: 2 }) }) await expect(todaySection.getByText('Today Standup')).toBeVisible() const laterSection = sections.filter({ has: page.getByRole('heading', { name: 'Later', level: 2 }) }) await expect(laterSection.getByText('Future Conference')).toBeVisible() const pastSection = sections.filter({ has: page.getByRole('heading', { name: 'Past', level: 2 }) }) await expect(pastSection.getByText('New Year Party')).toBeVisible() }) test('empty sections are not rendered', async ({ page }) => { // Only a past event — no Today, This Week, or Later sections await page.addInitScript(seedEvents([pastEvent])) await page.goto('/') await expect(page.getByRole('heading', { name: 'Past', level: 2 })).toBeVisible() await expect(page.getByRole('heading', { name: 'Today', level: 2 })).toHaveCount(0) await expect(page.getByRole('heading', { name: 'This Week', level: 2 })).toHaveCount(0) await expect(page.getByRole('heading', { name: 'Next Week', level: 2 })).toHaveCount(0) await expect(page.getByRole('heading', { name: 'Later', level: 2 })).toHaveCount(0) }) test('Today section header has emphasis CSS class', async ({ page }) => { const now = new Date() const todayEvent: StoredEvent = { eventToken: 'today-emph', title: 'Emphasis Test', dateTime: new Date(now.getFullYear(), now.getMonth(), now.getDate(), 20, 0, 0).toISOString(), } await page.addInitScript(seedEvents([todayEvent])) await page.goto('/') const todayHeader = page.getByRole('heading', { name: 'Today', level: 2 }) await expect(todayHeader).toHaveClass(/section-header--emphasized/) }) }) test.describe('Temporal Grouping: Date Subheaders', () => { test('no date subheader in Today section', async ({ page }) => { const now = new Date() const todayEvent: StoredEvent = { eventToken: 'today-sub', title: 'No Subheader Test', dateTime: new Date(now.getFullYear(), now.getMonth(), now.getDate(), 19, 0, 0).toISOString(), } await page.addInitScript(seedEvents([todayEvent])) await page.goto('/') const todaySection = page.locator('.event-section').filter({ has: page.getByRole('heading', { name: 'Today', level: 2 }), }) await expect(todaySection.locator('.date-subheader')).toHaveCount(0) }) test('date subheaders appear in Later section', async ({ page }) => { await page.addInitScript(seedEvents([futureEvent1, futureEvent2])) await page.goto('/') const laterSection = page.locator('.event-section').filter({ has: page.getByRole('heading', { name: 'Later', level: 2 }), }) // Both future events are on different dates, so expect subheaders const subheaders = laterSection.locator('.date-subheader') await expect(subheaders).toHaveCount(2) }) test('date subheaders appear in Past section', async ({ page }) => { await page.addInitScript(seedEvents([pastEvent])) await page.goto('/') const pastSection = page.locator('.event-section').filter({ has: page.getByRole('heading', { name: 'Past', level: 2 }), }) await expect(pastSection.locator('.date-subheader')).toHaveCount(1) }) }) test.describe('Temporal Grouping: Time Display', () => { test('future event cards show clock time', async ({ page }) => { await page.addInitScript(seedEvents([futureEvent1])) await page.goto('/') const timeLabel = page.locator('.event-card__time') const text = await timeLabel.first().textContent() // Should show clock time (e.g., "18:00" or "6:00 PM"), not relative time expect(text).toMatch(/\d{1,2}[:.]\d{2}/) }) test('past event cards show relative time', async ({ page }) => { await page.addInitScript(seedEvents([pastEvent])) await page.goto('/') const timeLabel = page.locator('.event-card__time') const text = await timeLabel.first().textContent() // Should show relative time like "X years ago" or "last year" expect(text).toMatch(/ago|last|yesterday/) }) }) test.describe('US1: View My Events', () => { test('displays all stored events with title and relative time', async ({ page }) => { await page.addInitScript(seedEvents([futureEvent1, futureEvent2, pastEvent])) await page.goto('/') await expect(page.getByText('Summer BBQ')).toBeVisible() await expect(page.getByText('Team Meeting')).toBeVisible() await expect(page.getByText('New Year Party')).toBeVisible() }) test('events are sorted: upcoming ascending, then past', async ({ page }) => { await page.addInitScript(seedEvents([futureEvent1, futureEvent2, pastEvent])) await page.goto('/') const titles = page.locator('.event-card__title') await expect(titles).toHaveCount(3) // Team Meeting (Jan 2027) before Summer BBQ (Jun 2027), then past event await expect(titles.nth(0)).toHaveText('Team Meeting') await expect(titles.nth(1)).toHaveText('Summer BBQ') await expect(titles.nth(2)).toHaveText('New Year Party') }) test('clicking an event navigates to its detail page', async ({ page, network }) => { await page.addInitScript(seedEvents([futureEvent1])) // Mock the event detail API so navigation doesn't fail const { http, HttpResponse } = await import('msw') network.use( http.get('*/api/events/:token', () => { return HttpResponse.json({ eventToken: futureEvent1.eventToken, title: futureEvent1.title, dateTime: futureEvent1.dateTime, description: '', location: '', timezone: 'UTC', attendeeCount: 0, }) }), ) await page.goto('/') await page.getByText('Summer BBQ').click() await expect(page).toHaveURL(`/events/${futureEvent1.eventToken}`) }) test('each event shows a relative time label', async ({ page }) => { await page.addInitScript(seedEvents([futureEvent1])) await page.goto('/') // The relative time element should exist and contain text (exact value depends on current time) const timeLabel = page.locator('.event-card__time') await expect(timeLabel).toHaveCount(1) await expect(timeLabel.first()).not.toBeEmpty() }) })