From d0ed6790ef5e60f5641d131567387770fd707f1e Mon Sep 17 00:00:00 2001 From: nitrix Date: Fri, 13 Mar 2026 21:40:51 +0100 Subject: [PATCH] Update E2E tests for kebab menu and add iCal download tests Co-Authored-By: Claude Opus 4.6 --- frontend/e2e/cancel-event.spec.ts | 22 +++--- frontend/e2e/ical-download.spec.ts | 108 +++++++++++++++++++++++++++++ frontend/e2e/watch-event.spec.ts | 14 ++-- 3 files changed, 128 insertions(+), 16 deletions(-) create mode 100644 frontend/e2e/ical-download.spec.ts diff --git a/frontend/e2e/cancel-event.spec.ts b/frontend/e2e/cancel-event.spec.ts index 9ba73e3..f6a831a 100644 --- a/frontend/e2e/cancel-event.spec.ts +++ b/frontend/e2e/cancel-event.spec.ts @@ -64,12 +64,14 @@ test.describe('US1: Organizer cancels event with reason', () => { await page.addInitScript(seedEvents([organizerSeed()])) await page.goto(`/events/${fullEvent.eventToken}`) - // Cancel button visible for organizer - const cancelBtn = page.getByRole('button', { name: /Cancel event/i }) - await expect(cancelBtn).toBeVisible() + // Open kebab menu, then cancel event + const kebabBtn = page.getByRole('button', { name: /Event actions/i }) + await expect(kebabBtn).toBeVisible() + await kebabBtn.click() - // Open cancel bottom sheet - await cancelBtn.click() + const cancelItem = page.getByRole('menuitem', { name: /Cancel event/i }) + await expect(cancelItem).toBeVisible() + await cancelItem.click() // Fill in reason const reasonField = page.getByLabel(/reason/i) @@ -83,8 +85,8 @@ test.describe('US1: Organizer cancels event with reason', () => { await expect(page.getByText(/This event has been cancelled/i)).toBeVisible() await expect(page.getByText('Venue closed')).toBeVisible() - // Cancel button should be gone - await expect(cancelBtn).not.toBeVisible() + // Kebab menu should be gone (event is cancelled) + await expect(kebabBtn).not.toBeVisible() }) }) @@ -118,7 +120,8 @@ test.describe('US1: Organizer cancels event without reason', () => { await page.addInitScript(seedEvents([organizerSeed()])) await page.goto(`/events/${fullEvent.eventToken}`) - await page.getByRole('button', { name: /Cancel event/i }).click() + await page.getByRole('button', { name: /Event actions/i }).click() + await page.getByRole('menuitem', { name: /Cancel event/i }).click() // Don't fill in reason, just confirm await page.getByRole('button', { name: /Confirm cancellation/i }).click() @@ -150,7 +153,8 @@ test.describe('US1: Cancel API failure', () => { await page.addInitScript(seedEvents([organizerSeed()])) await page.goto(`/events/${fullEvent.eventToken}`) - await page.getByRole('button', { name: /Cancel event/i }).click() + await page.getByRole('button', { name: /Event actions/i }).click() + await page.getByRole('menuitem', { name: /Cancel event/i }).click() await page.getByRole('button', { name: /Confirm cancellation/i }).click() // Error message in bottom sheet diff --git a/frontend/e2e/ical-download.spec.ts b/frontend/e2e/ical-download.spec.ts new file mode 100644 index 0000000..3deccdd --- /dev/null +++ b/frontend/e2e/ical-download.spec.ts @@ -0,0 +1,108 @@ +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: 'Sommerfest am See', + description: 'Bring your own drinks!', + dateTime: '2026-07-15T18:00:00+02:00', + timezone: 'Europe/Berlin', + location: 'Stadtpark Berlin', + attendeeCount: 12, + cancelled: false, +} + +const cancelledEvent = { + ...fullEvent, + cancelled: true, + cancellationReason: 'Bad weather', +} + +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: 'd4e5f6a7-b8c9-0123-4567-890abcdef012', + rsvpName: 'Anna', + } +} + +function organizerSeed(): StoredEvent { + return { + eventToken: fullEvent.eventToken, + title: fullEvent.title, + dateTime: fullEvent.dateTime, + organizerToken: 'org-token-1234', + } +} + +test.describe('iCal download: calendar button visibility', () => { + test('calendar button visible for pre-RSVP visitor', async ({ page, network }) => { + network.use( + http.get('*/api/events/:token', () => HttpResponse.json(fullEvent)), + ) + await page.goto(`/events/${fullEvent.eventToken}`) + + const calendarBtn = page.getByRole('button', { name: /add to calendar/i }) + await expect(calendarBtn).toBeVisible() + }) + + test('calendar button visible for post-RSVP 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}`) + + const calendarBtn = page.getByRole('button', { name: /add to calendar/i }) + await expect(calendarBtn).toBeVisible() + }) + + test('calendar button visible for organizer', async ({ page, network }) => { + network.use( + http.get('*/api/events/:token', () => HttpResponse.json(fullEvent)), + http.get('*/api/events/:token/attendees*', () => + HttpResponse.json({ attendees: [] }), + ), + ) + await page.addInitScript(seedEvents([organizerSeed()])) + await page.goto(`/events/${fullEvent.eventToken}`) + + const calendarBtn = page.getByRole('button', { name: /add to calendar/i }) + await expect(calendarBtn).toBeVisible() + }) + + test('calendar button NOT visible for cancelled event', async ({ page, network }) => { + network.use( + http.get('*/api/events/:token', () => HttpResponse.json(cancelledEvent)), + ) + await page.goto(`/events/${fullEvent.eventToken}`) + + const calendarBtn = page.getByRole('button', { name: /add to calendar/i }) + await expect(calendarBtn).not.toBeVisible() + }) +}) + +test.describe('iCal download: file generation', () => { + test('triggers download with correct filename', async ({ page, network }) => { + network.use( + http.get('*/api/events/:token', () => HttpResponse.json(fullEvent)), + ) + await page.goto(`/events/${fullEvent.eventToken}`) + + // Intercept the download by overriding the click-link mechanism + const downloadPromise = page.waitForEvent('download') + await page.getByRole('button', { name: /add to calendar/i }).click() + const download = await downloadPromise + + expect(download.suggestedFilename()).toBe('sommerfest-am-see.ics') + }) +}) diff --git a/frontend/e2e/watch-event.spec.ts b/frontend/e2e/watch-event.spec.ts index a976e74..87d3e0c 100644 --- a/frontend/e2e/watch-event.spec.ts +++ b/frontend/e2e/watch-event.spec.ts @@ -56,7 +56,7 @@ test.describe('US1: Watch event from detail page', () => { ) await page.goto(`/events/${fullEvent.eventToken}`) - const bookmark = page.locator('.rsvp-bar__bookmark-inner') + const bookmark = page.getByLabel(/watch.*this event/i) await expect(bookmark).toBeVisible() await expect(bookmark).toHaveAttribute('aria-label', 'Watch this event') @@ -81,7 +81,7 @@ test.describe('US2: Un-watch event from detail page', () => { await page.addInitScript(seedEvents([watchSeed()])) await page.goto(`/events/${fullEvent.eventToken}`) - const bookmark = page.locator('.rsvp-bar__bookmark-inner') + const bookmark = page.getByLabel(/watch.*this event/i) await expect(bookmark).toHaveAttribute('aria-label', 'Stop watching this event') await bookmark.click() @@ -105,7 +105,7 @@ test.describe('US3: Bookmark reflects attending status', () => { await page.goto(`/events/${fullEvent.eventToken}`) // Bookmark not shown for attendees — RsvpBar shows status state - const bookmark = page.locator('.rsvp-bar__bookmark-inner') + const bookmark = page.getByLabel(/watch.*this event/i) await expect(bookmark).not.toBeVisible() // Navigate to list via back link @@ -132,7 +132,7 @@ test.describe('US4: RSVP cancellation preserves watch status', () => { 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') + const bookmark = page.getByLabel(/watch.*this event/i) await expect(bookmark).toBeVisible() await expect(bookmark).toHaveAttribute('aria-label', 'Stop watching this event') @@ -151,7 +151,7 @@ test.describe('US5: No bookmark for attendees and organizers', () => { await page.addInitScript(seedEvents([rsvpSeed()])) await page.goto(`/events/${fullEvent.eventToken}`) - const bookmark = page.locator('.rsvp-bar__bookmark-inner') + const bookmark = page.getByLabel(/watch.*this event/i) await expect(bookmark).not.toBeVisible() }) @@ -162,7 +162,7 @@ test.describe('US5: No bookmark for attendees and organizers', () => { await page.addInitScript(seedEvents([organizerSeed()])) await page.goto(`/events/${fullEvent.eventToken}`) - const bookmark = page.locator('.rsvp-bar__bookmark-inner') + const bookmark = page.getByLabel(/watch.*this event/i) await expect(bookmark).not.toBeVisible() }) }) @@ -197,7 +197,7 @@ test.describe('US7: Watcher upgrades to attendee', () => { await page.goto(`/events/${fullEvent.eventToken}`) // Verify watching state — bookmark visible - const bookmark = page.locator('.rsvp-bar__bookmark-inner') + const bookmark = page.getByLabel(/watch.*this event/i) await expect(bookmark).toBeVisible() await expect(bookmark).toHaveAttribute('aria-label', 'Stop watching this event')