Add iCal download for calendar integration #43
@@ -64,12 +64,14 @@ test.describe('US1: Organizer cancels event with reason', () => {
|
|||||||
await page.addInitScript(seedEvents([organizerSeed()]))
|
await page.addInitScript(seedEvents([organizerSeed()]))
|
||||||
await page.goto(`/events/${fullEvent.eventToken}`)
|
await page.goto(`/events/${fullEvent.eventToken}`)
|
||||||
|
|
||||||
// Cancel button visible for organizer
|
// Open kebab menu, then cancel event
|
||||||
const cancelBtn = page.getByRole('button', { name: /Cancel event/i })
|
const kebabBtn = page.getByRole('button', { name: /Event actions/i })
|
||||||
await expect(cancelBtn).toBeVisible()
|
await expect(kebabBtn).toBeVisible()
|
||||||
|
await kebabBtn.click()
|
||||||
|
|
||||||
// Open cancel bottom sheet
|
const cancelItem = page.getByRole('menuitem', { name: /Cancel event/i })
|
||||||
await cancelBtn.click()
|
await expect(cancelItem).toBeVisible()
|
||||||
|
await cancelItem.click()
|
||||||
|
|
||||||
// Fill in reason
|
// Fill in reason
|
||||||
const reasonField = page.getByLabel(/reason/i)
|
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(/This event has been cancelled/i)).toBeVisible()
|
||||||
await expect(page.getByText('Venue closed')).toBeVisible()
|
await expect(page.getByText('Venue closed')).toBeVisible()
|
||||||
|
|
||||||
// Cancel button should be gone
|
// Kebab menu should be gone (event is cancelled)
|
||||||
await expect(cancelBtn).not.toBeVisible()
|
await expect(kebabBtn).not.toBeVisible()
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -118,7 +120,8 @@ test.describe('US1: Organizer cancels event without reason', () => {
|
|||||||
await page.addInitScript(seedEvents([organizerSeed()]))
|
await page.addInitScript(seedEvents([organizerSeed()]))
|
||||||
await page.goto(`/events/${fullEvent.eventToken}`)
|
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
|
// Don't fill in reason, just confirm
|
||||||
await page.getByRole('button', { name: /Confirm cancellation/i }).click()
|
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.addInitScript(seedEvents([organizerSeed()]))
|
||||||
await page.goto(`/events/${fullEvent.eventToken}`)
|
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()
|
await page.getByRole('button', { name: /Confirm cancellation/i }).click()
|
||||||
|
|
||||||
// Error message in bottom sheet
|
// Error message in bottom sheet
|
||||||
|
|||||||
108
frontend/e2e/ical-download.spec.ts
Normal file
108
frontend/e2e/ical-download.spec.ts
Normal file
@@ -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')
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -56,7 +56,7 @@ test.describe('US1: Watch event from detail page', () => {
|
|||||||
)
|
)
|
||||||
await page.goto(`/events/${fullEvent.eventToken}`)
|
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).toBeVisible()
|
||||||
await expect(bookmark).toHaveAttribute('aria-label', 'Watch this event')
|
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.addInitScript(seedEvents([watchSeed()]))
|
||||||
await page.goto(`/events/${fullEvent.eventToken}`)
|
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 expect(bookmark).toHaveAttribute('aria-label', 'Stop watching this event')
|
||||||
|
|
||||||
await bookmark.click()
|
await bookmark.click()
|
||||||
@@ -105,7 +105,7 @@ test.describe('US3: Bookmark reflects attending status', () => {
|
|||||||
await page.goto(`/events/${fullEvent.eventToken}`)
|
await page.goto(`/events/${fullEvent.eventToken}`)
|
||||||
|
|
||||||
// Bookmark not shown for attendees — RsvpBar shows status state
|
// 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()
|
await expect(bookmark).not.toBeVisible()
|
||||||
|
|
||||||
// Navigate to list via back link
|
// 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()
|
await page.getByRole('alertdialog').getByRole('button', { name: 'Cancel RSVP' }).click()
|
||||||
|
|
||||||
// Bookmark reappears in CTA state, filled because event is still stored
|
// 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).toBeVisible()
|
||||||
await expect(bookmark).toHaveAttribute('aria-label', 'Stop watching this event')
|
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.addInitScript(seedEvents([rsvpSeed()]))
|
||||||
await page.goto(`/events/${fullEvent.eventToken}`)
|
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()
|
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.addInitScript(seedEvents([organizerSeed()]))
|
||||||
await page.goto(`/events/${fullEvent.eventToken}`)
|
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()
|
await expect(bookmark).not.toBeVisible()
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
@@ -197,7 +197,7 @@ test.describe('US7: Watcher upgrades to attendee', () => {
|
|||||||
await page.goto(`/events/${fullEvent.eventToken}`)
|
await page.goto(`/events/${fullEvent.eventToken}`)
|
||||||
|
|
||||||
// Verify watching state — bookmark visible
|
// 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).toBeVisible()
|
||||||
await expect(bookmark).toHaveAttribute('aria-label', 'Stop watching this event')
|
await expect(bookmark).toHaveAttribute('aria-label', 'Stop watching this event')
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user