Add organizer cancel-event flow to EventList
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>
This commit is contained in:
@@ -1,8 +1,15 @@
|
||||
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'
|
||||
import { mount } from '@vue/test-utils'
|
||||
import { mount, flushPromises } from '@vue/test-utils'
|
||||
import { createRouter, createMemoryHistory } from 'vue-router'
|
||||
import EventList from '../EventList.vue'
|
||||
|
||||
vi.mock('../../api/client', () => ({
|
||||
api: {
|
||||
PATCH: vi.fn(),
|
||||
DELETE: vi.fn(),
|
||||
},
|
||||
}))
|
||||
|
||||
const router = createRouter({
|
||||
history: createMemoryHistory(),
|
||||
routes: [
|
||||
@@ -24,6 +31,8 @@ const mockEvents = [
|
||||
{ eventToken: 'rsvp-1', title: 'Attending Event', dateTime: '2026-03-11T20:00:00', rsvpToken: 'rsvp-token', rsvpName: 'Max' },
|
||||
]
|
||||
|
||||
const removeEventMock = vi.fn()
|
||||
|
||||
vi.mock('../../composables/useEventStorage', () => ({
|
||||
isValidStoredEvent: (e: unknown) => {
|
||||
if (typeof e !== 'object' || e === null) return false
|
||||
@@ -41,7 +50,14 @@ vi.mock('../../composables/useEventStorage', () => ({
|
||||
}
|
||||
return undefined
|
||||
},
|
||||
removeEvent: vi.fn(),
|
||||
getOrganizerToken: (token: string) => {
|
||||
const evt = mockEvents.find((e) => e.eventToken === token)
|
||||
if (evt && 'organizerToken' in evt) {
|
||||
return (evt as Record<string, unknown>).organizerToken as string
|
||||
}
|
||||
return undefined
|
||||
},
|
||||
removeEvent: removeEventMock,
|
||||
}),
|
||||
}))
|
||||
|
||||
@@ -60,7 +76,10 @@ vi.mock('../../composables/useRelativeTime', () => ({
|
||||
|
||||
function mountList() {
|
||||
return mount(EventList, {
|
||||
global: { plugins: [router] },
|
||||
global: {
|
||||
plugins: [router],
|
||||
stubs: { Teleport: true },
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
@@ -170,3 +189,118 @@ describe('EventList', () => {
|
||||
expect(badge.text()).toBe('Attending')
|
||||
})
|
||||
})
|
||||
|
||||
describe('EventList — Organizer Cancel (US1)', () => {
|
||||
beforeEach(() => {
|
||||
vi.useFakeTimers()
|
||||
vi.setSystemTime(NOW)
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
vi.useRealTimers()
|
||||
})
|
||||
|
||||
it('T005: confirmDelete calls PATCH cancel-event API when role is organizer', async () => {
|
||||
const { api } = await import('../../api/client')
|
||||
const patchMock = vi.mocked(api.PATCH)
|
||||
patchMock.mockResolvedValue({ response: { status: 204 } } as never)
|
||||
|
||||
const wrapper = mountList()
|
||||
|
||||
// Find the organizer event delete button and click it
|
||||
const orgCard = wrapper.findAll('.event-card').find((c) => c.text().includes('Organized Event'))
|
||||
expect(orgCard).toBeTruthy()
|
||||
await orgCard!.find('.event-card__delete').trigger('click')
|
||||
|
||||
// Confirm the dialog
|
||||
const confirmBtn = wrapper.find('.confirm-dialog__btn--confirm')
|
||||
await confirmBtn.trigger('click')
|
||||
await flushPromises()
|
||||
|
||||
expect(patchMock).toHaveBeenCalledWith(
|
||||
'/events/{eventToken}',
|
||||
expect.objectContaining({
|
||||
params: expect.objectContaining({
|
||||
path: { eventToken: 'org-1' },
|
||||
query: { organizerToken: 'org-token' },
|
||||
}),
|
||||
body: { cancelled: true },
|
||||
}),
|
||||
)
|
||||
})
|
||||
|
||||
it('T006: confirmDelete treats 409 response as success (removes event from list)', async () => {
|
||||
const { api } = await import('../../api/client')
|
||||
const patchMock = vi.mocked(api.PATCH)
|
||||
patchMock.mockResolvedValue({ response: { status: 409 } } as never)
|
||||
|
||||
removeEventMock.mockClear()
|
||||
|
||||
const wrapper = mountList()
|
||||
|
||||
const orgCard = wrapper.findAll('.event-card').find((c) => c.text().includes('Organized Event'))
|
||||
await orgCard!.find('.event-card__delete').trigger('click')
|
||||
|
||||
const confirmBtn = wrapper.find('.confirm-dialog__btn--confirm')
|
||||
await confirmBtn.trigger('click')
|
||||
await flushPromises()
|
||||
|
||||
// 409 should be treated as success — removeEvent should have been called
|
||||
expect(removeEventMock).toHaveBeenCalledWith('org-1')
|
||||
})
|
||||
|
||||
it('T006b: confirmDelete treats 404 response as success (removes event from list)', async () => {
|
||||
const { api } = await import('../../api/client')
|
||||
const patchMock = vi.mocked(api.PATCH)
|
||||
patchMock.mockResolvedValue({ response: { status: 404 } } as never)
|
||||
|
||||
removeEventMock.mockClear()
|
||||
|
||||
const wrapper = mountList()
|
||||
|
||||
const orgCard = wrapper.findAll('.event-card').find((c) => c.text().includes('Organized Event'))
|
||||
await orgCard!.find('.event-card__delete').trigger('click')
|
||||
|
||||
const confirmBtn = wrapper.find('.confirm-dialog__btn--confirm')
|
||||
await confirmBtn.trigger('click')
|
||||
await flushPromises()
|
||||
|
||||
expect(removeEventMock).toHaveBeenCalledWith('org-1')
|
||||
})
|
||||
})
|
||||
|
||||
describe('EventList — Dialog Differentiation (US2)', () => {
|
||||
beforeEach(() => {
|
||||
vi.useFakeTimers()
|
||||
vi.setSystemTime(NOW)
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
vi.useRealTimers()
|
||||
})
|
||||
|
||||
it('T013: deleteDialogTitle returns organizer-specific text when role is organizer', async () => {
|
||||
const wrapper = mountList()
|
||||
|
||||
// Click delete on organizer event
|
||||
const orgCard = wrapper.findAll('.event-card').find((c) => c.text().includes('Organized Event'))
|
||||
await orgCard!.find('.event-card__delete').trigger('click')
|
||||
|
||||
const dialogTitle = wrapper.find('.confirm-dialog__title')
|
||||
expect(dialogTitle.text()).toBe('Cancel event?')
|
||||
})
|
||||
|
||||
it('T014: deleteDialogMessage returns existing attendee text when role is attendee', async () => {
|
||||
const wrapper = mountList()
|
||||
|
||||
// Click delete on attendee event
|
||||
const attCard = wrapper.findAll('.event-card').find((c) => c.text().includes('Attending Event'))
|
||||
await attCard!.find('.event-card__delete').trigger('click')
|
||||
|
||||
const dialogTitle = wrapper.find('.confirm-dialog__title')
|
||||
expect(dialogTitle.text()).toBe('Remove event?')
|
||||
|
||||
const dialogMsg = wrapper.find('.confirm-dialog__message')
|
||||
expect(dialogMsg.text()).toContain('attendance will be cancelled')
|
||||
})
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user