Add iCal download for calendar integration #43

Merged
nitrix merged 7 commits from 019-ical-download into master 2026-03-14 11:40:43 +01:00
3 changed files with 128 additions and 16 deletions
Showing only changes of commit d0ed6790ef - Show all commits

View File

@@ -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

View 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')
})
})

View File

@@ -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')