/** * EventDetailPopover tests — Task 1 (TDD RED → GREEN) * * Guards: * - T-02e-01: Title containing HTML-looking strings renders as escaped text (XSS guard) * - Escape key closes the popover and clears openEventId * - All event fields (title, location, description, calendar name) render as text * - Close button has aria-label="Close" * - Backdrop click closes the popover */ import 'temporal-polyfill/global' import React from 'react' import { describe, it, expect, vi, beforeEach } from 'vitest' import { render, screen, fireEvent } from '@testing-library/react' import { QueryClient, QueryClientProvider } from '@tanstack/react-query' // window.matchMedia polyfilled in test-setup.ts // ── Module mocks ────────────────────────────────────────────────────────────── const { mockSetOpenEventId, mockSetEventForm, mockSetDeleteDialog, } = vi.hoisted(() => ({ mockSetOpenEventId: vi.fn(), mockSetEventForm: vi.fn(), mockSetDeleteDialog: vi.fn(), })) let mockOpenEventId: string | null = null vi.mock('../store/calendarStore.js', () => ({ useCalendarStore: vi.fn((selector?: (s: Record) => unknown) => { const state = { openEventId: mockOpenEventId, setOpenEventId: mockSetOpenEventId, setEventForm: mockSetEventForm, setDeleteDialog: mockSetDeleteDialog, } if (typeof selector === 'function') return selector(state) return state }), })) // ── Fixtures ─────────────────────────────────────────────────────────────────── import type { CalendarOccurrence } from '../api/client.js' const TIMED_OCCURRENCE: CalendarOccurrence = { id: 'test-uid::2026-06-15T10:00:00', uid: 'test-uid', calendarId: 1, calendarName: 'My Calendar', ownerUserId: 1, ownerName: 'Alice', color: '#4A90D9', isShared: false, title: 'Team Standup', start: '2026-06-15T10:00:00-04:00[America/New_York]', end: '2026-06-15T10:30:00-04:00[America/New_York]', allDay: false, location: 'Conference Room B', description: 'Daily team sync meeting', hasRrule: false, } const OCCURRENCE_WITH_HTML: CalendarOccurrence = { ...TIMED_OCCURRENCE, id: 'xss-uid::2026-06-15T10:00:00', uid: 'xss-uid', title: 'Team Meeting', description: 'Bold description', location: 'Room', } const ALLDAY_OCCURRENCE: CalendarOccurrence = { id: 'allday-uid::2026-06-20', uid: 'allday-uid', calendarId: 2, calendarName: 'Shared Calendar', ownerUserId: 1, ownerName: 'Alice', color: '#F25C7A', isShared: true, title: 'Birthday Party', start: '2026-06-20', end: '2026-06-20', allDay: true, location: null, description: null, hasRrule: false, } // ── Import component (after mocks are declared) ─────────────────────────────── import { EventDetailPopover } from './EventDetailPopover.js' import { useCalendarStore } from '../store/calendarStore.js' // ── Helpers ─────────────────────────────────────────────────────────────────── function renderPopover(occurrence = TIMED_OCCURRENCE) { mockOpenEventId = occurrence.id ;(useCalendarStore as unknown as ReturnType).mockImplementation( (selector?: (s: Record) => unknown) => { const state = { openEventId: mockOpenEventId, setOpenEventId: mockSetOpenEventId, setEventForm: mockSetEventForm, setDeleteDialog: mockSetDeleteDialog, } if (typeof selector === 'function') return selector(state) return state }, ) const client = new QueryClient({ defaultOptions: { queries: { retry: false, staleTime: Infinity } }, }) // Pre-populate the events cache so EventDetailPopover can resolve by id client.setQueryData(['events'], { occurrences: [occurrence] }) return render( , ) } // ── Tests ───────────────────────────────────────────────────────────────────── describe('EventDetailPopover', () => { beforeEach(() => { vi.clearAllMocks() mockOpenEventId = null }) it('renders the event title as a heading', () => { renderPopover(TIMED_OCCURRENCE) expect(screen.getByRole('heading')).toHaveTextContent('Team Standup') }) it('renders location when present', () => { renderPopover(TIMED_OCCURRENCE) expect(screen.getByText(/Conference Room B/)).toBeDefined() }) it('renders description when present', () => { renderPopover(TIMED_OCCURRENCE) expect(screen.getByText(/Daily team sync meeting/)).toBeDefined() }) it('renders owner name in footer for personal events', () => { renderPopover(TIMED_OCCURRENCE) // TIMED_OCCURRENCE is personal (isShared:false) with ownerName:'Alice' expect(screen.getByText(/Alice/)).toBeDefined() }) it('renders "Family" in footer for shared calendar events', () => { renderPopover(ALLDAY_OCCURRENCE) // ALLDAY_OCCURRENCE has isShared:true — footer must show 'Family' expect(screen.getByText('Family')).toBeDefined() }) it('renders calendarName in footer when ownerName is null', () => { const noOwnerName: CalendarOccurrence = { ...TIMED_OCCURRENCE, id: 'no-owner-uid::2026-06-15T10:00:00', ownerName: null, } renderPopover(noOwnerName) expect(screen.getByText(/My Calendar/)).toBeDefined() }) it('renders an all-day event without crashing', () => { renderPopover(ALLDAY_OCCURRENCE) expect(screen.getByRole('heading')).toHaveTextContent('Birthday Party') }) it('close button has aria-label="Close"', () => { renderPopover(TIMED_OCCURRENCE) expect(screen.getByLabelText('Close')).toBeDefined() }) it('pressing Escape calls setOpenEventId(null)', () => { renderPopover(TIMED_OCCURRENCE) fireEvent.keyDown(document, { key: 'Escape' }) expect(mockSetOpenEventId).toHaveBeenCalledWith(null) }) it('clicking the close button calls setOpenEventId(null)', () => { renderPopover(TIMED_OCCURRENCE) fireEvent.click(screen.getByLabelText('Close')) expect(mockSetOpenEventId).toHaveBeenCalledWith(null) }) it('clicking the backdrop calls setOpenEventId(null)', () => { renderPopover(TIMED_OCCURRENCE) fireEvent.click(screen.getByTestId('popover-backdrop')) expect(mockSetOpenEventId).toHaveBeenCalledWith(null) }) it('renders nothing when openEventId is null', () => { mockOpenEventId = null ;(useCalendarStore as unknown as ReturnType).mockImplementation( (selector?: (s: Record) => unknown) => { const state = { openEventId: null, setOpenEventId: mockSetOpenEventId, setEventForm: mockSetEventForm, setDeleteDialog: mockSetDeleteDialog, } if (typeof selector === 'function') return selector(state) return state }, ) const client = new QueryClient({ defaultOptions: { queries: { retry: false, staleTime: Infinity } }, }) const { container } = render( , ) expect(container.firstChild).toBeNull() }) it('XSS guard: HTML in title renders as escaped text, not as DOM elements', () => { renderPopover(OCCURRENCE_WITH_HTML) const heading = screen.getByRole('heading') // Team Meeting') }) it('XSS guard: HTML in description renders as escaped text', () => { renderPopover(OCCURRENCE_WITH_HTML) const descEl = screen.getByTestId('event-description') // must NOT be rendered as a bold element expect(descEl.innerHTML).not.toContain('') expect(descEl.textContent).toContain('Bold description') }) // ── Phase 3 footer: Edit/Delete actions ──────────────────────────────────── it('footer renders an "Edit" button', () => { renderPopover(TIMED_OCCURRENCE) expect(screen.getByRole('button', { name: /edit/i })).toBeInTheDocument() }) it('footer renders a "Delete" button', () => { renderPopover(TIMED_OCCURRENCE) expect(screen.getByRole('button', { name: /delete/i })).toBeInTheDocument() }) it('clicking "Edit" opens EventForm in edit mode and closes popover', () => { renderPopover(TIMED_OCCURRENCE) fireEvent.click(screen.getByRole('button', { name: /edit/i })) expect(mockSetEventForm).toHaveBeenCalledWith(true, 'edit', TIMED_OCCURRENCE.uid) expect(mockSetOpenEventId).toHaveBeenCalledWith(null) }) it('clicking "Delete" opens DeleteConfirmationDialog (setDeleteDialog)', () => { renderPopover(TIMED_OCCURRENCE) fireEvent.click(screen.getByRole('button', { name: /delete/i })) expect(mockSetDeleteDialog).toHaveBeenCalledWith(true, TIMED_OCCURRENCE.uid) }) it('BUG-3 regression: IANA-bracketed start/end does not produce "Invalid Date" in rendered output', () => { // Fastmail events are serialized with IANA bracket notation e.g. '2026-06-18T08:00:00-04:00[America/Toronto]'. // new Date() cannot parse the bracket, so the date/time line showed "Invalid Date, Invalid Date – Invalid Date". // After the fix, the bracket is stripped before parsing. const occurrence: CalendarOccurrence = { ...TIMED_OCCURRENCE, id: 'iana-bracket-uid::1718712000000', uid: 'iana-bracket-uid', start: '2026-06-18T08:00:00-04:00[America/Toronto]', end: '2026-06-18T09:00:00-04:00[America/Toronto]', } renderPopover(occurrence) // The date/time text must not contain 'Invalid Date' const dialogEl = screen.getByRole('dialog') expect(dialogEl.textContent).not.toContain('Invalid Date') // It must contain recognizable date content (month name or a digit) // toLocaleDateString output varies by locale; check for a digit at minimum const dateTimeText = dialogEl.textContent ?? '' expect(dateTimeText).toMatch(/\d/) }) })