Files
fete/frontend/e2e/watch-event.spec.ts
nitrix 8885dbd722 Soften RSVP cancellation dialog wording
Replace harsh "permanently cancelled" language with friendlier
"The organizer will no longer see you as attending" and rename
buttons from "Cancel attendance" to "Cancel RSVP".

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-12 22:32:19 +01:00

219 lines
7.6 KiB
TypeScript

import { http, HttpResponse } from 'msw'
import { test, expect } from './msw-setup'
import type { StoredEvent } from '../src/composables/useEventStorage'
const STORAGE_KEY = 'fete:events'
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,
cancelled: false,
}
const rsvpToken = 'd4e5f6a7-b8c9-0123-4567-890abcdef012'
const organizerToken = 'org-token-1234'
function seedEvents(events: StoredEvent[]): string {
return `window.localStorage.setItem('${STORAGE_KEY}', ${JSON.stringify(JSON.stringify(events))})`
}
function watchSeed(): StoredEvent {
return {
eventToken: fullEvent.eventToken,
title: fullEvent.title,
dateTime: fullEvent.dateTime,
}
}
function rsvpSeed(): StoredEvent {
return {
eventToken: fullEvent.eventToken,
title: fullEvent.title,
dateTime: fullEvent.dateTime,
rsvpToken,
rsvpName: 'Anna',
}
}
function organizerSeed(): StoredEvent {
return {
eventToken: fullEvent.eventToken,
title: fullEvent.title,
dateTime: fullEvent.dateTime,
organizerToken,
}
}
test.describe('US1: Watch event from detail page', () => {
test('bookmark unfilled by default, tapping watches the event', async ({ page, network }) => {
network.use(
http.get('*/api/events/:token', () => HttpResponse.json(fullEvent)),
)
await page.goto(`/events/${fullEvent.eventToken}`)
const bookmark = page.locator('.rsvp-bar__bookmark-inner')
await expect(bookmark).toBeVisible()
await expect(bookmark).toHaveAttribute('aria-label', 'Watch this event')
await bookmark.click()
await expect(bookmark).toHaveAttribute('aria-label', 'Stop watching this event')
// Navigate to event list via back link
await page.locator('.detail__back').click()
// Event appears with "Watching" label
await expect(page.getByText('Summer BBQ')).toBeVisible()
await expect(page.getByText('Watching')).toBeVisible()
})
})
test.describe('US2: Un-watch event from detail page', () => {
test('tapping filled bookmark un-watches the event', async ({ page, network }) => {
network.use(
http.get('*/api/events/:token', () => HttpResponse.json(fullEvent)),
)
await page.addInitScript(seedEvents([watchSeed()]))
await page.goto(`/events/${fullEvent.eventToken}`)
const bookmark = page.locator('.rsvp-bar__bookmark-inner')
await expect(bookmark).toHaveAttribute('aria-label', 'Stop watching this event')
await bookmark.click()
await expect(bookmark).toHaveAttribute('aria-label', 'Watch this event')
// Navigate to event list via back link (avoid page.goto re-running addInitScript)
await page.locator('.detail__back').click()
// Event is gone
await expect(page.getByText('Summer BBQ')).not.toBeVisible()
})
})
test.describe('US3: Bookmark reflects attending status', () => {
test('bookmark is not visible when user has RSVPed, list shows Attendee', async ({ page, network }) => {
network.use(
http.get('*/api/events/:token', () => HttpResponse.json(fullEvent)),
)
await page.addInitScript(seedEvents([rsvpSeed()]))
await page.goto(`/events/${fullEvent.eventToken}`)
// Bookmark not shown for attendees — RsvpBar shows status state
const bookmark = page.locator('.rsvp-bar__bookmark-inner')
await expect(bookmark).not.toBeVisible()
// Navigate to list via back link
await page.locator('.detail__back').click()
await expect(page.getByText('Attendee')).toBeVisible()
await expect(page.getByText('Watching')).not.toBeVisible()
})
})
test.describe('US4: RSVP cancellation preserves watch status', () => {
test('cancel RSVP → bookmark reappears, list shows Watching', async ({ page, network }) => {
network.use(
http.get('*/api/events/:token', () => HttpResponse.json(fullEvent)),
http.delete('*/api/events/:token/rsvps/:rsvpToken', () => {
return new HttpResponse(null, { status: 204 })
}),
)
await page.addInitScript(seedEvents([rsvpSeed()]))
await page.goto(`/events/${fullEvent.eventToken}`)
// Cancel RSVP
await page.getByRole('button', { name: /You're attending/ }).click()
await page.locator('.rsvp-bar__cancel').click()
await page.getByRole('alertdialog').getByRole('button', { name: 'Cancel RSVP' }).click()
// Bookmark reappears in CTA state, filled because event is still stored
const bookmark = page.locator('.rsvp-bar__bookmark-inner')
await expect(bookmark).toBeVisible()
await expect(bookmark).toHaveAttribute('aria-label', 'Stop watching this event')
// Navigate to list via back link
await page.locator('.detail__back').click()
await expect(page.getByText('Watching')).toBeVisible()
await expect(page.getByText('Attendee')).not.toBeVisible()
})
})
test.describe('US5: No bookmark for attendees and organizers', () => {
test('attendee does not see bookmark', async ({ page, network }) => {
network.use(
http.get('*/api/events/:token', () => HttpResponse.json(fullEvent)),
)
await page.addInitScript(seedEvents([rsvpSeed()]))
await page.goto(`/events/${fullEvent.eventToken}`)
const bookmark = page.locator('.rsvp-bar__bookmark-inner')
await expect(bookmark).not.toBeVisible()
})
test('organizer does not see bookmark', async ({ page, network }) => {
network.use(
http.get('*/api/events/:token', () => HttpResponse.json(fullEvent)),
)
await page.addInitScript(seedEvents([organizerSeed()]))
await page.goto(`/events/${fullEvent.eventToken}`)
const bookmark = page.locator('.rsvp-bar__bookmark-inner')
await expect(bookmark).not.toBeVisible()
})
})
test.describe('US6: Un-watch from event list', () => {
test('deleting a watched event skips confirmation dialog', async ({ page }) => {
await page.addInitScript(seedEvents([watchSeed()]))
await page.goto('/')
await expect(page.getByText('Summer BBQ')).toBeVisible()
await page.getByRole('button', { name: /Remove Summer BBQ/ }).click()
// No confirmation dialog — event removed immediately
await expect(page.getByText('Remove event?')).not.toBeVisible()
await expect(page.getByText('Summer BBQ')).not.toBeVisible()
})
})
test.describe('US7: Watcher upgrades to attendee', () => {
test('watch → RSVP → bookmark disappears, list shows Attendee', async ({ page, network }) => {
network.use(
http.get('*/api/events/:token', () => HttpResponse.json(fullEvent)),
http.post('*/api/events/:token/rsvps', () => {
return HttpResponse.json(
{ rsvpToken: 'new-rsvp-token', name: 'Max' },
{ status: 201 },
)
}),
)
await page.addInitScript(seedEvents([watchSeed()]))
await page.goto(`/events/${fullEvent.eventToken}`)
// Verify watching state — bookmark visible
const bookmark = page.locator('.rsvp-bar__bookmark-inner')
await expect(bookmark).toBeVisible()
await expect(bookmark).toHaveAttribute('aria-label', 'Stop watching this event')
// RSVP
await page.getByRole('button', { name: "I'm attending" }).click()
const dialog = page.getByRole('dialog', { name: 'RSVP' })
await dialog.getByLabel('Your name').fill('Max')
await dialog.getByRole('button', { name: 'Count me in' }).click()
// Bookmark gone — status bar shown instead
await expect(bookmark).not.toBeVisible()
// Navigate to list via back link
await page.locator('.detail__back').click()
await expect(page.getByText('Attendee')).toBeVisible()
await expect(page.getByText('Watching')).not.toBeVisible()
})
})