Add EventDetailView with loading, expired, not-found, and error states
New view fetches event via openapi-fetch, formats date/time with Intl.DateTimeFormat. Skeleton shimmer during loading (CSS-only). Create form now sends auto-detected timezone. Unit tests for all five view states, E2E tests with MSW mocks. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -12,7 +12,7 @@ test.describe('US-1: Create an event', () => {
|
||||
await expect(page.getByText('Expiry date is required.')).toBeVisible()
|
||||
})
|
||||
|
||||
test('creates an event and redirects to stub page', async ({ page }) => {
|
||||
test('creates an event and redirects to event detail page', async ({ page }) => {
|
||||
await page.goto('/create')
|
||||
|
||||
await page.getByLabel(/title/i).fill('Summer BBQ')
|
||||
@@ -24,7 +24,6 @@ test.describe('US-1: Create an event', () => {
|
||||
await page.getByRole('button', { name: /create event/i }).click()
|
||||
|
||||
await expect(page).toHaveURL(/\/events\/.+/)
|
||||
await expect(page.getByText('Event created!')).toBeVisible()
|
||||
})
|
||||
|
||||
test('stores event data in localStorage after creation', async ({ page }) => {
|
||||
|
||||
127
frontend/e2e/event-view.spec.ts
Normal file
127
frontend/e2e/event-view.spec.ts
Normal file
@@ -0,0 +1,127 @@
|
||||
import { http, HttpResponse } from 'msw'
|
||||
import { test, expect } from './msw-setup'
|
||||
|
||||
const fullEvent = {
|
||||
eventToken: 'a1b2c3d4-e5f6-7890-abcd-ef1234567890',
|
||||
title: 'Summer BBQ',
|
||||
description: 'Bring your own drinks!',
|
||||
dateTime: '2026-03-15T20:00:00+01:00',
|
||||
timezone: 'Europe/Berlin',
|
||||
location: 'Central Park, NYC',
|
||||
attendeeCount: 12,
|
||||
expired: false,
|
||||
}
|
||||
|
||||
test.describe('US-1: View event details', () => {
|
||||
test('displays all event fields for a valid event', async ({ page, network }) => {
|
||||
network.use(
|
||||
http.get('*/api/events/:token', () => {
|
||||
return HttpResponse.json(fullEvent)
|
||||
}),
|
||||
)
|
||||
|
||||
await page.goto(`/events/${fullEvent.eventToken}`)
|
||||
|
||||
await expect(page.getByRole('heading', { name: 'Summer BBQ' })).toBeVisible()
|
||||
await expect(page.getByText('Bring your own drinks!')).toBeVisible()
|
||||
await expect(page.getByText('Central Park, NYC')).toBeVisible()
|
||||
await expect(page.getByText('12')).toBeVisible()
|
||||
await expect(page.getByText('Europe/Berlin')).toBeVisible()
|
||||
await expect(page.getByText('2026')).toBeVisible()
|
||||
})
|
||||
|
||||
test('does not load external resources', async ({ page, network }) => {
|
||||
const externalRequests: string[] = []
|
||||
page.on('request', (req) => {
|
||||
const url = new URL(req.url())
|
||||
if (!['localhost', '127.0.0.1'].includes(url.hostname)) {
|
||||
externalRequests.push(req.url())
|
||||
}
|
||||
})
|
||||
|
||||
network.use(
|
||||
http.get('*/api/events/:token', () => {
|
||||
return HttpResponse.json(fullEvent)
|
||||
}),
|
||||
)
|
||||
|
||||
await page.goto(`/events/${fullEvent.eventToken}`)
|
||||
await expect(page.getByRole('heading', { name: 'Summer BBQ' })).toBeVisible()
|
||||
|
||||
expect(externalRequests).toEqual([])
|
||||
})
|
||||
})
|
||||
|
||||
test.describe('US-2: View expired event', () => {
|
||||
test('shows "event has ended" banner for expired event', async ({ page, network }) => {
|
||||
network.use(
|
||||
http.get('*/api/events/:token', () => {
|
||||
return HttpResponse.json({ ...fullEvent, expired: true })
|
||||
}),
|
||||
)
|
||||
|
||||
await page.goto(`/events/${fullEvent.eventToken}`)
|
||||
|
||||
await expect(page.getByText('This event has ended.')).toBeVisible()
|
||||
})
|
||||
})
|
||||
|
||||
test.describe('US-4: Event not found', () => {
|
||||
test('shows "event not found" for unknown token', async ({ page, network }) => {
|
||||
network.use(
|
||||
http.get('*/api/events/:token', () => {
|
||||
return HttpResponse.json(
|
||||
{ type: 'urn:problem-type:event-not-found', title: 'Event Not Found', status: 404, detail: 'Event not found.' },
|
||||
{ status: 404, headers: { 'Content-Type': 'application/problem+json' } },
|
||||
)
|
||||
}),
|
||||
)
|
||||
|
||||
await page.goto('/events/00000000-0000-0000-0000-000000000000')
|
||||
|
||||
await expect(page.getByText('Event not found.')).toBeVisible()
|
||||
// No event data visible
|
||||
await expect(page.locator('.detail__title')).not.toBeVisible()
|
||||
})
|
||||
})
|
||||
|
||||
test.describe('Server error', () => {
|
||||
test('shows error message and retry button on 500', async ({ page, network }) => {
|
||||
network.use(
|
||||
http.get('*/api/events/:token', () => {
|
||||
return HttpResponse.json(
|
||||
{ type: 'about:blank', title: 'Internal Server Error', status: 500, detail: 'An unexpected error occurred.' },
|
||||
{ status: 500, headers: { 'Content-Type': 'application/problem+json' } },
|
||||
)
|
||||
}),
|
||||
)
|
||||
|
||||
await page.goto(`/events/${fullEvent.eventToken}`)
|
||||
|
||||
await expect(page.getByText('Something went wrong.')).toBeVisible()
|
||||
await expect(page.getByRole('button', { name: 'Retry' })).toBeVisible()
|
||||
})
|
||||
|
||||
test('retry button re-fetches the event', async ({ page, network }) => {
|
||||
let callCount = 0
|
||||
network.use(
|
||||
http.get('*/api/events/:token', () => {
|
||||
callCount++
|
||||
if (callCount === 1) {
|
||||
return HttpResponse.json(
|
||||
{ type: 'about:blank', title: 'Error', status: 500 },
|
||||
{ status: 500, headers: { 'Content-Type': 'application/problem+json' } },
|
||||
)
|
||||
}
|
||||
return HttpResponse.json(fullEvent)
|
||||
}),
|
||||
)
|
||||
|
||||
await page.goto(`/events/${fullEvent.eventToken}`)
|
||||
await expect(page.getByText('Something went wrong.')).toBeVisible()
|
||||
|
||||
await page.getByRole('button', { name: 'Retry' }).click()
|
||||
|
||||
await expect(page.getByRole('heading', { name: 'Summer BBQ' })).toBeVisible()
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user