import * as appHooks from '@/app/hooks' import { AppDispatch } from '@/app/store' import { InfoRow } from '@/components/Event/InfoRow' import { stringAvatar, stringToColor } from '@/components/Event/utils/eventUtils' import { DelegationAccess } from '@/features/Calendars/CalendarTypes' import * as eventThunks from '@/features/Calendars/services' import EventPreviewModal from '@/features/Events/EventPreview' import { cleanup, fireEvent, screen, waitFor } from '@testing-library/react' import { renderWithProviders } from '../../utils/Renderwithproviders' describe('Event Preview Display', () => { const mockOnClose = jest.fn() const day = new Date('2025-01-15T10:00:00.000Z') // Fixed date in UTC: Jan 15, 2025 10:00 AM UTC beforeEach(() => { jest.clearAllMocks() sessionStorage.clear() window.MAIL_SPA_URL = null }) const preloadedState = { user: { userData: { sub: 'test', email: 'test@test.com', sid: 'aiYbWZSk2g0F+LrQeD7Dg4QcUMR8R/zTZdZBiA7N6Ro', openpaasId: '667037022b752d0026472254' }, organiserData: { cn: 'test', cal_address: 'test@test.com' } }, calendars: { list: { '667037022b752d0026472254/cal1': { id: '667037022b752d0026472254/cal1', name: 'Calendar', color: '#FF0000', events: { event1: { uid: 'event1', title: 'Test Event', calId: '667037022b752d0026472254/cal1', start: day.toISOString(), end: day.toISOString(), organizer: { cn: 'test', cal_address: 'test@test.com' }, attendee: [ { cn: 'test', cal_address: 'test@test.com', partstat: 'NEEDS-ACTION', rsvp: 'TRUE', role: 'REQ-PARTICIPANT', cutype: 'INDIVIDUAL' }, { cn: 'John', cal_address: 'john@test.com', partstat: 'NEEDS-ACTION', rsvp: 'TRUE', role: 'REQ-PARTICIPANT', cutype: 'INDIVIDUAL' }, { cn: 'Projector Room', cal_address: 'room1@test.com', partstat: 'ACCEPTED', rsvp: 'TRUE', role: 'REQ-PARTICIPANT', cutype: 'RESOURCE' } ] }, event2: { uid: 'event2', title: 'Test Event', calId: '667037022b752d0026472254/cal1', start: day.toISOString(), end: day.toISOString(), organizer: { cn: 'test', cal_address: 'test@test.com' } }, event3: { uid: 'event3', calId: '667037022b752d0026472254/cal1', start: day.toISOString(), end: day.toISOString(), organizer: { cn: 'test', cal_address: 'test@test.com' } }, event4: { uid: 'event4', calId: '667037022b752d0026472254/cal1', start: day.toISOString(), end: day.toISOString() } }, owner: { emails: ['test@test.com'] } }, 'otherCal/cal': { id: 'otherCal/cal', name: 'Calendar 1', color: '#FF0000', owner: { emails: ['other@test.com'] }, events: { event1: { uid: 'event1', calId: 'otherCal/cal', title: 'Test Event Other cal', start: day.toISOString(), end: day.toISOString(), organizer: { cn: 'john', cal_address: 'john@test.com' } }, event2: { uid: 'event2', calId: 'otherCal/cal', start: day.toISOString(), end: day.toISOString() } } } }, pending: false } } it('renders correctly event data', () => { const originalToLocaleString = Date.prototype.toLocaleString jest.spyOn(Date.prototype, 'toLocaleString').mockImplementation(function ( this: Date, locales?: Intl.LocalesArgument, options?: Intl.DateTimeFormatOptions ) { return originalToLocaleString.call(this, 'en-US', options) }) const originalToLocaleTimeString = Date.prototype.toLocaleTimeString jest .spyOn(Date.prototype, 'toLocaleTimeString') .mockImplementation(function ( this: Date, locales?: Intl.LocalesArgument, options?: Intl.DateTimeFormatOptions ) { return originalToLocaleTimeString.call(this, 'en-US', options) }) renderWithProviders( , preloadedState ) // With UTC timezone, we can assert exact values // Date: January 15, 2025 10:00 AM UTC expect(screen.getByText('Test Event')).toBeInTheDocument() expect(screen.getByText(/Wednesday/i)).toBeInTheDocument() expect(screen.getByText(/January/i)).toBeInTheDocument() expect(screen.getByText(/15/)).toBeInTheDocument() // Check time is displayed in 24h format (no AM/PM) // Format: "Wednesday, January 15, 2025 at 10:00" and " – 10:00" are in separate elements expect( screen.getByText(/Wednesday, January 15, 2025 at 10:00/) ).toBeInTheDocument() expect(screen.getByText(/– 10:00/)).toBeInTheDocument() expect(screen.getByText('Calendar')).toBeInTheDocument() expect(screen.getByText('Projector Room')).toBeInTheDocument() }) it('calls onClose when Cancel clicked', () => { renderWithProviders( , preloadedState ) fireEvent.click(screen.getByTestId('CloseIcon')) expect(mockOnClose).toHaveBeenCalledWith({}, 'backdropClick') }) it('Shows delete button only when calendar is own', () => { // Renders the other cal event renderWithProviders( , preloadedState ) const moreButton = screen.getByTestId('MoreVertIcon') if (moreButton) { expect( screen.queryByText('eventPreview.deleteEvent') ).not.toBeInTheDocument() } cleanup() // Renders the personal cal event renderWithProviders( , preloadedState ) fireEvent.click(screen.getByTestId('MoreVertIcon')) expect(screen.queryByText('eventPreview.deleteEvent')).toBeInTheDocument() }) it('calls delete when Delete clicked', async () => { renderWithProviders( , preloadedState ) const spy = jest .spyOn(eventThunks, 'deleteEventAsync') .mockImplementation(payload => { return () => Promise.resolve(payload) as any }) fireEvent.click(screen.getByTestId('MoreVertIcon')) fireEvent.click( screen.getByRole('menuitem', { name: 'eventPreview.deleteEvent' }) ) await waitFor(() => { expect(spy).toHaveBeenCalled() }) const receivedPayload = spy.mock.calls[0][0] expect(receivedPayload).toEqual({ calId: '667037022b752d0026472254/cal1', eventId: 'event1' }) expect(mockOnClose).toHaveBeenCalledWith({}, 'backdropClick') }) it('renders RSVP buttons when user is an attendee', () => { const rsvpStateIsOrga = { ...preloadedState, calendars: { ...preloadedState.calendars, list: { ...preloadedState.calendars.list, '667037022b752d0026472254/cal1': { ...preloadedState.calendars.list['667037022b752d0026472254/cal1'], events: { event1: { ...preloadedState.calendars.list[ '667037022b752d0026472254/cal1' ].events.event1, attendee: [ { cal_address: 'test@test.com', cn: 'Test User', partstat: 'NEEDS-ACTION' }, { cal_address: 'organizer@test.com', cn: 'Test Organizer', partstat: 'NEEDS-ACTION' } ], organizer: { cal_address: 'organizer@test.com' } } } } } } } renderWithProviders( , rsvpStateIsOrga ) expect( screen.getByText('eventPreview.attendingQuestion') ).toBeInTheDocument() expect( screen.getByRole('button', { name: 'eventPreview.ACCEPTED' }) ).toBeInTheDocument() expect( screen.getByRole('button', { name: 'eventPreview.TENTATIVE' }) ).toBeInTheDocument() expect( screen.getByRole('button', { name: 'eventPreview.DECLINED' }) ).toBeInTheDocument() }) it('doesnt renders RSVP buttons when user isnt an attendee', () => { const rsvpStateIsOrga = { ...preloadedState, calendars: { ...preloadedState.calendars, list: { ...preloadedState.calendars.list, '667037022b752d0026472254/cal1': { ...preloadedState.calendars.list['667037022b752d0026472254/cal1'], events: { event1: { ...preloadedState.calendars.list[ '667037022b752d0026472254/cal1' ].events.event1, attendee: [ { cal_address: 'organizer@test.com', cn: 'Test Organizer', partstat: 'NEEDS-ACTION' } ], organizer: { cal_address: 'organizer@test.com' } } } } } } } renderWithProviders( , rsvpStateIsOrga ) expect( screen.queryByText('eventPreview.attendingQuestion') ).not.toBeInTheDocument() expect( screen.queryByRole('button', { name: 'eventPreview.ACCEPTED' }) ).not.toBeInTheDocument() expect( screen.queryByRole('button', { name: 'eventPreview.TENTATIVE' }) ).not.toBeInTheDocument() expect( screen.queryByRole('button', { name: 'eventPreview.DECLINED' }) ).not.toBeInTheDocument() }) it('handles RSVP Accept click', async () => { const spy = jest .spyOn(eventThunks, 'putEventAsync') .mockImplementation(payload => { const promise = Promise.resolve(payload) ;(promise as any).unwrap = () => promise return () => promise as any }) const rsvpState = { ...preloadedState, calendars: { ...preloadedState.calendars, list: { ...preloadedState.calendars.list, '667037022b752d0026472254/cal1': { ...preloadedState.calendars.list['667037022b752d0026472254/cal1'], events: { event1: { ...preloadedState.calendars.list[ '667037022b752d0026472254/cal1' ].events.event1, attendee: [ { cal_address: 'test@test.com', cn: 'Test User', partstat: 'NEEDS-ACTION' } ], organizer: { cal_address: 'organizer@test.com' } } } } } } } renderWithProviders( , rsvpState ) fireEvent.click( screen.getByRole('button', { name: 'eventPreview.ACCEPTED' }) ) await waitFor(() => { expect(spy).toHaveBeenCalled() }) const updatedEvent = spy.mock.calls[0][0].newEvent expect(updatedEvent.attendee[0].partstat).toBe('ACCEPTED') }) it('handles RSVP Maybe click', async () => { const spy = jest .spyOn(eventThunks, 'putEventAsync') .mockImplementation(payload => { const promise = Promise.resolve(payload) ;(promise as any).unwrap = () => promise return () => promise as any }) const rsvpState = { ...preloadedState, calendars: { ...preloadedState.calendars, list: { ...preloadedState.calendars.list, '667037022b752d0026472254/cal1': { ...preloadedState.calendars.list['667037022b752d0026472254/cal1'], events: { event1: { ...preloadedState.calendars.list[ '667037022b752d0026472254/cal1' ].events.event1, attendee: [ { cal_address: 'test@test.com', cn: 'Test User', partstat: 'NEEDS-ACTION' } ], organizer: { cal_address: 'organizer@test.com' } } } } } } } renderWithProviders( , rsvpState ) fireEvent.click( screen.getByRole('button', { name: 'eventPreview.TENTATIVE' }) ) await waitFor(() => { expect(spy).toHaveBeenCalled() }) const updatedEvent = spy.mock.calls[0][0].newEvent expect(updatedEvent.attendee[0].partstat).toBe('TENTATIVE') }) it('handles RSVP Decline click', async () => { const spy = jest .spyOn(eventThunks, 'putEventAsync') .mockImplementation(payload => { const promise = Promise.resolve(payload) ;(promise as any).unwrap = () => promise return () => promise as any }) const rsvpState = { ...preloadedState, calendars: { ...preloadedState.calendars, list: { ...preloadedState.calendars.list, '667037022b752d0026472254/cal1': { ...preloadedState.calendars.list['667037022b752d0026472254/cal1'], events: { event1: { ...preloadedState.calendars.list[ '667037022b752d0026472254/cal1' ].events.event1, attendee: [ { cal_address: 'test@test.com', cn: 'Test User', partstat: 'NEEDS-ACTION' } ], organizer: { cal_address: 'organizer@test.com' } } } } } } } renderWithProviders( , rsvpState ) fireEvent.click( screen.getByRole('button', { name: 'eventPreview.DECLINED' }) ) await waitFor(() => { expect(spy).toHaveBeenCalled() }) const updatedEvent = spy.mock.calls[0][0].newEvent expect(updatedEvent.attendee[0].partstat).toBe('DECLINED') }) it('handles Edit click when is own but with no organizer', async () => { renderWithProviders( , preloadedState ) fireEvent.click(screen.getByTestId('EditIcon')) await waitFor(() => { expect(screen.getByText('event.updateEvent')).toBeInTheDocument() }) }) it('doesnt show edit button when is not own and with no organizer', async () => { renderWithProviders( , preloadedState ) await waitFor(() => { expect(screen.queryByTestId('EditIcon')).not.toBeInTheDocument() }) }) it('handles Edit click', async () => { renderWithProviders( , preloadedState ) fireEvent.click(screen.getByTestId('EditIcon')) await waitFor(() => { expect(screen.getByText('event.updateEvent')).toBeInTheDocument() }) }) it('properly render message button when MAIL_SPA_URL is not null and event has attendees', () => { window.MAIL_SPA_URL = 'test' renderWithProviders( , preloadedState ) fireEvent.click(screen.getByTestId('MoreVertIcon')) expect(screen.getByText('eventPreview.emailAttendees')).toBeInTheDocument() }) it('doesnt render message button when MAIL_SPA_URL is not null and event has no attendees', () => { window.MAIL_SPA_URL = 'test' renderWithProviders( , preloadedState ) expect(screen.queryByTestId('EmailIcon')).not.toBeInTheDocument() }) it('doesnt render message button when MAIL_SPA_URL is null and event has attendees', () => { renderWithProviders( , preloadedState ) expect(screen.queryByTestId('EmailIcon')).not.toBeInTheDocument() }) it('message button opens url with attendees as uri and title as subject', () => { window.MAIL_SPA_URL = 'test' const mockOpen = jest.fn() window.open = mockOpen renderWithProviders( , preloadedState ) fireEvent.click(screen.getByTestId('MoreVertIcon')) const emailButton = screen.getByRole('menuitem', { name: 'eventPreview.emailAttendees' }) expect(emailButton).toBeInTheDocument() fireEvent.click(emailButton) const expectedUrl = `test/mailto/?uri=mailto%3Ajohn%40test.com&subject=Test%20Event` expect(mockOpen).toHaveBeenCalledWith( expectedUrl, '_blank', 'noopener,noreferrer' ) }) it('message button encodes special characters in event title correctly', () => { window.MAIL_SPA_URL = 'test' const mockOpen = jest.fn() window.open = mockOpen const specialCharState = { ...preloadedState, calendars: { ...preloadedState.calendars, list: { ...preloadedState.calendars.list, '667037022b752d0026472254/cal1': { ...preloadedState.calendars.list['667037022b752d0026472254/cal1'], events: { ...preloadedState.calendars.list['667037022b752d0026472254/cal1'] .events, eventWithSpecialChars: { uid: 'eventWithSpecialChars', title: 'Meeting & Discussion? #Important', calId: '667037022b752d0026472254/cal1', start: day.toISOString(), end: day.toISOString(), organizer: { cn: 'test', cal_address: 'test@test.com' }, attendee: [ { cn: 'test', cal_address: 'test@test.com', partstat: 'NEEDS-ACTION', rsvp: 'TRUE', role: 'REQ-PARTICIPANT', cutype: 'INDIVIDUAL' }, { cn: 'John', cal_address: 'john@test.com', partstat: 'NEEDS-ACTION', rsvp: 'TRUE', role: 'REQ-PARTICIPANT', cutype: 'INDIVIDUAL' } ] } } } } } } renderWithProviders( , specialCharState ) fireEvent.click(screen.getByTestId('MoreVertIcon')) const emailButton = screen.getByRole('menuitem', { name: 'eventPreview.emailAttendees' }) expect(emailButton).toBeInTheDocument() fireEvent.click(emailButton) const expectedUrl = `test/mailto/?uri=mailto%3Ajohn%40test.com&subject=Meeting%20%26%20Discussion%3F%20%23Important` expect(mockOpen).toHaveBeenCalledWith( expectedUrl, '_blank', 'noopener,noreferrer' ) }) describe('Owner Email Permissions', () => { const today = new Date() const start = new Date(today) start.setHours(10, 0, 0, 0) const end = new Date(today) end.setHours(11, 0, 0, 0) beforeEach(() => { jest.clearAllMocks() const dispatch = jest.fn() as AppDispatch jest.spyOn(appHooks, 'useAppDispatch').mockReturnValue(dispatch) }) const createPreloadedState = ( userEmail: string, ownerEmails: string[], organizerEmail: string ) => ({ user: { userData: { sub: 'test', email: userEmail, sid: 'mockSid', openpaasId: '667037022b752d0026472254' }, tokens: { accessToken: 'token' } }, calendars: { list: { '667037022b752d0026472254/cal1': { name: 'Calendar 1', id: '667037022b752d0026472254/cal1', color: '#FF0000', owner: { emails: ownerEmails }, events: { event1: { uid: 'event1', calId: '667037022b752d0026472254/cal1', title: 'Test Event', start: start.toISOString(), end: end.toISOString(), partstat: 'ACCEPTED', organizer: { cn: 'Organizer', cal_address: organizerEmail }, attendee: [ { cn: 'Organizer', partstat: 'ACCEPTED', rsvp: 'TRUE', role: 'REQ-PARTICIPANT', cutype: 'INDIVIDUAL', cal_address: organizerEmail } ] } } } }, pending: false } }) it('should show edit button when user email matches organizer and is in ownerEmails', async () => { const preloadedState = createPreloadedState( 'owner@test.com', ['owner@test.com'], 'owner@test.com' ) const mockOnClose = jest.fn() renderWithProviders( , preloadedState ) await waitFor(() => { const editButton = screen.queryByTestId('EditIcon') expect(editButton).toBeInTheDocument() }) }) it('should NOT show edit button when user is organizer but not in owner of calendar', async () => { const preloadedState = createPreloadedState( 'organizer@test.com', ['other@test.com'], // organizer not in list 'organizer@test.com' ) const mockOnClose = jest.fn() renderWithProviders( , preloadedState ) await waitFor(() => { const editButton = screen.queryByTestId('EditIcon') expect(editButton).not.toBeInTheDocument() }) }) it('should NOT show edit button when user is in ownerEmails but not organizer', async () => { const preloadedState = createPreloadedState( 'owner@test.com', ['owner@test.com'], 'other@test.com' // different organizer ) const mockOnClose = jest.fn() renderWithProviders( , preloadedState ) await waitFor(() => { const editButton = screen.queryByTestId('EditIcon') expect(editButton).not.toBeInTheDocument() }) }) it('should handle calendars without ownerEmails property gracefully', async () => { const preloadedStateWithoutOwnerEmails = { user: { userData: { sub: 'test', email: 'test@test.com', sid: 'mockSid', openpaasId: '667037022b752d0026472254' }, tokens: { accessToken: 'token' } }, calendars: { list: { '667037022b752d0026472254/cal1': { name: 'Calendar 1', id: '667037022b752d0026472254/cal1', color: '#FF0000', owner: {}, events: { event1: { calId: '667037022b752d0026472254/cal1', uid: 'event1', title: 'Test Event', start: start.toISOString(), end: end.toISOString(), organizer: { cn: 'Test', cal_address: 'test@test.com' } } } } }, pending: false } } const mockOnClose = jest.fn() expect(() => { renderWithProviders( , preloadedStateWithoutOwnerEmails ) }).not.toThrow() }) }) it('renders correctly event data event with empty title', () => { renderWithProviders( , preloadedState ) expect(screen.getByText('event.untitled')).toBeInTheDocument() expect(screen.getByText('Calendar')).toBeInTheDocument() }) describe('Attendee Preview Display', () => { const mockOnClose = jest.fn() const day = new Date('2025-01-15T10:00:00.000Z') beforeEach(() => { jest.clearAllMocks() window.MAIL_SPA_URL = null }) const createStateWithAttendees = (attendees: any[]) => ({ user: { userData: { sub: 'test', email: 'test@test.com', sid: 'aiYbWZSk2g0F+LrQeD7Dg4QcUMR8R/zTZdZBiA7N6Ro', openpaasId: '667037022b752d0026472254' }, organiserData: { cn: 'test', cal_address: 'test@test.com' } }, calendars: { list: { '667037022b752d0026472254/cal1': { id: '667037022b752d0026472254/cal1', name: 'Calendar', color: '#FF0000', events: { event1: { uid: 'event1', title: 'Test Event', calId: '667037022b752d0026472254/cal1', start: day.toISOString(), end: day.toISOString(), organizer: { cn: 'organizer', cal_address: 'organizer@test.com' }, attendee: attendees } }, owner: { emails: ['test@test.com'] } } }, pending: false } }) describe('Guest count display', () => { it('displays correct guest count including organizer', () => { const attendees = [ { cn: 'organizer', cal_address: 'organizer@test.com', partstat: 'ACCEPTED', rsvp: 'TRUE', role: 'REQ-PARTICIPANT', cutype: 'INDIVIDUAL' }, { cn: 'Guest 1', cal_address: 'guest1@test.com', partstat: 'NEEDS-ACTION', rsvp: 'TRUE', role: 'REQ-PARTICIPANT', cutype: 'INDIVIDUAL' }, { cn: 'Guest 2', cal_address: 'guest2@test.com', partstat: 'NEEDS-ACTION', rsvp: 'TRUE', role: 'REQ-PARTICIPANT', cutype: 'INDIVIDUAL' } ] const state = createStateWithAttendees(attendees) renderWithProviders( , state ) // Should display "3 guests" (organizer + 2 guests) expect(screen.getByText(/3/)).toBeInTheDocument() }) }) describe('All attendees with single status', () => { it('displays only yes count when all attendees accepted', () => { const attendees = [ { cn: 'organizer', cal_address: 'organizer@test.com', partstat: 'ACCEPTED', rsvp: 'TRUE', role: 'REQ-PARTICIPANT', cutype: 'INDIVIDUAL' }, { cn: 'Guest 1', cal_address: 'guest1@test.com', partstat: 'ACCEPTED', rsvp: 'TRUE', role: 'REQ-PARTICIPANT', cutype: 'INDIVIDUAL' }, { cn: 'Guest 2', cal_address: 'guest2@test.com', partstat: 'ACCEPTED', rsvp: 'TRUE', role: 'REQ-PARTICIPANT', cutype: 'INDIVIDUAL' } ] const state = createStateWithAttendees(attendees) renderWithProviders( , state ) expect( screen.getByText('eventPreview.yesCount(count=3)') ).toBeInTheDocument() expect( screen.queryByText(/eventPreview.maybeCount/i) ).not.toBeInTheDocument() expect( screen.queryByText(/eventPreview.needActionCount/i) ).not.toBeInTheDocument() expect( screen.queryByText(/eventPreview.noCount\b/i) ).not.toBeInTheDocument() }) it('displays only TENTATIVE count when all attendees tentative', () => { const attendees = [ { cn: 'organizer', cal_address: 'organizer@test.com', partstat: 'TENTATIVE', rsvp: 'TRUE', role: 'REQ-PARTICIPANT', cutype: 'INDIVIDUAL' }, { cn: 'Guest 1', cal_address: 'guest1@test.com', partstat: 'TENTATIVE', rsvp: 'TRUE', role: 'REQ-PARTICIPANT', cutype: 'INDIVIDUAL' } ] const state = createStateWithAttendees(attendees) renderWithProviders( , state ) expect( screen.getByText('eventPreview.maybeCount(count=2)') ).toBeInTheDocument() expect( screen.queryByText(/eventPreview.yesCount/i) ).not.toBeInTheDocument() expect( screen.queryByText(/eventPreview.needActionCount/i) ).not.toBeInTheDocument() expect( screen.queryByText(/eventPreview.noCount\b/i) ).not.toBeInTheDocument() }) it('displays only no count when all attendees declined', () => { const attendees = [ { cn: 'organizer', cal_address: 'organizer@test.com', partstat: 'DECLINED', rsvp: 'TRUE', role: 'REQ-PARTICIPANT', cutype: 'INDIVIDUAL' }, { cn: 'Guest 1', cal_address: 'guest1@test.com', partstat: 'DECLINED', rsvp: 'TRUE', role: 'REQ-PARTICIPANT', cutype: 'INDIVIDUAL' }, { cn: 'Guest 2', cal_address: 'guest2@test.com', partstat: 'DECLINED', rsvp: 'TRUE', role: 'REQ-PARTICIPANT', cutype: 'INDIVIDUAL' }, { cn: 'Guest 3', cal_address: 'guest3@test.com', partstat: 'DECLINED', rsvp: 'TRUE', role: 'REQ-PARTICIPANT', cutype: 'INDIVIDUAL' } ] const state = createStateWithAttendees(attendees) renderWithProviders( , state ) expect( screen.getByText('eventPreview.noCount(count=4)') ).toBeInTheDocument() expect( screen.queryByText(/eventPreview.yesCount/i) ).not.toBeInTheDocument() expect( screen.queryByText(/eventPreview.maybeCount/i) ).not.toBeInTheDocument() expect( screen.queryByText(/eventPreview.needActionCount/i) ).not.toBeInTheDocument() }) it('displays only waiting count when all attendees need action', () => { const attendees = [ { cn: 'organizer', cal_address: 'organizer@test.com', partstat: 'NEEDS-ACTION', rsvp: 'TRUE', role: 'REQ-PARTICIPANT', cutype: 'INDIVIDUAL' }, { cn: 'Guest 1', cal_address: 'guest1@test.com', partstat: 'NEEDS-ACTION', rsvp: 'TRUE', role: 'REQ-PARTICIPANT', cutype: 'INDIVIDUAL' } ] const state = createStateWithAttendees(attendees) renderWithProviders( , state ) expect( screen.getByText('eventPreview.needActionCount(count=2)') ).toBeInTheDocument() expect( screen.queryByText(/eventPreview.yesCount/i) ).not.toBeInTheDocument() expect( screen.queryByText(/eventPreview.maybeCount/i) ).not.toBeInTheDocument() expect( screen.queryByText(/eventPreview.noCount/i) ).not.toBeInTheDocument() }) }) describe('No display when count is 0', () => { it('does not display attendee preview when no attendees', () => { const state = createStateWithAttendees([]) renderWithProviders( , state ) expect( screen.queryByTestId('PeopleAltOutlinedIcon') ).not.toBeInTheDocument() expect(screen.queryByText(/guests/i)).not.toBeInTheDocument() }) }) describe('Regression tests: mixed statuses with correct counts', () => { it('displays all statuses when attendees have mixed responses', () => { const attendees = [ { cn: 'organizer', cal_address: 'organizer@test.com', partstat: 'ACCEPTED', rsvp: 'TRUE', role: 'REQ-PARTICIPANT', cutype: 'INDIVIDUAL' }, { cn: 'Guest 1', cal_address: 'guest1@test.com', partstat: 'ACCEPTED', rsvp: 'TRUE', role: 'REQ-PARTICIPANT', cutype: 'INDIVIDUAL' }, { cn: 'Guest 2', cal_address: 'guest2@test.com', partstat: 'TENTATIVE', rsvp: 'TRUE', role: 'REQ-PARTICIPANT', cutype: 'INDIVIDUAL' }, { cn: 'Guest 3', cal_address: 'guest3@test.com', partstat: 'NEEDS-ACTION', rsvp: 'TRUE', role: 'REQ-PARTICIPANT', cutype: 'INDIVIDUAL' }, { cn: 'Guest 4', cal_address: 'guest4@test.com', partstat: 'DECLINED', rsvp: 'TRUE', role: 'REQ-PARTICIPANT', cutype: 'INDIVIDUAL' } ] const state = createStateWithAttendees(attendees) renderWithProviders( , state ) expect( screen.getByText(/eventPreview.yesCount\(count\=2\)/i) ).toBeInTheDocument() expect( screen.getByText(/eventPreview.maybeCount\(count\=1\)/i) ).toBeInTheDocument() expect( screen.getByText(/eventPreview.needActionCount\(count\=1\)/i) ).toBeInTheDocument() expect( screen.getByText(/eventPreview.noCount\(count\=1\)/i) ).toBeInTheDocument() }) it('displays correct counts with multiple yes and TENTATIVE', () => { const attendees = [ { cn: 'Guest 1', cal_address: 'guest1@test.com', partstat: 'ACCEPTED', rsvp: 'TRUE', role: 'REQ-PARTICIPANT', cutype: 'INDIVIDUAL' }, { cn: 'Guest 2', cal_address: 'guest2@test.com', partstat: 'ACCEPTED', rsvp: 'TRUE', role: 'REQ-PARTICIPANT', cutype: 'INDIVIDUAL' }, { cn: 'Guest 3', cal_address: 'guest3@test.com', partstat: 'ACCEPTED', rsvp: 'TRUE', role: 'REQ-PARTICIPANT', cutype: 'INDIVIDUAL' }, { cn: 'Guest 4', cal_address: 'guest4@test.com', partstat: 'TENTATIVE', rsvp: 'TRUE', role: 'REQ-PARTICIPANT', cutype: 'INDIVIDUAL' }, { cn: 'Guest 5', cal_address: 'guest5@test.com', partstat: 'TENTATIVE', rsvp: 'TRUE', role: 'REQ-PARTICIPANT', cutype: 'INDIVIDUAL' } ] const state = createStateWithAttendees(attendees) renderWithProviders( , state ) expect( screen.getByText(/eventPreview\.yesCount\(count\=3\)/i) ).toBeInTheDocument() expect( screen.getByText(/eventPreview\.maybeCount\(count\=2\)/i) ).toBeInTheDocument() expect( screen.queryByText(/eventPreview.noCount/i) ).not.toBeInTheDocument() expect( screen.queryByText(/eventPreview.needActionCount/i) ).not.toBeInTheDocument() }) it('displays correct counts with waiting and declined', () => { const attendees = [ { cn: 'Guest 1', cal_address: 'guest1@test.com', partstat: 'NEEDS-ACTION', rsvp: 'TRUE', role: 'REQ-PARTICIPANT', cutype: 'INDIVIDUAL' }, { cn: 'Guest 2', cal_address: 'guest2@test.com', partstat: 'NEEDS-ACTION', rsvp: 'TRUE', role: 'REQ-PARTICIPANT', cutype: 'INDIVIDUAL' }, { cn: 'Guest 3', cal_address: 'guest3@test.com', partstat: 'NEEDS-ACTION', rsvp: 'TRUE', role: 'REQ-PARTICIPANT', cutype: 'INDIVIDUAL' }, { cn: 'Guest 4', cal_address: 'guest4@test.com', partstat: 'DECLINED', rsvp: 'TRUE', role: 'REQ-PARTICIPANT', cutype: 'INDIVIDUAL' } ] const state = createStateWithAttendees(attendees) renderWithProviders( , state ) expect( screen.getByText(/eventPreview.needActionCount\(count\=3\)/i) ).toBeInTheDocument() expect( screen.getByText(/eventPreview.noCount\(count\=1\)/i) ).toBeInTheDocument() expect( screen.queryByText(/eventPreview.yesCount/i) ).not.toBeInTheDocument() expect( screen.queryByText(/eventPreview.maybeCount/i) ).not.toBeInTheDocument() }) it('does not display status categories with zero count', () => { const attendees = [ { cn: 'Guest 1', cal_address: 'guest1@test.com', partstat: 'ACCEPTED', rsvp: 'TRUE', role: 'REQ-PARTICIPANT', cutype: 'INDIVIDUAL' }, { cn: 'Guest 2', cal_address: 'guest2@test.com', partstat: 'DECLINED', rsvp: 'TRUE', role: 'REQ-PARTICIPANT', cutype: 'INDIVIDUAL' } ] const state = createStateWithAttendees(attendees) renderWithProviders( , state ) expect( screen.getByText(/eventPreview.yesCount\(count\=1\)/i) ).toBeInTheDocument() expect( screen.getByText(/eventPreview.noCount\(count\=1\)/i) ).toBeInTheDocument() expect( screen.queryByText(/eventPreview.maybeCount/i) ).not.toBeInTheDocument() expect( screen.queryByText(/eventPreview.needActionCount/i) ).not.toBeInTheDocument() }) it('handles large number of attendees correctly', () => { const attendees = Array.from({ length: 15 }, (_, i) => ({ cn: `Guest ${i}`, cal_address: `guest${i}@test.com`, partstat: i < 8 ? 'ACCEPTED' : i < 11 ? 'TENTATIVE' : i < 13 ? 'NEEDS-ACTION' : 'DECLINED', rsvp: 'TRUE', role: 'REQ-PARTICIPANT', cutype: 'INDIVIDUAL' })) const state = createStateWithAttendees(attendees) renderWithProviders( , state ) expect( screen.getByText(/eventPreview.yesCount\(count\=8\)/i) ).toBeInTheDocument() expect( screen.getByText(/eventPreview.maybeCount\(count\=3\)/i) ).toBeInTheDocument() expect( screen.getByText(/eventPreview.needActionCount\(count\=2\)/i) ).toBeInTheDocument() expect( screen.getByText(/eventPreview.noCount\(count\=2\)/i) ).toBeInTheDocument() }) }) }) describe('EventDisplayPreview - delegation', () => { const delegatedBaseEvent = { uid: 'event-1', calId: 'user2/cal1', title: 'Delegated Event', start: day.toISOString(), end: day.toISOString(), organizer: { cal_address: 'owner@example.com' }, attendee: [ { cal_address: 'owner@example.com', partstat: 'NEEDS-ACTION' } ], URL: '/calendars/user2/cal1/event-1.ics' } const makeDelegatedState = ( eventOverrides = {}, access: DelegationAccess = { write: true, freebusy: false, read: true, 'write-properties': false, all: false } ) => ({ ...preloadedState, calendars: { list: { 'user2/cal1': { id: 'user2/cal1', name: 'Delegated Calendar', delegated: true, access, owner: { id: 'user2', firstname: 'Bob', lastname: 'Owner', emails: ['owner@example.com'], preferredEmail: 'owner@example.com' }, color: { light: '#FF0000', dark: '#000' }, events: { 'event-1': { ...delegatedBaseEvent, ...eventOverrides } } } }, templist: {}, pending: false }, user: { userData: { ...preloadedState.user.userData, email: 'alice@example.com', openpaasId: 'user1' } } }) describe('edit button visibility', () => { it('shows edit button when calendar is write-delegated and owner is organizer', () => { renderWithProviders( , makeDelegatedState() ) expect( screen.getByTestId('EditIcon').closest('button') ).toBeInTheDocument() }) it('does not show edit button when delegated but owner is not organizer', () => { renderWithProviders( , makeDelegatedState({ organizer: { cal_address: 'someone-else@example.com' } }) ) expect(screen.queryByTestId('EditIcon')).not.toBeInTheDocument() }) it('does not show edit button when delegated with read-only access', () => { renderWithProviders( , makeDelegatedState( {}, { write: false, freebusy: false, read: true, 'write-properties': false, all: false } ) ) expect(screen.queryByTestId('EditIcon')).not.toBeInTheDocument() }) }) describe('delete menu item visibility', () => { it('shows delete option when calendar is write-delegated', () => { renderWithProviders( , makeDelegatedState() ) fireEvent.click(screen.getByTestId('MoreVertIcon')) expect(screen.getByText('eventPreview.deleteEvent')).toBeInTheDocument() }) it('does not show delete option when delegated with read-only access', () => { renderWithProviders( , makeDelegatedState( {}, { write: false, freebusy: false, read: true, 'write-properties': false, all: false } ) ) fireEvent.click(screen.getByTestId('MoreVertIcon')) expect( screen.queryByText('eventPreview.deleteEvent') ).not.toBeInTheDocument() }) }) }) describe('BUGFIX', () => { it('doesnt render anything next to date of all day preview', () => { const allDayState = { ...preloadedState, calendars: { ...preloadedState.calendars, list: { ...preloadedState.calendars.list, '667037022b752d0026472254/cal1': { ...preloadedState.calendars.list['667037022b752d0026472254/cal1'], events: { event1: { ...preloadedState.calendars.list[ '667037022b752d0026472254/cal1' ].events.event1, allday: true // <-- the fix condition } } } } } } renderWithProviders( , allDayState ) const dateElement = screen.getByText(/January/i).closest('p') ?? screen.getByText(/January/i) expect(dateElement).toBeInTheDocument() console.log(dateElement.textContent) const text = dateElement.textContent?.trim() ?? '' expect(text).toMatch(/\d{4}$/) }) }) }) describe('Helper functions', () => { it('stringToColor generates consistent color', () => { expect(stringToColor('Alice')).toMatch(/^#[0-9a-f]{6}$/) expect(stringToColor('Alice')).toBe(stringToColor('Alice')) }) it('stringAvatar returns correct props', () => { const result = stringAvatar('Alice') expect(result.children).toBe('A') expect(result.color).toBeDefined() expect(typeof result.color).toBe('string') }) it('stringAvatar returns 2-letter initials for full name', () => { const result = stringAvatar('John Doe') expect(result.children).toBe('JD') expect(result.color).toBeDefined() expect(typeof result.color).toBe('string') }) it('stringAvatar handles single word names', () => { const result = stringAvatar('Alice') expect(result.children).toBe('A') expect(result.color).toBeDefined() }) it('stringAvatar handles email addresses', () => { const result = stringAvatar('test@example.com') expect(result.children).toBe('T') expect(result.color).toBeDefined() }) it('InfoRow renders text and link if url is valid', () => { renderWithProviders( ico} text="Meeting" data="https://example.com" /> ) expect(screen.getByText('Meeting').closest('a')).toHaveAttribute( 'href', 'https://example.com' ) }) })