diff --git a/src/components/EventCalendar/YearlyEventCalender.spec.tsx b/src/components/EventCalendar/YearlyEventCalender.spec.tsx new file mode 100644 index 0000000000..a28fd1893d --- /dev/null +++ b/src/components/EventCalendar/YearlyEventCalender.spec.tsx @@ -0,0 +1,657 @@ +import React, { Suspense } from 'react'; +import { render, fireEvent, act, screen, waitFor, within } from '@testing-library/react'; +import { vi } from 'vitest'; +import Calendar from './YearlyEventCalender'; +import { BrowserRouter } from 'react-router-dom'; +import type { InterfaceRecurrenceRule } from 'utils/recurrenceUtils'; +import styles from '../../style/app.module.css'; + +enum Role { + USER = 'USER', + SUPERADMIN = 'SUPERADMIN', + ADMIN = 'ADMIN', +} + +const filterData = ( + eventData: any[], + orgData?: { admins: { _id: string }[] }, + userRole?: string, + userId?: string, +): any[] => { + + if (userRole === Role.SUPERADMIN) return eventData; + + const filteredEvents: any[] = []; + + // ADMIN case: only public events or events in their org + if (userRole === Role.ADMIN) { + const isOrgAdmin = orgData?.admins?.some( + (admin) => admin._id === userId + ); + + eventData.forEach((event) => { + // Always include public events + if (event.isPublic) { + filteredEvents.push(event); + } + // Include private events if the user is an admin of the organization + if (!event.isPublic && isOrgAdmin) { + filteredEvents.push(event); + } + }); + } + // USER case: public events or events they're attending + else { + eventData.forEach((event) => { + // Public events + if (event.isPublic) { + filteredEvents.push(event); + } + + // Events user is attending + const userAttending = event.attendees?.some( + (attendee:any) => attendee._id === userId + ); + + if (userAttending) { + filteredEvents.push(event); + } + }); + } + + // Remove duplicates + return Array.from(new Set(filteredEvents.map(e => e._id))) + .map(id => filteredEvents.find(e => e._id === id)) + .filter(Boolean); +}; + +const renderWithRouter = (ui: React.ReactElement) => { + return render( + + Loading...}> + {ui} + + + ); +}; + +describe('Calendar Component', () => { + const mockRefetchEvents = vi.fn(); + const today = new Date(); + + const mockEventData = [ + { + _id: '1', + location: 'Test Location', + title: 'Test Event', + description: 'Test Description', + startDate: new Date().toISOString(), + endDate: new Date().toISOString(), + startTime: '10:00', + endTime: '11:00', + allDay: false, + recurring: false, + recurrenceRule: null, + isRecurringEventException: false, + isPublic: true, + isRegisterable: true, + attendees: [{ _id: 'user1' }], + creator: { + firstName: 'John', + lastName: 'Doe', + _id: 'creator1' + } + }, + { + _id: '2', + location: 'Private Location', + title: 'Private Event', + description: 'Private Description', + startDate: new Date().toISOString(), + endDate: new Date().toISOString(), + startTime: '12:00', + endTime: '13:00', + allDay: false, + recurring: false, + recurrenceRule: null, + isRecurringEventException: false, + isPublic: false, + isRegisterable: true, + attendees: [{ _id: 'user2' }], + creator: { + firstName: 'Jane', + lastName: 'Doe', + _id: 'creator2' + } + } + ]; + + const mockOrgData = { + admins: [{ _id: 'admin1' }] + }; + + beforeEach(() => { + vi.clearAllMocks(); + }); + it('filters private events for ADMIN not in organization admins', async () => { + const privateEvent = { + ...mockEventData[1], + startDate: new Date().toISOString(), + endDate: new Date().toISOString() + }; + + const { container } = renderWithRouter( + + ); + + await waitFor(() => { + expect(container.querySelector('[data-testid="event-card"]')).toBeNull(); + }); + + // Verify empty state appears + const expandButton = container.querySelector(`.${styles.btn__more}`); + await act(async () => { + fireEvent.click(expandButton!); + }); + + await waitFor(() => { + expect(screen.getByText('No Event Available!')).toBeInTheDocument(); + }); + }); + + it('renders correctly with basic props', async () => { + const { getByText, getAllByTestId, container } = renderWithRouter( + + ); + + await waitFor(() => { + expect(getByText(new Date().getFullYear().toString())).toBeInTheDocument(); + }); + + expect(getByText('January')).toBeInTheDocument(); + expect(getByText('December')).toBeInTheDocument(); + + const weekdayHeaders = container.querySelectorAll('._calendar__weekdays_658d08'); + expect(weekdayHeaders.length).toBe(12); + + weekdayHeaders.forEach(header => { + const weekdaySlots = header.querySelectorAll('._weekday__yearly_658d08'); + expect(weekdaySlots.length).toBe(7); + }); + + const days = getAllByTestId('day'); + expect(days.length).toBeGreaterThan(0); + }); + + it('handles year navigation correctly', async () => { + const { getByTestId, getByText } = renderWithRouter( + + ); + + const currentYear = new Date().getFullYear(); + + await act(async () => { + fireEvent.click(getByTestId('prevYear')); + }); + await waitFor(() => { + expect(getByText(String(currentYear - 1))).toBeInTheDocument(); + }); + + await act(async () => { + fireEvent.click(getByTestId('nextYear')); + }); + await waitFor(() => { + expect(getByText(String(currentYear))).toBeInTheDocument(); + }); + }); + + it('filters events correctly for SUPERADMIN role', async () => { + renderWithRouter( + + ); + + const todayCell = await screen.findAllByTestId('day'); + expect(todayCell.length).toBeGreaterThan(0); + }); + + it('filters events correctly for ADMIN role', async () => { + const today = new Date(); + const mockEvent = { + ...mockEventData[0], + startDate: today.toISOString(), + endDate: today.toISOString() + }; + renderWithRouter( + + ); + + const todayCell = await screen.findAllByTestId('day'); + expect(todayCell.length).toBeGreaterThan(0); + }); + + it('filters events correctly for regular USER role', async () => { + const today = new Date(); + const mockEvent = { + ...mockEventData[0], + startDate: today.toISOString(), + endDate: today.toISOString() + }; + + renderWithRouter( + + ); + + const todayCell = await screen.findAllByTestId('day'); + expect(todayCell.length).toBeGreaterThan(0); + }); + + it('toggles expansion state when clicked', async () => { + const today = new Date(); + const mockEvent = { + ...mockEventData[0], + startDate: today.toISOString(), + endDate: today.toISOString() + }; + + const { container } = renderWithRouter( + + ); + + // Find the button with the circular button class + const expandButton = container.querySelector('._btn__more_658d08'); + expect(expandButton).toBeInTheDocument(); + + // Click and verify class changes (expansion) + await act(async () => { + fireEvent.click(expandButton!); + }); + + await waitFor(() => { + const expandedList = container.querySelector('._expand_event_list_658d08'); + expect(expandedList).toBeInTheDocument(); + }); + }); + + it('displays "No Event Available!" message when no events exist', async () => { + const { container, findByText } = renderWithRouter( + + ); + + const expandButton = container.querySelector('.btn__more'); + if (expandButton) { + await act(async () => { + fireEvent.click(expandButton); + }); + expect(await findByText('No Event Available!')).toBeInTheDocument(); + } + }); + + it('updates events when props change', async () => { + const mockEvent = { + ...mockEventData[0], + title: 'Test Event', + startDate: new Date().toISOString(), + endDate: new Date().toISOString() + }; + + const { rerender, container } = renderWithRouter( + + ); + + await screen.findAllByTestId('day'); + + const newMockEvents = [ + mockEvent, + { + ...mockEvent, + _id: '2', + title: 'New Test Event' + } + ]; + + rerender( + + Loading...}> + + + + ); + + const expandButtons = container.querySelectorAll('._btn__more_658d08'); + + for (const button of Array.from(expandButtons)) { + fireEvent.click(button); + + const eventList = container.querySelector('._event_list_658d08'); + if (eventList) { + expect(eventList).toBeInTheDocument(); + break; + } + } + }); + + it('filters events correctly for ADMIN role with private events', async () => { + const today = new Date(); + const mockEvent = { + ...mockEventData[1], + startDate: today.toISOString(), + endDate: today.toISOString() + }; + + renderWithRouter( + + ); + + const todayCell = await screen.findAllByTestId('day'); + expect(todayCell.length).toBeGreaterThan(0); + }); + + it('handles event expansion with various event scenarios', async () => { + const multiMonthEvents = [ + { + ...mockEventData[0], + startDate: new Date(today.getFullYear(), 0, 15).toISOString(), + endDate: new Date(today.getFullYear(), 1, 15).toISOString() + } + ]; + + const { container } = renderWithRouter( + + ); + + const expandButtons = container.querySelectorAll('._btn__more_658d08'); + + for (const button of Array.from(expandButtons)) { + await act(async () => { + fireEvent.click(button); + }); + } + + const expandedLists = container.querySelectorAll('._expand_event_list_658d08'); + expect(expandedLists.length).toBeGreaterThan(0); + }); + + it('handles calendar navigation and date rendering edge cases', async () => { + const { getByTestId, getByText, rerender } = renderWithRouter( + + ); + + await act(async () => { + fireEvent.click(getByTestId('prevYear')); + fireEvent.click(getByTestId('prevYear')); + }); + + await act(async () => { + fireEvent.click(getByTestId('nextYear')); + fireEvent.click(getByTestId('nextYear')); + }); + + const currentYear = new Date().getFullYear(); + expect(getByText(String(currentYear))).toBeInTheDocument(); + + rerender( + + ); + + expect(getByText(String(currentYear))).toBeInTheDocument(); + }); + + it('renders event list card with all possible configurations', async () => { + const complexEvent = { + _id: '2', + location: 'Complex Event Location', + title: 'Complex Recurring Event', + description: 'A complex recurring event description', + startDate: today.toISOString(), + endDate: today.toISOString(), + startTime: '10:00', + endTime: '11:00', + allDay: true, + recurring: true, + recurrenceRule: { + frequency: 'DAILY', + interval: 1 + } as InterfaceRecurrenceRule, + isRecurringEventException: false, + isPublic: true, + isRegisterable: true, + attendees: [ + { _id: 'user1' }, + { _id: 'user2' } + ], + creator: { + firstName: 'Jane', + lastName: 'Doe', + _id: 'creator2' + } + }; + + const { container } = renderWithRouter( + + ); + + const expandButtons = container.querySelectorAll('._btn__more_658d08'); + + for (const button of Array.from(expandButtons)) { + await act(async () => { + fireEvent.click(button); + }); + } + + const eventList = container.querySelector('._event_list_658d08'); + expect(eventList).toBeInTheDocument(); + }); + + it('collapses expanded event list when clicked again', async () => { + + const today = new Date(); + const mockEvent = { + ...mockEventData[0], + startDate: today.toISOString(), + endDate: today.toISOString() + }; + + const { container } = renderWithRouter( + + ); + + // Find the expansion button in a day cell. + const expandButton = container.querySelector('._btn__more_658d08'); + expect(expandButton).toBeInTheDocument(); + + // Click to expand. + await act(async () => { + fireEvent.click(expandButton!); + }); + await waitFor(() => { + const expandedList = container.querySelector('._expand_event_list_658d08'); + expect(expandedList).toBeInTheDocument(); + }); + + // Click again to collapse. + await act(async () => { + fireEvent.click(expandButton!); + }); + + await waitFor(() => { + expect(container.querySelector('._expand_event_list_658d08')).toBeNull(); + }); + }); + + + it('does not show registration option for non-registerable events', async () => { + // Create an event that is not registerable. + const nonRegisterableEvent = { + ...mockEventData[0], + _id: 'nonreg', + isRegisterable: false, + title: 'Non-Registerable Event', + startDate: new Date().toISOString(), + endDate: new Date().toISOString(), + }; + + const { container, queryByText } = renderWithRouter( + + ); + + const expandButton = container.querySelector('._btn__more_658d08'); + expect(expandButton).toBeInTheDocument(); + + await act(async () => { + fireEvent.click(expandButton!); + }); + + expect(queryByText(/register/i)).toBeNull(); + }); +}); + +describe('filterData function', () => { + const events = [ + { + _id: 'a', + isPublic: true, + attendees: [{ _id: 'user1' }] + }, + { + _id: 'b', + isPublic: false, + attendees: [{ _id: 'user2' }] + }, + { + _id: 'c', + isPublic: true, + attendees: [{ _id: 'user1' }, { _id: 'user3' }] + } + ]; + + const orgData = { admins: [{ _id: 'admin1' }] }; + + it('returns an empty array when no events are provided', () => { + expect(filterData([], orgData, Role.USER, 'user1')).toEqual([]); + }); + + it('returns all events for SUPERADMIN', () => { + expect(filterData(events, orgData, Role.SUPERADMIN, 'anyUser')).toEqual(events); + }); + + it('returns only public events for ADMIN when user is not an org admin', () => { + // Passing orgData with admins not including userId + const result = filterData(events, orgData, Role.ADMIN, 'notAdmin'); + // Only events with isPublic true should be returned + expect(result).toEqual( + expect.arrayContaining([ + expect.objectContaining({ _id: 'a' }), + expect.objectContaining({ _id: 'c' }), + ]) + ); + // No private events should be included + expect(result.find(e => e._id === 'b')).toBeUndefined(); + }); + + it('returns both public and private events for ADMIN when user is an org admin', () => { + const result = filterData(events, orgData, Role.ADMIN, 'admin1'); + expect(result).toEqual( + expect.arrayContaining([ + expect.objectContaining({ _id: 'a' }), + expect.objectContaining({ _id: 'b' }), + expect.objectContaining({ _id: 'c' }), + ]) + ); + }); + + it('for regular USER, returns public events and events user is attending (deduplicated)', () => { + + const result = filterData(events, orgData, Role.USER, 'user1'); + // Expect one copy per unique _id + const ids = result.map(e => e._id); + expect(ids.filter(id => id === 'a').length).toBe(1); + expect(ids.filter(id => id === 'c').length).toBe(1); + + expect(ids.includes('b')).toBe(false); + }); + + it('for regular USER, returns private events if the user is attending them', () => { + + const modifiedEvents = events.map(e => { + if (e._id === 'b') { + return { ...e, attendees: [{ _id: 'user1' }] }; + } + return e; + }); + const result = filterData(modifiedEvents, orgData, Role.USER, 'user1'); + const ids = result.map(e => e._id); + expect(ids.includes('b')).toBe(true); + }); +}); + + + + diff --git a/src/components/EventCalendar/YearlyEventCalender.tsx b/src/components/EventCalendar/YearlyEventCalender.tsx index 55e7332357..021b890a7f 100644 --- a/src/components/EventCalendar/YearlyEventCalender.tsx +++ b/src/components/EventCalendar/YearlyEventCalender.tsx @@ -140,7 +140,6 @@ const Calendar: React.FC = ({ const data: InterfaceEventListCardProps[] = []; if (userRole === Role.SUPERADMIN) return eventData; // Hard to test all the cases - /* istanbul ignore next */ if (userRole === Role.ADMIN) { eventData?.forEach((event) => { if (event.isPublic) data.push(event); @@ -218,7 +217,7 @@ const Calendar: React.FC = ({ currentDate.getFullYear(), currentDate.getMonth(), currentDate.getDate() + 1, - ); + ) } const renderedDays = days.map((date, dayIndex) => {