diff --git a/__test__/components/MiniCalendarColor.test.tsx b/__test__/components/MiniCalendarColor.test.tsx index 4a3e3ab..941b3fd 100644 --- a/__test__/components/MiniCalendarColor.test.tsx +++ b/__test__/components/MiniCalendarColor.test.tsx @@ -47,10 +47,10 @@ describe("MiniCalendar", () => { it("renders mini calendar with today in orange", async () => { renderCalendar(); - const today = new Date().getDate().toString(); - const todayTile = screen - .getAllByText(today) - .find((el) => el.tagName.toLowerCase() === "abbr"); + const today = new Date(); + const dateTestId = `date-${today.getFullYear()}-${today.getMonth()}-${today.getDate()}`; + + const todayTile = screen.getByTestId(dateTestId); expect(todayTile?.parentElement).toHaveClass("today"); }); @@ -65,13 +65,10 @@ describe("MiniCalendar", () => { const date = new Date(sunday); date.setDate(sunday.getDate() + i); const dateTestId = `date-${date.getFullYear()}-${date.getMonth()}-${date.getDate()}`; - preview.debug(); const tile = screen.getByTestId(dateTestId); if (date.getTime() !== today.setHours(0, 0, 0, 0)) { - preview.debug(); - expect(tile?.parentElement).toHaveClass("selectedWeek"); } } @@ -84,10 +81,10 @@ describe("MiniCalendar", () => { const dayViewButton = await screen.findByTitle(/day view/i); fireEvent.click(dayViewButton); - const today = new Date().getDate().toString(); - const todayTile = screen - .getAllByText(today) - .find((el) => el.tagName.toLowerCase() === "abbr"); + const today = new Date(); + const dateTestId = `date-${today.getFullYear()}-${today.getMonth()}-${today.getDate()}`; + + const todayTile = screen.getByTestId(dateTestId); expect(todayTile?.parentElement).toHaveClass("selectedWeek"); }); @@ -164,23 +161,19 @@ describe("Found Bugs", () => { const previousMonthButton = screen.getByText("<"); fireEvent.click(nextMonthButton); fireEvent.click(previousMonthButton); - const shownDay = screen.getByText((content, element) => { - return ( - element?.className.toLowerCase().includes("fc-daygrid-day-number") ?? - false - ); - }); const selectedTile = screen.getByText((content, element) => { return element?.className.includes("selectedWeek") ?? false; }); - const supposedSelectedTile = screen - .getAllByText((content, element) => { - return element?.tagName.toLowerCase() === "abbr"; - }) - .find((el) => el.innerHTML === shownDay.innerHTML); + const ariaLabel = screen.getByRole("columnheader"); + const shownDayDate = new Date( + ariaLabel.getAttribute("data-date") as string + ); + const dateTestId = `date-${shownDayDate.getFullYear()}-${shownDayDate.getMonth()}-${shownDayDate.getDate()}`; + + const supposedSelectedTile = screen.getByTestId(dateTestId); expect(selectedTile.children[0].innerHTML).toBe( - supposedSelectedTile?.innerHTML + supposedSelectedTile.parentElement?.children[0]?.innerHTML ); expect(supposedSelectedTile?.parentElement).toHaveClass("selectedWeek"); }); diff --git a/__test__/features/Events/EventDisplay.test.tsx b/__test__/features/Events/EventDisplay.test.tsx new file mode 100644 index 0000000..59d15c3 --- /dev/null +++ b/__test__/features/Events/EventDisplay.test.tsx @@ -0,0 +1,172 @@ +import { screen, fireEvent, waitFor } from "@testing-library/react"; +import * as eventThunks from "../../../src/features/Calendars/CalendarSlice"; +import { renderWithProviders } from "../../utils/Renderwithproviders"; +import EventPopover from "../../../src/features/Events/EventModal"; +import { DateSelectArg } from "@fullcalendar/core"; +import preview from "jest-preview"; +import { formatDateToYYYYMMDDTHHMMSS } from "../../../src/utils/dateUtils"; +import EventDisplayModal from "../../../src/features/Events/EventDisplay"; + +describe("Event Display", () => { + const mockOnClose = jest.fn(); + const day = new Date(); + const RealDateToLocaleString = Date.prototype.toLocaleString; + + beforeEach(() => { + jest.clearAllMocks(); + }); + + const preloadedState = { + user: { + userData: { + sub: "test", + email: "test@test.com", + sid: "aiYbWZSk2g0F+LrQeD7Dg4QcUMR8R/zTZdZBiA7N6Ro", + openpaasId: "667037022b752d0026472254", + }, + organiserData: { + cn: "test", + cal_address: "mailto:test@test.com", + }, + }, + calendars: { + list: { + "667037022b752d0026472254/cal1": { + id: "667037022b752d0026472254/cal1", + name: "Calendar 1", + color: "#FF0000", + events: { + event1: { + id: "event1", + title: "Test Event", + start: day.toISOString(), + end: day.toISOString(), + }, + }, + }, + "otherCal/cal": { + id: "otherCal/cal", + name: "Calendar 1", + color: "#FF0000", + events: { + event1: { + id: "event1", + title: "Test Event Other cal", + start: day.toISOString(), + end: day.toISOString(), + }, + }, + }, + }, + pending: false, + }, + }; + + it("renders correctly event data", () => { + jest + .spyOn(Date.prototype, "toLocaleString") + .mockImplementation(function ( + this: Date, + locales?: Intl.LocalesArgument, + options?: Intl.DateTimeFormatOptions | undefined + ): string { + return RealDateToLocaleString.call(this, "en-UK", options); + }); + renderWithProviders( + , + preloadedState + ); + const weekday = day.toLocaleString("en-UK", { weekday: "long" }); + const month = day.toLocaleString("en-UK", { month: "long" }); + const dayOfMonth = day.getDate().toString(); + + expect(screen.getByText("Test Event")).toBeInTheDocument(); + expect(screen.getByText(new RegExp(weekday, "i"))).toBeInTheDocument(); + expect(screen.getByText(new RegExp(month, "i"))).toBeInTheDocument(); + expect( + screen.getByText(new RegExp(`\\b${dayOfMonth}\\b`)) + ).toBeInTheDocument(); + + expect(screen.getByText(/\d{2}:\d{2} – \d{2}:\d{2}/)).toBeInTheDocument(); + expect(screen.getByText("Calendar 1")).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 + ); + expect(screen.queryByTestId("DeleteIcon")).not.toBeInTheDocument(); + // Renders the personnal cal event + renderWithProviders( + , + preloadedState + ); + expect(screen.queryByTestId("DeleteIcon")).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("DeleteIcon")); + + await waitFor(() => { + expect(spy).toHaveBeenCalled(); + }); + + const receivedPayload = spy.mock.calls[0][0]; + + expect(receivedPayload).toEqual({ + calId: "667037022b752d0026472254/cal1", + eventId: "event1", + }); + + expect(mockOnClose).toHaveBeenCalledWith({}, "backdropClick"); + }); +}); diff --git a/__test__/features/Events/EventModal.test.tsx b/__test__/features/Events/EventModal.test.tsx index 8f06500..0fb59ab 100644 --- a/__test__/features/Events/EventModal.test.tsx +++ b/__test__/features/Events/EventModal.test.tsx @@ -165,7 +165,6 @@ describe("EventPopover", () => { fireEvent.change(screen.getByLabelText("Location"), { target: { value: newEvent.location }, }); - preview.debug(); const spy = jest .spyOn(eventThunks, "putEventAsync") .mockImplementation((payload) => { diff --git a/src/features/Events/EventDisplay.tsx b/src/features/Events/EventDisplay.tsx index 3c830da..182aac3 100644 --- a/src/features/Events/EventDisplay.tsx +++ b/src/features/Events/EventDisplay.tsx @@ -11,6 +11,8 @@ import { CardContent, Divider, IconButton, + Avatar, + Badge, } from "@mui/material"; import EditIcon from "@mui/icons-material/Edit"; import DeleteIcon from "@mui/icons-material/Delete"; @@ -20,6 +22,9 @@ import LocationOnIcon from "@mui/icons-material/LocationOn"; import VideocamIcon from "@mui/icons-material/Videocam"; import ErrorOutlineIcon from "@mui/icons-material/ErrorOutline"; import CircleIcon from "@mui/icons-material/Circle"; +import CancelIcon from "@mui/icons-material/Cancel"; +import CheckCircleIcon from "@mui/icons-material/CheckCircle"; +import { userAttendee } from "../User/userDataTypes"; function EventDisplayModal({ eventId, @@ -34,44 +39,58 @@ function EventDisplayModal({ open: boolean; onClose: (event: {}, reason: "backdropClick" | "escapeKeyDown") => void; }) { - if (calId && eventId) { - const dispatch = useAppDispatch(); - const calendar = useAppSelector((state) => state.calendars.list[calId]); - const event = useAppSelector( - (state) => state.calendars.list[calId].events[eventId] - ); - const user = useAppSelector((state) => state.user); - const [showAllAttendees, setShowAllAttendees] = useState(false); - const attendeeDisplayLimit = 3; + const dispatch = useAppDispatch(); + const calendar = useAppSelector((state) => state.calendars.list[calId]); + const event = useAppSelector( + (state) => state.calendars.list[calId]?.events[eventId] + ); + const user = useAppSelector((state) => state.user); + const [showAllAttendees, setShowAllAttendees] = useState(false); - useEffect(() => { - if (!event || !calendar) { - onClose({}, "backdropClick"); - } - }, [event, calendar, onClose]); + useEffect(() => { + if (!event || !calendar) { + onClose({}, "backdropClick"); + } + }, [event, calendar, onClose]); - if (!event || !calendar) return null; + if (!event || !calendar) return null; - const visibleAttendees = showAllAttendees - ? event.attendee - : event.attendee.slice(0, attendeeDisplayLimit); - console.log(event); - return ( - - - {/* Top-right buttons */} - - - - + const attendeeDisplayLimit = 3; + + const attendees = + event.attendee?.filter( + (a) => a.cal_address !== event.organizer?.cal_address + ) || []; + + const visibleAttendees = showAllAttendees + ? attendees + : attendees.slice(0, attendeeDisplayLimit); + + const currentUserAttendee = event.attendee?.find( + (person) => person.cal_address === user.userData.email + ); + + const organizer = event.attendee?.find( + (a) => a.cal_address === event.organizer?.cal_address + ); + + return ( + + + {/* Top-right buttons */} + + + + + {calendar.id.split("/")[0] === user.userData.openpaasId && ( { @@ -81,146 +100,240 @@ function EventDisplayModal({ > - onClose({}, "backdropClick")} - > - - + )} + onClose({}, "backdropClick")}> + + + + + + {event.title && ( + + {event.title} + + )} + + {/* Time info*/} + + {formatDate(event.start)} + {event.end && + ` – ${new Date(event.end).toLocaleTimeString(undefined, { + hour: "2-digit", + minute: "2-digit", + })}`} + + + {/* Location */} + {event.location && ( + } + text={event.location} + /> + )} + + {/* Video */} + {event.x_openpass_videoconference && ( + } + text="Video conference available" + /> + )} + + {/* Attendees */} + {event.attendee?.length > 0 && ( + + Attendees: + {organizer && renderAttendeeBadge(organizer, "org", true)} + {visibleAttendees.map((a, idx) => + renderAttendeeBadge(a, idx.toString()) + )} + {attendees.length > attendeeDisplayLimit && ( + setShowAllAttendees(!showAllAttendees)} + > + {showAllAttendees + ? "Show less" + : `Show more (${ + attendees.length - attendeeDisplayLimit + } more)`} + + )} + + )} + + {/* Error */} + {event.error && ( + } + text={event.error} + error + /> + )} + + {/* Calendar color dot */} + + + + {calendar.name} - - {/* Title */} - {event.title && ( - - {event.title} - - )} + - {/* Time info */} - - {formatDate(event.start)} - {event.end && - ` – ${new Date(event.end).toLocaleTimeString(undefined, { - hour: "2-digit", - minute: "2-digit", - })}`} - - - {event.location && ( - - - {event.location} - - )} - - {/* Description */} - {event.description && ( + {/* RSVP */} + {currentUserAttendee && ( + - {event.description} + Will you attend? - )} - - {/* Video conference */} - {event.x_openpass_videoconference && ( - - - - Video conference available - - - )} - - {/* Attendees */} - {/* Attendees with toggle */} - {event.attendee?.length > 0 && ( - - Attendees: - {visibleAttendees.map((a, idx) => ( - - {a.cn} - - ))} - - {event.attendee.length > attendeeDisplayLimit && ( - setShowAllAttendees(!showAllAttendees)} - > - {showAllAttendees - ? "Show less" - : `Show more (${ - event.attendee.length - attendeeDisplayLimit - } more)`} - - )} - - )} - - {/* Error */} - {event.error && ( - - - - {event.error} - - - )} - - {/* Colored dot and calendar icon row */} - - - - {calendar.name} + + + + + + )} - - - {/* Attendance options */} - {event.attendee.find( - (person) => person.cal_address === user.userData.email - ) && ( - - - Will you attend? - - - - - - - - )} - - - - ); - } else { - return <>; - } + {/* Description */} + {event.description && ( + + {event.description} + + )} + + + + ); } -const formatDate = (date: Date) => - new Date(date).toLocaleString(undefined, { +function InfoRow({ + icon, + text, + error = false, +}: { + icon: React.ReactNode; + text: string; + error?: boolean; +}) { + return ( + + {icon} + + {text} + + + ); +} + +function renderAttendeeBadge( + a: userAttendee, + key: string, + isOrganizer?: boolean +) { + const statusIcon = + a.partstat === "ACCEPTED" ? ( + + ) : a.partstat === "DECLINED" ? ( + + ) : null; + + return ( + + + {statusIcon} + + ) + } + > + + + + + {a.cn || a.cal_address} + + {isOrganizer && ( + + Organizer + + )} + + + ); +} + +function formatDate(date: Date) { + return new Date(date).toLocaleString(undefined, { weekday: "long", month: "long", day: "numeric", hour: "2-digit", minute: "2-digit", }); +} + +function stringToColor(string: string) { + let hash = 0; + for (let i = 0; i < string.length; i++) { + hash = string.charCodeAt(i) + ((hash << 5) - hash); + } + + let color = "#"; + for (let i = 0; i < 3; i++) { + const value = (hash >> (i * 8)) & 0xff; + color += `00${value.toString(16)}`.slice(-2); + } + + return color; +} + +function stringAvatar(name: string) { + return { + sx: { width: 24, height: 24, fontSize: 18, bgcolor: stringToColor(name) }, + children: name[0], + }; +} export default EventDisplayModal;