Add organizer-only attendee list to event detail view (011)
New GET /events/{token}/attendees endpoint returns attendee names when
a valid organizer token is provided (403 otherwise). The frontend
conditionally renders the list below the attendee count for organizers,
silently degrading for visitors.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
99
frontend/e2e/view-attendee-list.spec.ts
Normal file
99
frontend/e2e/view-attendee-list.spec.ts
Normal file
@@ -0,0 +1,99 @@
|
||||
import { http, HttpResponse } from 'msw'
|
||||
import { test, expect } from './msw-setup'
|
||||
|
||||
const eventToken = 'a1b2c3d4-e5f6-7890-abcd-ef1234567890'
|
||||
const organizerToken = 'f9e8d7c6-b5a4-3210-fedc-ba9876543210'
|
||||
|
||||
const fullEvent = {
|
||||
eventToken,
|
||||
title: 'Summer BBQ',
|
||||
description: 'Bring your own drinks!',
|
||||
dateTime: '2026-03-15T20:00:00+01:00',
|
||||
timezone: 'Europe/Berlin',
|
||||
location: 'Central Park, NYC',
|
||||
attendeeCount: 3,
|
||||
expired: false,
|
||||
}
|
||||
|
||||
const attendeesResponse = {
|
||||
attendees: [
|
||||
{ name: 'Alice' },
|
||||
{ name: 'Bob' },
|
||||
{ name: 'Charlie' },
|
||||
],
|
||||
}
|
||||
|
||||
test.describe('US-1: View attendee list as organizer', () => {
|
||||
test('organizer sees attendee names', async ({ page, network }) => {
|
||||
network.use(
|
||||
http.get('*/api/events/:token', () => {
|
||||
return HttpResponse.json(fullEvent)
|
||||
}),
|
||||
http.get('*/api/events/:token/attendees', () => {
|
||||
return HttpResponse.json(attendeesResponse)
|
||||
}),
|
||||
)
|
||||
|
||||
// Set organizer token in localStorage before navigating
|
||||
await page.goto('/')
|
||||
await page.evaluate(
|
||||
([et, ot]) => {
|
||||
localStorage.setItem(
|
||||
'fete:events',
|
||||
JSON.stringify([{ eventToken: et, organizerToken: ot, title: 'Summer BBQ', dateTime: '2026-03-15T20:00:00+01:00', expiryDate: '' }]),
|
||||
)
|
||||
},
|
||||
[eventToken, organizerToken],
|
||||
)
|
||||
|
||||
await page.goto(`/events/${eventToken}`)
|
||||
|
||||
await expect(page.getByRole('heading', { name: 'Summer BBQ' })).toBeVisible()
|
||||
await expect(page.getByText('3 Attendees')).toBeVisible()
|
||||
await expect(page.getByText('Alice')).toBeVisible()
|
||||
await expect(page.getByText('Bob')).toBeVisible()
|
||||
await expect(page.getByText('Charlie')).toBeVisible()
|
||||
})
|
||||
|
||||
test('visitor does not see attendee list', async ({ page, network }) => {
|
||||
network.use(
|
||||
http.get('*/api/events/:token', () => {
|
||||
return HttpResponse.json(fullEvent)
|
||||
}),
|
||||
)
|
||||
|
||||
await page.goto(`/events/${eventToken}`)
|
||||
|
||||
await expect(page.getByRole('heading', { name: 'Summer BBQ' })).toBeVisible()
|
||||
await expect(page.getByText('3 going')).toBeVisible()
|
||||
await expect(page.locator('.attendee-list')).not.toBeVisible()
|
||||
})
|
||||
|
||||
test('organizer sees empty state when no attendees', async ({ page, network }) => {
|
||||
network.use(
|
||||
http.get('*/api/events/:token', () => {
|
||||
return HttpResponse.json({ ...fullEvent, attendeeCount: 0 })
|
||||
}),
|
||||
http.get('*/api/events/:token/attendees', () => {
|
||||
return HttpResponse.json({ attendees: [] })
|
||||
}),
|
||||
)
|
||||
|
||||
await page.goto('/')
|
||||
await page.evaluate(
|
||||
([et, ot]) => {
|
||||
localStorage.setItem(
|
||||
'fete:events',
|
||||
JSON.stringify([{ eventToken: et, organizerToken: ot, title: 'Summer BBQ', dateTime: '2026-03-15T20:00:00+01:00', expiryDate: '' }]),
|
||||
)
|
||||
},
|
||||
[eventToken, organizerToken],
|
||||
)
|
||||
|
||||
await page.goto(`/events/${eventToken}`)
|
||||
|
||||
await expect(page.getByRole('heading', { name: 'Summer BBQ' })).toBeVisible()
|
||||
await expect(page.getByText('0 Attendees')).toBeVisible()
|
||||
await expect(page.getByText('No attendees yet.')).toBeVisible()
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user