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, } const rsvpToken = 'd4e5f6a7-b8c9-0123-4567-890abcdef012' function seedEvents(events: StoredEvent[]): string { return `window.localStorage.setItem('${STORAGE_KEY}', ${JSON.stringify(JSON.stringify(events))})` } function rsvpSeed(): StoredEvent { return { eventToken: fullEvent.eventToken, title: fullEvent.title, dateTime: fullEvent.dateTime, rsvpToken, rsvpName: 'Anna', } } test.describe('US1: Cancel RSVP from Event Detail View', () => { test('status bar shows cancel affordance when RSVP\'d', async ({ page, network }) => { network.use( http.get('*/api/events/:token', () => HttpResponse.json(fullEvent)), ) await page.addInitScript(seedEvents([rsvpSeed()])) await page.goto(`/events/${fullEvent.eventToken}`) // Status bar visible const statusBar = page.getByRole('button', { name: /You're attending/ }) await expect(statusBar).toBeVisible() // Cancel button hidden initially await expect(page.getByRole('button', { name: 'Cancel RSVP' })).not.toBeVisible() }) test('tapping status bar reveals cancel button', async ({ page, network }) => { network.use( http.get('*/api/events/:token', () => HttpResponse.json(fullEvent)), ) await page.addInitScript(seedEvents([rsvpSeed()])) await page.goto(`/events/${fullEvent.eventToken}`) // Tap status bar await page.getByRole('button', { name: /You're attending/ }).click() // Cancel button appears await expect(page.getByRole('button', { name: 'Cancel RSVP' })).toBeVisible() }) test('confirm cancellation → localStorage cleared, count decremented, bar reset', 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}`) // Expand → Cancel RSVP → Confirm in dialog await page.getByRole('button', { name: /You're attending/ }).click() await page.locator('.rsvp-bar__cancel').click() // Confirm dialog await expect(page.getByText('The organizer will no longer see you as attending.')).toBeVisible() await page.getByRole('alertdialog').getByRole('button', { name: 'Cancel RSVP' }).click() // Bar resets to CTA state await expect(page.getByRole('button', { name: "I'm attending" })).toBeVisible() await expect(page.getByText("You're attending!")).not.toBeVisible() // Attendee count decremented await expect(page.getByText('11 going')).toBeVisible() // localStorage cleared const stored = await page.evaluate(() => { const raw = localStorage.getItem('fete:events') return raw ? JSON.parse(raw) : null }) const event = stored?.find((e: StoredEvent) => e.eventToken === fullEvent.eventToken) expect(event?.rsvpToken).toBeUndefined() expect(event?.rsvpName).toBeUndefined() }) test('server error → error message, state unchanged', async ({ page, network }) => { network.use( http.get('*/api/events/:token', () => HttpResponse.json(fullEvent)), http.delete('*/api/events/:token/rsvps/:rsvpToken', () => { return HttpResponse.json({ error: 'fail' }, { status: 500 }) }), ) await page.addInitScript(seedEvents([rsvpSeed()])) await page.goto(`/events/${fullEvent.eventToken}`) // Expand → Cancel → Confirm in dialog 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() // Error message await expect(page.getByText('Could not cancel RSVP. Please try again.')).toBeVisible() // Attendee count unchanged await expect(page.getByText('12 going')).toBeVisible() }) test('re-RSVP after cancel works', 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 }) }), http.post('*/api/events/:token/rsvps', () => { return HttpResponse.json( { rsvpToken: 'new-rsvp-token', name: 'Max' }, { status: 201 }, ) }), ) await page.addInitScript(seedEvents([rsvpSeed()])) await page.goto(`/events/${fullEvent.eventToken}`) // Cancel first 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() // CTA should be back await expect(page.getByRole('button', { name: "I'm attending" })).toBeVisible() // Re-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() // Status bar returns await expect(page.getByText("You're attending!")).toBeVisible() }) }) test.describe('US2: Auto-Cancel on Event List Removal', () => { test('removal of RSVP\'d event shows attendance warning in dialog', async ({ page }) => { await page.addInitScript(seedEvents([rsvpSeed()])) await page.goto('/') await page.getByRole('button', { name: /Remove Summer BBQ/ }).click() await expect(page.getByText('your attendance will be cancelled')).toBeVisible() }) test('removal of non-RSVP\'d event shows standard dialog', async ({ page }) => { const noRsvp: StoredEvent = { eventToken: 'no-rsvp-token', title: 'No RSVP Event', dateTime: '2027-06-15T18:00:00Z', organizerToken: 'org-123', } await page.addInitScript(seedEvents([noRsvp])) await page.goto('/') await page.getByRole('button', { name: /Remove No RSVP Event/ }).click() await expect(page.getByText('This event will be removed from your list.')).toBeVisible() await expect(page.getByText('attendance will be cancelled')).not.toBeVisible() }) test('confirm removal → DELETE called → event removed from list', async ({ page, network }) => { network.use( http.delete('*/api/events/:token/rsvps/:rsvpToken', () => { return new HttpResponse(null, { status: 204 }) }), ) await page.addInitScript(seedEvents([rsvpSeed()])) await page.goto('/') await page.getByRole('button', { name: /Remove Summer BBQ/ }).click() await page.getByRole('button', { name: 'Remove', exact: true }).click() // Event gone await expect(page.getByText('Summer BBQ')).not.toBeVisible() // localStorage updated const stored = await page.evaluate(() => { const raw = localStorage.getItem('fete:events') return raw ? JSON.parse(raw) : null }) const found = stored?.find((e: StoredEvent) => e.eventToken === fullEvent.eventToken) expect(found).toBeUndefined() }) test('server error on DELETE → error message, event stays in list', async ({ page, network }) => { network.use( http.delete('*/api/events/:token/rsvps/:rsvpToken', () => { return HttpResponse.json({ error: 'fail' }, { status: 500 }) }), ) await page.addInitScript(seedEvents([rsvpSeed()])) await page.goto('/') await page.getByRole('button', { name: /Remove Summer BBQ/ }).click() await page.getByRole('button', { name: 'Remove', exact: true }).click() // Event still in list await expect(page.getByText('Summer BBQ')).toBeVisible() }) test('dismiss dialog → no changes', async ({ page }) => { await page.addInitScript(seedEvents([rsvpSeed()])) await page.goto('/') await page.getByRole('button', { name: /Remove Summer BBQ/ }).click() await page.getByRole('button', { name: 'Cancel' }).click() // Event still there await expect(page.getByText('Summer BBQ')).toBeVisible() }) }) test.describe('US3: Cancel RSVP with Stale/Invalid Token', () => { test('cancel from detail view with stale token (404) → treated as success', async ({ page, network }) => { network.use( http.get('*/api/events/:token', () => HttpResponse.json(fullEvent)), http.delete('*/api/events/:token/rsvps/:rsvpToken', () => { return HttpResponse.json({ error: 'not found' }, { status: 404 }) }), ) await page.addInitScript(seedEvents([rsvpSeed()])) await page.goto(`/events/${fullEvent.eventToken}`) // Cancel flow 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() // Treated as success — CTA returns await expect(page.getByRole('button', { name: "I'm attending" })).toBeVisible() // localStorage cleaned const stored = await page.evaluate(() => { const raw = localStorage.getItem('fete:events') return raw ? JSON.parse(raw) : null }) const event = stored?.find((e: StoredEvent) => e.eventToken === fullEvent.eventToken) expect(event?.rsvpToken).toBeUndefined() }) test('event list removal with stale token (404) → treated as success', async ({ page, network }) => { network.use( http.delete('*/api/events/:token/rsvps/:rsvpToken', () => { return HttpResponse.json({ error: 'not found' }, { status: 404 }) }), ) await page.addInitScript(seedEvents([rsvpSeed()])) await page.goto('/') await page.getByRole('button', { name: /Remove Summer BBQ/ }).click() await page.getByRole('button', { name: 'Remove', exact: true }).click() // Event removed from list await expect(page.getByText('Summer BBQ')).not.toBeVisible() }) })