Organizers can now cancel events directly from the event list via the
existing PATCH /events/{eventToken} API. The confirmation dialog shows
role-differentiated messaging: "Cancel event?" with a severity warning
for organizers vs. "Remove event?" for attendees. Responses 204, 409,
and 404 all result in successful removal from the local list.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
211 lines
7.0 KiB
TypeScript
211 lines
7.0 KiB
TypeScript
import { test, expect } from './msw-setup'
|
|
import type { StoredEvent } from '../src/composables/useEventStorage'
|
|
|
|
const STORAGE_KEY = 'fete:events'
|
|
|
|
const organizerEvent: StoredEvent = {
|
|
eventToken: 'org-event-aaa',
|
|
title: 'Summer BBQ',
|
|
dateTime: '2027-06-15T18:00:00Z',
|
|
organizerToken: 'org-secret-token',
|
|
}
|
|
|
|
const attendeeEvent: StoredEvent = {
|
|
eventToken: 'att-event-bbb',
|
|
title: 'Team Meeting',
|
|
dateTime: '2027-01-10T09:00:00Z',
|
|
rsvpToken: 'rsvp-token-1',
|
|
rsvpName: 'Alice',
|
|
}
|
|
|
|
function seedEvents(events: StoredEvent[]): string {
|
|
return `window.localStorage.setItem('${STORAGE_KEY}', ${JSON.stringify(JSON.stringify(events))})`
|
|
}
|
|
|
|
test.describe('US1: Organizer Cancels Event from List', () => {
|
|
test('T001: organizer taps delete, confirms, event is removed after successful API call', async ({
|
|
page,
|
|
network,
|
|
}) => {
|
|
await page.addInitScript(seedEvents([organizerEvent, attendeeEvent]))
|
|
|
|
const { http, HttpResponse } = await import('msw')
|
|
let patchCalled = false
|
|
network.use(
|
|
http.patch('*/api/events/:token', ({ request, params }) => {
|
|
const url = new URL(request.url)
|
|
if (
|
|
params['token'] === organizerEvent.eventToken &&
|
|
url.searchParams.get('organizerToken') === organizerEvent.organizerToken
|
|
) {
|
|
patchCalled = true
|
|
return new HttpResponse(null, { status: 204 })
|
|
}
|
|
return HttpResponse.json(
|
|
{ type: 'about:blank', title: 'Forbidden', status: 403 },
|
|
{ status: 403, headers: { 'Content-Type': 'application/problem+json' } },
|
|
)
|
|
}),
|
|
)
|
|
|
|
await page.goto('/')
|
|
await expect(page.getByText('Summer BBQ')).toBeVisible()
|
|
|
|
// Click delete on organizer event
|
|
await page.getByRole('button', { name: /Remove Summer BBQ/ }).click()
|
|
|
|
// Confirmation dialog appears with organizer-specific text
|
|
await expect(page.getByRole('alertdialog')).toBeVisible()
|
|
|
|
// Confirm cancellation
|
|
await page.getByRole('button', { name: 'Remove', exact: true }).click()
|
|
|
|
// Event is removed from list
|
|
await expect(page.getByText('Summer BBQ')).not.toBeVisible()
|
|
// Other event remains
|
|
await expect(page.getByText('Team Meeting')).toBeVisible()
|
|
|
|
expect(patchCalled).toBe(true)
|
|
})
|
|
|
|
test('T002: organizer confirms cancellation, API fails, event stays in list and error shown', async ({
|
|
page,
|
|
network,
|
|
}) => {
|
|
await page.addInitScript(seedEvents([organizerEvent]))
|
|
|
|
const { http, HttpResponse } = await import('msw')
|
|
network.use(
|
|
http.patch('*/api/events/:token', () => {
|
|
return HttpResponse.json(
|
|
{
|
|
type: 'about:blank',
|
|
title: 'Internal Server Error',
|
|
status: 500,
|
|
},
|
|
{ status: 500, headers: { 'Content-Type': 'application/problem+json' } },
|
|
)
|
|
}),
|
|
)
|
|
|
|
await page.goto('/')
|
|
await page.getByRole('button', { name: /Remove Summer BBQ/ }).click()
|
|
await expect(page.getByRole('alertdialog')).toBeVisible()
|
|
|
|
await page.getByRole('button', { name: 'Remove', exact: true }).click()
|
|
|
|
// Event stays in list
|
|
await expect(page.getByText('Summer BBQ')).toBeVisible()
|
|
})
|
|
|
|
test('T003: organizer confirms cancellation, API returns 409 Conflict, event is silently removed', async ({
|
|
page,
|
|
network,
|
|
}) => {
|
|
await page.addInitScript(seedEvents([organizerEvent]))
|
|
|
|
const { http, HttpResponse } = await import('msw')
|
|
network.use(
|
|
http.patch('*/api/events/:token', () => {
|
|
return HttpResponse.json(
|
|
{
|
|
type: 'about:blank',
|
|
title: 'Conflict',
|
|
status: 409,
|
|
detail: 'Event is already cancelled.',
|
|
},
|
|
{ status: 409, headers: { 'Content-Type': 'application/problem+json' } },
|
|
)
|
|
}),
|
|
)
|
|
|
|
await page.goto('/')
|
|
await page.getByRole('button', { name: /Remove Summer BBQ/ }).click()
|
|
await expect(page.getByRole('alertdialog')).toBeVisible()
|
|
|
|
await page.getByRole('button', { name: 'Remove', exact: true }).click()
|
|
|
|
// 409 treated as success — event removed
|
|
await expect(page.getByText('Summer BBQ')).not.toBeVisible()
|
|
})
|
|
|
|
test('T004: organizer opens cancel dialog then dismisses (cancel button), event remains', async ({
|
|
page,
|
|
}) => {
|
|
await page.addInitScript(seedEvents([organizerEvent]))
|
|
await page.goto('/')
|
|
|
|
await page.getByRole('button', { name: /Remove Summer BBQ/ }).click()
|
|
await expect(page.getByRole('alertdialog')).toBeVisible()
|
|
|
|
// Dismiss via Cancel button
|
|
await page.getByRole('button', { name: 'Cancel' }).click()
|
|
|
|
await expect(page.getByRole('alertdialog')).not.toBeVisible()
|
|
await expect(page.getByText('Summer BBQ')).toBeVisible()
|
|
})
|
|
|
|
test('T004b: organizer opens cancel dialog then dismisses via Escape', async ({ page }) => {
|
|
await page.addInitScript(seedEvents([organizerEvent]))
|
|
await page.goto('/')
|
|
|
|
await page.getByRole('button', { name: /Remove Summer BBQ/ }).click()
|
|
await expect(page.getByRole('alertdialog')).toBeVisible()
|
|
|
|
await page.keyboard.press('Escape')
|
|
|
|
await expect(page.getByRole('alertdialog')).not.toBeVisible()
|
|
await expect(page.getByText('Summer BBQ')).toBeVisible()
|
|
})
|
|
|
|
test('T004c: organizer opens cancel dialog then dismisses via overlay click', async ({
|
|
page,
|
|
}) => {
|
|
await page.addInitScript(seedEvents([organizerEvent]))
|
|
await page.goto('/')
|
|
|
|
await page.getByRole('button', { name: /Remove Summer BBQ/ }).click()
|
|
await expect(page.getByRole('alertdialog')).toBeVisible()
|
|
|
|
// Click on overlay (outside dialog)
|
|
await page.locator('.confirm-dialog__overlay').click({ position: { x: 10, y: 10 } })
|
|
|
|
await expect(page.getByRole('alertdialog')).not.toBeVisible()
|
|
await expect(page.getByText('Summer BBQ')).toBeVisible()
|
|
})
|
|
})
|
|
|
|
test.describe('US2: Distinct Dialog for Organizer vs. Attendee', () => {
|
|
test('T011: organizer dialog shows event-cancellation warning', async ({ page }) => {
|
|
await page.addInitScript(seedEvents([organizerEvent]))
|
|
await page.goto('/')
|
|
|
|
await page.getByRole('button', { name: /Remove Summer BBQ/ }).click()
|
|
const dialog = page.getByRole('alertdialog')
|
|
await expect(dialog).toBeVisible()
|
|
|
|
// Organizer-specific title and message
|
|
await expect(dialog.locator('.confirm-dialog__title')).toHaveText('Cancel event?')
|
|
await expect(dialog.locator('.confirm-dialog__message')).toContainText(
|
|
'all attendees',
|
|
)
|
|
})
|
|
|
|
test('T012: attendee dialog preserves existing RSVP-cancellation message', async ({
|
|
page,
|
|
}) => {
|
|
await page.addInitScript(seedEvents([attendeeEvent]))
|
|
await page.goto('/')
|
|
|
|
await page.getByRole('button', { name: /Remove Team Meeting/ }).click()
|
|
const dialog = page.getByRole('alertdialog')
|
|
await expect(dialog).toBeVisible()
|
|
|
|
// Attendee-specific title and message
|
|
await expect(dialog.locator('.confirm-dialog__title')).toHaveText('Remove event?')
|
|
await expect(dialog.locator('.confirm-dialog__message')).toContainText(
|
|
'attendance will be cancelled',
|
|
)
|
|
})
|
|
})
|