Enable users to see all their saved events on the home screen, sorted by date with upcoming events first. Key capabilities: - EventCard with title, relative time display, and organizer/attendee role badge - Sortable EventList with past-event visual distinction (faded style) - Empty state when no events are stored - Swipe-to-delete gesture with confirmation dialog - Floating action button for quick event creation - Rename router param :token → :eventToken across all views - useRelativeTime composable (Intl.RelativeTimeFormat) - useEventStorage: add validation, removeEvent(), reactive versioning - Full E2E and unit test coverage for all new components Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
284 lines
7.5 KiB
TypeScript
284 lines
7.5 KiB
TypeScript
import { describe, it, expect, beforeEach } from 'vitest'
|
|
import { useEventStorage } from '../useEventStorage'
|
|
|
|
// jsdom provides a working localStorage in the window object
|
|
// but Node's --localstorage-file warning can be ignored
|
|
function clearStorage() {
|
|
try {
|
|
window.localStorage.setItem('fete:events', '[]')
|
|
} catch {
|
|
// Provide a minimal mock if localStorage is broken
|
|
const store: Record<string, string> = {}
|
|
Object.defineProperty(globalThis, 'localStorage', {
|
|
value: {
|
|
getItem: (key: string) => store[key] ?? null,
|
|
setItem: (key: string, val: string) => {
|
|
store[key] = val
|
|
},
|
|
removeItem: (key: string) => {
|
|
delete store[key]
|
|
},
|
|
},
|
|
writable: true,
|
|
configurable: true,
|
|
})
|
|
}
|
|
}
|
|
|
|
describe('useEventStorage', () => {
|
|
beforeEach(() => {
|
|
clearStorage()
|
|
})
|
|
|
|
it('returns empty array when no events stored', () => {
|
|
const { getStoredEvents } = useEventStorage()
|
|
expect(getStoredEvents()).toEqual([])
|
|
})
|
|
|
|
it('saves and retrieves a created event', () => {
|
|
const { saveCreatedEvent, getStoredEvents } = useEventStorage()
|
|
|
|
saveCreatedEvent({
|
|
eventToken: 'abc-123',
|
|
organizerToken: 'org-456',
|
|
title: 'Birthday',
|
|
dateTime: '2026-06-15T20:00:00+02:00',
|
|
expiryDate: '2026-07-15',
|
|
})
|
|
|
|
const events = getStoredEvents()
|
|
expect(events).toHaveLength(1)
|
|
expect(events[0]!.eventToken).toBe('abc-123')
|
|
expect(events[0]!.organizerToken).toBe('org-456')
|
|
expect(events[0]!.title).toBe('Birthday')
|
|
})
|
|
|
|
it('returns organizer token for known event', () => {
|
|
const { saveCreatedEvent, getOrganizerToken } = useEventStorage()
|
|
|
|
saveCreatedEvent({
|
|
eventToken: 'abc-123',
|
|
organizerToken: 'org-456',
|
|
title: 'Test',
|
|
dateTime: '2026-06-15T20:00:00+02:00',
|
|
expiryDate: '2026-07-15',
|
|
})
|
|
|
|
expect(getOrganizerToken('abc-123')).toBe('org-456')
|
|
})
|
|
|
|
it('returns undefined organizer token for unknown event', () => {
|
|
const { getOrganizerToken } = useEventStorage()
|
|
expect(getOrganizerToken('unknown')).toBeUndefined()
|
|
})
|
|
|
|
it('stores multiple events independently', () => {
|
|
const { saveCreatedEvent, getStoredEvents } = useEventStorage()
|
|
|
|
saveCreatedEvent({
|
|
eventToken: 'event-1',
|
|
title: 'First',
|
|
dateTime: '2026-06-15T20:00:00+02:00',
|
|
expiryDate: '2026-07-15',
|
|
})
|
|
|
|
saveCreatedEvent({
|
|
eventToken: 'event-2',
|
|
title: 'Second',
|
|
dateTime: '2026-07-15T20:00:00+02:00',
|
|
expiryDate: '2026-08-15',
|
|
})
|
|
|
|
const events = getStoredEvents()
|
|
expect(events).toHaveLength(2)
|
|
expect(events.map((e) => e.eventToken)).toContain('event-1')
|
|
expect(events.map((e) => e.eventToken)).toContain('event-2')
|
|
})
|
|
|
|
it('overwrites event with same token', () => {
|
|
const { saveCreatedEvent, getStoredEvents } = useEventStorage()
|
|
|
|
saveCreatedEvent({
|
|
eventToken: 'abc-123',
|
|
title: 'Old Title',
|
|
dateTime: '2026-06-15T20:00:00+02:00',
|
|
expiryDate: '2026-07-15',
|
|
})
|
|
|
|
saveCreatedEvent({
|
|
eventToken: 'abc-123',
|
|
title: 'New Title',
|
|
dateTime: '2026-06-15T20:00:00+02:00',
|
|
expiryDate: '2026-07-15',
|
|
})
|
|
|
|
const events = getStoredEvents()
|
|
expect(events).toHaveLength(1)
|
|
expect(events[0]!.title).toBe('New Title')
|
|
})
|
|
|
|
it('saves and retrieves RSVP for an existing event', () => {
|
|
const { saveCreatedEvent, saveRsvp, getRsvp } = useEventStorage()
|
|
|
|
saveCreatedEvent({
|
|
eventToken: 'abc-123',
|
|
title: 'Birthday',
|
|
dateTime: '2026-06-15T20:00:00+02:00',
|
|
expiryDate: '2026-07-15',
|
|
})
|
|
|
|
saveRsvp('abc-123', 'rsvp-token-1', 'Max', 'Birthday', '2026-06-15T20:00:00+02:00')
|
|
|
|
const rsvp = getRsvp('abc-123')
|
|
expect(rsvp).toEqual({ rsvpToken: 'rsvp-token-1', rsvpName: 'Max' })
|
|
})
|
|
|
|
it('saves RSVP for a new event (not previously stored)', () => {
|
|
const { saveRsvp, getRsvp, getStoredEvents } = useEventStorage()
|
|
|
|
saveRsvp('new-event', 'rsvp-token-2', 'Anna', 'Party', '2026-08-01T18:00:00+02:00')
|
|
|
|
const rsvp = getRsvp('new-event')
|
|
expect(rsvp).toEqual({ rsvpToken: 'rsvp-token-2', rsvpName: 'Anna' })
|
|
|
|
const events = getStoredEvents()
|
|
expect(events).toHaveLength(1)
|
|
expect(events[0]!.eventToken).toBe('new-event')
|
|
expect(events[0]!.title).toBe('Party')
|
|
})
|
|
|
|
it('returns undefined RSVP for event without RSVP', () => {
|
|
const { saveCreatedEvent, getRsvp } = useEventStorage()
|
|
|
|
saveCreatedEvent({
|
|
eventToken: 'abc-123',
|
|
title: 'Test',
|
|
dateTime: '2026-06-15T20:00:00+02:00',
|
|
expiryDate: '2026-07-15',
|
|
})
|
|
|
|
expect(getRsvp('abc-123')).toBeUndefined()
|
|
})
|
|
|
|
it('returns undefined RSVP for unknown event', () => {
|
|
const { getRsvp } = useEventStorage()
|
|
expect(getRsvp('unknown')).toBeUndefined()
|
|
})
|
|
|
|
it('removes an event by token', () => {
|
|
const { saveCreatedEvent, getStoredEvents, removeEvent } = useEventStorage()
|
|
|
|
saveCreatedEvent({
|
|
eventToken: 'event-1',
|
|
title: 'First',
|
|
dateTime: '2026-06-15T20:00:00+02:00',
|
|
expiryDate: '2026-07-15',
|
|
})
|
|
|
|
saveCreatedEvent({
|
|
eventToken: 'event-2',
|
|
title: 'Second',
|
|
dateTime: '2026-07-15T20:00:00+02:00',
|
|
expiryDate: '2026-08-15',
|
|
})
|
|
|
|
removeEvent('event-1')
|
|
|
|
const events = getStoredEvents()
|
|
expect(events).toHaveLength(1)
|
|
expect(events[0]!.eventToken).toBe('event-2')
|
|
})
|
|
|
|
it('removeEvent does nothing for unknown token', () => {
|
|
const { saveCreatedEvent, getStoredEvents, removeEvent } = useEventStorage()
|
|
|
|
saveCreatedEvent({
|
|
eventToken: 'event-1',
|
|
title: 'First',
|
|
dateTime: '2026-06-15T20:00:00+02:00',
|
|
expiryDate: '2026-07-15',
|
|
})
|
|
|
|
removeEvent('nonexistent')
|
|
|
|
expect(getStoredEvents()).toHaveLength(1)
|
|
})
|
|
})
|
|
|
|
describe('isValidStoredEvent', () => {
|
|
// Import directly since it's an exported function
|
|
let isValidStoredEvent: (e: unknown) => boolean
|
|
|
|
beforeEach(async () => {
|
|
const mod = await import('../useEventStorage')
|
|
isValidStoredEvent = mod.isValidStoredEvent
|
|
})
|
|
|
|
it('returns true for a valid event', () => {
|
|
expect(
|
|
isValidStoredEvent({
|
|
eventToken: 'abc-123',
|
|
title: 'Birthday',
|
|
dateTime: '2026-06-15T20:00:00+02:00',
|
|
expiryDate: '2026-07-15',
|
|
}),
|
|
).toBe(true)
|
|
})
|
|
|
|
it('returns false for null', () => {
|
|
expect(isValidStoredEvent(null)).toBe(false)
|
|
})
|
|
|
|
it('returns false for non-object', () => {
|
|
expect(isValidStoredEvent('string')).toBe(false)
|
|
})
|
|
|
|
it('returns false when eventToken is missing', () => {
|
|
expect(
|
|
isValidStoredEvent({
|
|
title: 'Birthday',
|
|
dateTime: '2026-06-15T20:00:00+02:00',
|
|
}),
|
|
).toBe(false)
|
|
})
|
|
|
|
it('returns false when eventToken is empty', () => {
|
|
expect(
|
|
isValidStoredEvent({
|
|
eventToken: '',
|
|
title: 'Birthday',
|
|
dateTime: '2026-06-15T20:00:00+02:00',
|
|
}),
|
|
).toBe(false)
|
|
})
|
|
|
|
it('returns false when title is missing', () => {
|
|
expect(
|
|
isValidStoredEvent({
|
|
eventToken: 'abc-123',
|
|
dateTime: '2026-06-15T20:00:00+02:00',
|
|
}),
|
|
).toBe(false)
|
|
})
|
|
|
|
it('returns false when dateTime is invalid', () => {
|
|
expect(
|
|
isValidStoredEvent({
|
|
eventToken: 'abc-123',
|
|
title: 'Birthday',
|
|
dateTime: 'not-a-date',
|
|
}),
|
|
).toBe(false)
|
|
})
|
|
|
|
it('returns false when dateTime is empty', () => {
|
|
expect(
|
|
isValidStoredEvent({
|
|
eventToken: 'abc-123',
|
|
title: 'Birthday',
|
|
dateTime: '',
|
|
}),
|
|
).toBe(false)
|
|
})
|
|
})
|