From 8f2277b27866684b2ec85d6fde79fb55cccfcffd Mon Sep 17 00:00:00 2001 From: Camille Moussu Date: Thu, 11 Sep 2025 10:28:54 +0200 Subject: [PATCH 1/5] [#72] added duplicate button and fixed tests broken by new interaction between components --- __test__/components/DuplicateEvent.test.tsx | 366 ++++++++++++++++++ .../features/Events/EventDisplay.test.tsx | 16 +- src/components/Event/EventDuplicate.tsx | 52 +++ src/features/Events/EventDisplay.tsx | 2 + src/features/Events/EventModal.tsx | 42 +- 5 files changed, 450 insertions(+), 28 deletions(-) create mode 100644 __test__/components/DuplicateEvent.test.tsx create mode 100644 src/components/Event/EventDuplicate.tsx diff --git a/__test__/components/DuplicateEvent.test.tsx b/__test__/components/DuplicateEvent.test.tsx new file mode 100644 index 0000000..0a89775 --- /dev/null +++ b/__test__/components/DuplicateEvent.test.tsx @@ -0,0 +1,366 @@ +import { screen, fireEvent, waitFor, act } from "@testing-library/react"; +import userEvent from "@testing-library/user-event"; +import { renderWithProviders } from "../utils/Renderwithproviders"; +import RepeatEvent from "../../src/components/Event/EventRepeat"; +import EventPopover from "../../src/features/Events/EventModal"; +import { RepetitionObject } from "../../src/features/Events/EventsTypes"; +import { DateSelectArg } from "@fullcalendar/core"; +import { formatDateToYYYYMMDDTHHMMSS } from "../../src/utils/dateUtils"; +import * as eventThunks from "../../src/features/Calendars/CalendarSlice"; +import * as apiUtils from "../../src/utils/apiUtils"; + +const baseRepetition: RepetitionObject = { + freq: "", + interval: 1, + occurrences: 0, + endDate: "", + selectedDays: [], +}; + +const mockOnClose = jest.fn(); +const mockSetSelectedRange = jest.fn(); +const mockCalendarRef = { current: { select: jest.fn() } } as any; + +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 1", + color: "#FF0000", + }, + "667037022b752d0026472254/cal2": { + id: "667037022b752d0026472254/cal2", + name: "Calendar 2", + color: "#00FF00", + }, + }, + pending: false, + }, +}; + +const defaultSelectedRange = { + startStr: "2025-07-18T09:00", + endStr: "2025-07-18T10:00", + start: new Date("2025-07-18T09:00"), + end: new Date("2025-07-18T10:00"), + allDay: false, + resource: undefined, +} as unknown as DateSelectArg; + +function setupRepeatEvent(props?: Partial, state?: any) { + const setRepetition = jest.fn(); + renderWithProviders( + , + state + ); + return { setRepetition }; +} + +async function setupEventPopover( + overrides?: Partial<{ start: string; end: string }> +) { + jest + .spyOn(crypto, "randomUUID") + .mockReturnValue("fixed-uuid-with-correct-format"); + const originalDateResolvedOptions = + new Intl.DateTimeFormat().resolvedOptions(); + jest.spyOn(Intl.DateTimeFormat.prototype, "resolvedOptions").mockReturnValue({ + ...originalDateResolvedOptions, + timeZone: "UTC", + }); + + renderWithProviders( + , + preloadedState + ); + act(() => { + fireEvent.change(screen.getByLabelText("Title"), { + target: { value: "Meeting" }, + }); + fireEvent.click(screen.getByLabelText("All day")); + fireEvent.change(screen.getByLabelText("Start"), { + target: { + value: (overrides?.start ?? "2025-07-18T00:00:00.000Z").split("T")[0], + }, + }); + fireEvent.change(screen.getByLabelText("End"), { + target: { + value: (overrides?.end ?? "2025-07-19T00:00:00.000Z").split("T")[0], + }, + }); + fireEvent.click(screen.getByText("Show More")); + }); + const select = screen.getByLabelText(/repetition/i); + userEvent.click(select); + + return jest.spyOn(apiUtils, "api"); +} + +async function expectRRule(expected: any) { + const spyAPi = jest.spyOn(apiUtils, "api"); + + act(() => fireEvent.click(screen.getByText("Save"))); + + await waitFor(() => { + expect(spyAPi).toHaveBeenCalled(); + }); + + const receivedPayload: string = + spyAPi.mock.calls[0][1]?.body?.toString() ?? ""; + const [, , [vevent]] = JSON.parse(receivedPayload); + const rrule = vevent[1].find(([name]: any) => name === "rrule"); + + if (rrule[3].byday) { + expect({ + ...rrule[3], + byday: rrule[3].byday.sort(), + }).toEqual({ + ...expected, + byday: expected.byday.sort(), + }); + } else { + expect(rrule[3]).toEqual(expected); + } +} + +describe("RepeatEvent", () => { + it("renders with no repetition by default", () => { + setupRepeatEvent(); + expect(screen.getByLabelText(/repetition/i)).toBeInTheDocument(); + expect(screen.queryByText(/daily/i)).not.toBeInTheDocument(); + }); + + it("allows selecting repetition frequency", async () => { + const { setRepetition } = setupRepeatEvent(); + const select = screen.getByLabelText(/repetition/i); + act(() => { + userEvent.click(select); + }); + await waitFor(async () => + userEvent.click(await screen.findByText(/repeat weekly/i)) + ); + + expect(setRepetition).toHaveBeenCalledWith( + expect.objectContaining({ freq: "weekly" }) + ); + }); + + it("renders interval input when frequency is selected", () => { + setupRepeatEvent({ freq: "daily", interval: 2 }); + expect(screen.getByText(/interval/i)).toBeInTheDocument(); + expect(screen.getByDisplayValue("2")).toBeInTheDocument(); + }); + + it("updates interval value", () => { + const { setRepetition } = setupRepeatEvent({ freq: "daily", interval: 1 }); + const input = screen.getByDisplayValue("1"); + fireEvent.change(input, { target: { value: "5" } }); + expect(setRepetition).toHaveBeenCalledWith( + expect.objectContaining({ interval: 5 }) + ); + }); + + it("toggles day selection for weekly frequency", () => { + const { setRepetition } = setupRepeatEvent({ + freq: "weekly", + selectedDays: [], + }); + act(() => { + const mondayCheckbox = screen.getByLabelText("MO"); + fireEvent.click(mondayCheckbox); + }); + expect(setRepetition).toHaveBeenCalledWith( + expect.objectContaining({ selectedDays: ["MO"] }) + ); + }); +}); + +describe("Repeat Event API calls", () => { + beforeEach(() => { + jest.clearAllMocks(); + jest.restoreAllMocks(); + }); + + it("sends correct CalendarEvent payload", async () => { + setupEventPopover(); + + userEvent.click(await screen.findByText(/repeat weekly/i)); + + const spy = jest + .spyOn(eventThunks, "putEventAsync") + .mockImplementation((payload) => () => Promise.resolve(payload) as any); + act(() => fireEvent.click(screen.getByText("Save"))); + await waitFor(() => expect(spy).toHaveBeenCalled()); + + const received = spy.mock.calls[0][0]; + expect(received.cal).toEqual( + preloadedState.calendars.list["667037022b752d0026472254/cal1"] + ); + expect(received.newEvent.title).toBe("Meeting"); + expect( + formatDateToYYYYMMDDTHHMMSS(received.newEvent.start).split("T")[0] + ).toBe("20250718"); + expect( + formatDateToYYYYMMDDTHHMMSS(received.newEvent.end || new Date()).split( + "T" + )[0] + ).toBe("20250719"); + expect(received.newEvent.organizer).toEqual( + preloadedState.user.organiserData + ); + const day = new Date(received.newEvent.start) + .toLocaleString("en-UK", { + weekday: "short", + }) + .slice(0, 2) + .toUpperCase(); + + expect(received.newEvent.repetition).toEqual({ + freq: "weekly", + selectedDays: [day], + }); + expect(received.newEvent.color).toEqual( + preloadedState.calendars.list["667037022b752d0026472254/cal1"].color + ); + expect(mockOnClose).toHaveBeenCalledWith({}, "backdropClick"); + }); + + it("sends correct API payload for repeat daily", async () => { + await setupEventPopover(); + + userEvent.click(await screen.findByText(/repeat daily/i)); + + await expectRRule({ freq: "daily" }); + expect(mockOnClose).toHaveBeenCalledWith({}, "backdropClick"); + }); + + it("sends correct API payload for repeat daily with 2 day interval", async () => { + await setupEventPopover(); + + userEvent.click(await screen.findByText(/repeat daily/i)); + const intervalInput = screen.getByDisplayValue("1"); + fireEvent.change(intervalInput, { target: { value: "2" } }); + + await expectRRule({ freq: "daily", interval: 2 }); + expect(mockOnClose).toHaveBeenCalledWith({}, "backdropClick"); + }); + + it("sends correct API payload for repeat daily for 5 repetitions", async () => { + await setupEventPopover(); + + userEvent.click(await screen.findByText(/repeat daily/i)); + userEvent.click(screen.getByLabelText(/after/i)); + const input = screen.getAllByRole("spinbutton")[1]; + fireEvent.change(input, { target: { value: "5" } }); + + await expectRRule({ freq: "daily", count: 5 }); + expect(mockOnClose).toHaveBeenCalledWith({}, "backdropClick"); + }); + + it("sends correct API payload for repeat daily until now+5days", async () => { + await setupEventPopover(); + + userEvent.click(await screen.findByText(/repeat daily/i)); + userEvent.click(screen.getAllByLabelText(/on/i)[3]); + + const untilInput = screen.getByTestId("end-date"); + const futureDate = new Date(); + futureDate.setDate(futureDate.getDate() + 5); + + fireEvent.change(untilInput, { + target: { value: futureDate.toISOString().split("T")[0] }, + }); + await expectRRule({ + freq: "daily", + until: futureDate.toISOString().split("T")[0], + }); + expect(mockOnClose).toHaveBeenCalledWith({}, "backdropClick"); + }); + + it("sends correct API payload for repeat weekly on Thursday and event day (Friday)", async () => { + await setupEventPopover(); + + userEvent.click(await screen.findByText(/repeat weekly/i)); + userEvent.click(screen.getByLabelText("TH")); + + await expectRRule({ freq: "weekly", byday: ["TH", "FR"] }); + expect(mockOnClose).toHaveBeenCalledWith({}, "backdropClick"); + }); + it("sends correct API payload for repeat weekly on Thursday and event day (Friday) and an interval of 3 weeks", async () => { + await setupEventPopover(); + + userEvent.click(await screen.findByText(/repeat weekly/i)); + + userEvent.click(screen.getByLabelText("TH")); + + const intervalInput = screen.getByDisplayValue("1"); + fireEvent.change(intervalInput, { target: { value: "3" } }); + + await expectRRule({ freq: "weekly", byday: ["TH", "FR"], interval: 3 }); + expect(mockOnClose).toHaveBeenCalledWith({}, "backdropClick"); + }); + it("sends correct API payload for repeat monthly", async () => { + await setupEventPopover(); + + userEvent.click(await screen.findByText(/repeat monthly/i)); + + await expectRRule({ freq: "monthly" }); + expect(mockOnClose).toHaveBeenCalledWith({}, "backdropClick"); + }); + it("sends correct API payload for repeat monthly and end after 5 occurrences", async () => { + await setupEventPopover(); + + userEvent.click(await screen.findByText(/repeat monthly/i)); + userEvent.click(screen.getByLabelText(/after/i)); + const input = screen.getAllByRole("spinbutton")[1]; + fireEvent.change(input, { target: { value: "5" } }); + await expectRRule({ freq: "monthly", count: 5 }); + expect(mockOnClose).toHaveBeenCalledWith({}, "backdropClick"); + }); + it("sends correct API payload for repeat yearly", async () => { + await setupEventPopover(); + + userEvent.click(await screen.findByText(/repeat yearly/i)); + + await expectRRule({ freq: "yearly" }); + expect(mockOnClose).toHaveBeenCalledWith({}, "backdropClick"); + }); + + it("sends correct API payload for repeat yearly, but user first choose to end after 5 occurrences then changed mind and chose to not end", async () => { + await setupEventPopover(); + + userEvent.click(await screen.findByText(/repeat yearly/i)); + userEvent.click(screen.getByLabelText(/after/i)); + const input = screen.getAllByRole("spinbutton")[1]; + fireEvent.change(input, { target: { value: "5" } }); + userEvent.click(screen.getByLabelText(/never/i)); + + await expectRRule({ freq: "yearly" }); + expect(mockOnClose).toHaveBeenCalledWith({}, "backdropClick"); + }); +}); diff --git a/__test__/features/Events/EventDisplay.test.tsx b/__test__/features/Events/EventDisplay.test.tsx index 28ded30..0fee032 100644 --- a/__test__/features/Events/EventDisplay.test.tsx +++ b/__test__/features/Events/EventDisplay.test.tsx @@ -1137,13 +1137,7 @@ describe("Event Full Display", () => { const day = new Date(); const preloadedTwoCals = { - user: { - userData: { - sub: "test", - email: "test@test.com", - openpaasId: "667037022b752d0026472254", - }, - }, + ...preloadedState, calendars: { list: { "667037022b752d0026472254/cal1": { @@ -1210,13 +1204,7 @@ describe("Event Full Display", () => { const day = new Date(); const preloadedRecurrence = { - user: { - userData: { - sub: "test", - email: "test@test.com", - openpaasId: "667037022b752d0026472254", - }, - }, + ...preloadedState, calendars: { list: { "667037022b752d0026472254/cal1": { diff --git a/src/components/Event/EventDuplicate.tsx b/src/components/Event/EventDuplicate.tsx new file mode 100644 index 0000000..9e5d453 --- /dev/null +++ b/src/components/Event/EventDuplicate.tsx @@ -0,0 +1,52 @@ +import { IconButton } from "@mui/material"; +import { CalendarEvent } from "../../features/Events/EventsTypes"; +import AddToPhotosIcon from "@mui/icons-material/AddToPhotos"; +import { useRef, useState } from "react"; +import EventPopover from "../../features/Events/EventModal"; +import { CalendarApi, DateSelectArg } from "@fullcalendar/core"; + +export default function EventDuplication({ + onClose, + event, +}: { + onClose: Function; + event: CalendarEvent; +}) { + const [openModal, setOpenModal] = useState(false); + const [selectedRange, setSelectedRange] = useState({ + start: new Date(event.start), + startStr: new Date(event.start).toISOString(), + end: new Date(event.end ?? ""), + endStr: new Date(event.end ?? "").toISOString(), + allDay: event.allday ?? false, + } as DateSelectArg); + const calendarRef = useRef(null); + + const handleClosePopover = () => { + calendarRef.current?.unselect(); + setSelectedRange(null); + setOpenModal(!openModal); + onClose({}, "backdropClick"); + }; + return ( + <> + { + setOpenModal(true); + }} + > + + + + + ); +} diff --git a/src/features/Events/EventDisplay.tsx b/src/features/Events/EventDisplay.tsx index 5bc56ab..b28dd93 100644 --- a/src/features/Events/EventDisplay.tsx +++ b/src/features/Events/EventDisplay.tsx @@ -47,6 +47,7 @@ import { CalendarEvent, RepetitionObject } from "./EventsTypes"; import { isValidUrl } from "../../utils/apiUtils"; import { formatLocalDateTime } from "./EventModal"; import RepeatEvent from "../../components/Event/EventRepeat"; +import EventDuplication from "../../components/Event/EventDuplicate"; export default function EventDisplayModal({ eventId, @@ -243,6 +244,7 @@ export default function EventDisplayModal({ {/* Close button */} + onClose({}, "backdropClick")} diff --git a/src/features/Events/EventModal.tsx b/src/features/Events/EventModal.tsx index 94aa481..92aa3ae 100644 --- a/src/features/Events/EventModal.tsx +++ b/src/features/Events/EventModal.tsx @@ -33,6 +33,7 @@ function EventPopover({ selectedRange, setSelectedRange, calendarRef, + event, }: { anchorEl: HTMLElement | null; open: boolean; @@ -40,6 +41,7 @@ function EventPopover({ selectedRange: DateSelectArg | null; setSelectedRange: Function; calendarRef: React.RefObject; + event?: CalendarEvent; }) { const dispatch = useAppDispatch(); @@ -63,20 +65,32 @@ function EventPopover({ const timezones = TIMEZONES.aliases; const [showMore, setShowMore] = useState(false); - const [title, setTitle] = useState(""); - const [description, setDescription] = useState(""); - const [location, setLocation] = useState(""); - const [start, setStart] = useState(""); - const [end, setEnd] = useState(""); - const [calendarid, setCalendarid] = useState(0); - const [allday, setAllDay] = useState(false); - const [repetition, setRepetition] = useState( - {} as RepetitionObject + const [title, setTitle] = useState(event?.title ?? ""); + const [description, setDescription] = useState(event?.description ?? ""); + const [location, setLocation] = useState(event?.location ?? ""); + const [start, setStart] = useState( + event?.start ? new Date(event.start).toISOString() : "" ); - const [attendees, setAttendees] = useState([]); - const [alarm, setAlarm] = useState(""); - const [eventClass, setEventClass] = useState("PUBLIC"); - const [busy, setBusy] = useState("OPAQUE"); + const [end, setEnd] = useState( + event?.end ? new Date(event.end)?.toISOString() : "" + ); + const [calendarid, setCalendarid] = useState( + event?.calId + ? userPersonnalCalendars.findIndex((e) => e.id === event?.calId) + : 0 + ); + const [allday, setAllDay] = useState(event?.allday ?? false); + const [repetition, setRepetition] = useState( + event?.repetition ?? ({} as RepetitionObject) + ); + const [attendees, setAttendees] = useState( + event?.attendee + ? event.attendee.filter((a) => a.cal_address !== organizer.cal_address) + : [] + ); + const [alarm, setAlarm] = useState(event?.alarm?.trigger ?? ""); + const [eventClass, setEventClass] = useState(event?.class ?? "PUBLIC"); + const [busy, setBusy] = useState(event?.transp ?? "OPAQUE"); const [timezone, setTimezone] = useState( Intl.DateTimeFormat().resolvedOptions().timeZone @@ -157,7 +171,7 @@ function EventPopover({ }} > - + From 1a086e938443e2cb8fa9914897c8a6d3feb65686 Mon Sep 17 00:00:00 2001 From: Camille Moussu Date: Thu, 11 Sep 2025 11:09:21 +0200 Subject: [PATCH 2/5] [#72] added tests --- __test__/components/DuplicateEvent.test.tsx | 460 ++++++-------------- src/features/Events/EventsTypes.ts | 2 +- 2 files changed, 136 insertions(+), 326 deletions(-) diff --git a/__test__/components/DuplicateEvent.test.tsx b/__test__/components/DuplicateEvent.test.tsx index 0a89775..6d25407 100644 --- a/__test__/components/DuplicateEvent.test.tsx +++ b/__test__/components/DuplicateEvent.test.tsx @@ -1,26 +1,11 @@ -import { screen, fireEvent, waitFor, act } from "@testing-library/react"; -import userEvent from "@testing-library/user-event"; -import { renderWithProviders } from "../utils/Renderwithproviders"; -import RepeatEvent from "../../src/components/Event/EventRepeat"; +import { fireEvent, screen } from "@testing-library/react"; +import EventDuplication from "../../src/components/Event/EventDuplicate"; +import EventDisplayModal from "../../src/features/Events/EventDisplay"; import EventPopover from "../../src/features/Events/EventModal"; -import { RepetitionObject } from "../../src/features/Events/EventsTypes"; -import { DateSelectArg } from "@fullcalendar/core"; -import { formatDateToYYYYMMDDTHHMMSS } from "../../src/utils/dateUtils"; -import * as eventThunks from "../../src/features/Calendars/CalendarSlice"; -import * as apiUtils from "../../src/utils/apiUtils"; - -const baseRepetition: RepetitionObject = { - freq: "", - interval: 1, - occurrences: 0, - endDate: "", - selectedDays: [], -}; - -const mockOnClose = jest.fn(); -const mockSetSelectedRange = jest.fn(); -const mockCalendarRef = { current: { select: jest.fn() } } as any; +import { renderWithProviders } from "../utils/Renderwithproviders"; +import preview from "jest-preview"; +const day = new Date(); const preloadedState = { user: { userData: { @@ -31,336 +16,161 @@ const preloadedState = { }, organiserData: { cn: "test", - cal_address: "test@test.com", + cal_address: "mailto:test@test.com", }, }, calendars: { list: { "667037022b752d0026472254/cal1": { id: "667037022b752d0026472254/cal1", - name: "Calendar 1", + name: "Calendar", color: "#FF0000", - }, - "667037022b752d0026472254/cal2": { - id: "667037022b752d0026472254/cal2", - name: "Calendar 2", - color: "#00FF00", + events: { + event1: { + uid: "event1", + URL: "calendars/667037022b752d0026472254/cal1/event1.ics", + title: "Test Event", + calId: "667037022b752d0026472254/cal1", + start: day, + end: day, + timezone: "UTC", + 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", + }, + ], + }, + }, }, }, pending: false, }, }; -const defaultSelectedRange = { - startStr: "2025-07-18T09:00", - endStr: "2025-07-18T10:00", - start: new Date("2025-07-18T09:00"), - end: new Date("2025-07-18T10:00"), - allDay: false, - resource: undefined, -} as unknown as DateSelectArg; - -function setupRepeatEvent(props?: Partial, state?: any) { - const setRepetition = jest.fn(); - renderWithProviders( - , - state - ); - return { setRepetition }; -} - -async function setupEventPopover( - overrides?: Partial<{ start: string; end: string }> -) { - jest - .spyOn(crypto, "randomUUID") - .mockReturnValue("fixed-uuid-with-correct-format"); - const originalDateResolvedOptions = - new Intl.DateTimeFormat().resolvedOptions(); - jest.spyOn(Intl.DateTimeFormat.prototype, "resolvedOptions").mockReturnValue({ - ...originalDateResolvedOptions, - timeZone: "UTC", - }); - - renderWithProviders( - , - preloadedState - ); - act(() => { - fireEvent.change(screen.getByLabelText("Title"), { - target: { value: "Meeting" }, - }); - fireEvent.click(screen.getByLabelText("All day")); - fireEvent.change(screen.getByLabelText("Start"), { - target: { - value: (overrides?.start ?? "2025-07-18T00:00:00.000Z").split("T")[0], - }, - }); - fireEvent.change(screen.getByLabelText("End"), { - target: { - value: (overrides?.end ?? "2025-07-19T00:00:00.000Z").split("T")[0], - }, - }); - fireEvent.click(screen.getByText("Show More")); - }); - const select = screen.getByLabelText(/repetition/i); - userEvent.click(select); - - return jest.spyOn(apiUtils, "api"); -} - -async function expectRRule(expected: any) { - const spyAPi = jest.spyOn(apiUtils, "api"); - - act(() => fireEvent.click(screen.getByText("Save"))); - - await waitFor(() => { - expect(spyAPi).toHaveBeenCalled(); - }); - - const receivedPayload: string = - spyAPi.mock.calls[0][1]?.body?.toString() ?? ""; - const [, , [vevent]] = JSON.parse(receivedPayload); - const rrule = vevent[1].find(([name]: any) => name === "rrule"); - - if (rrule[3].byday) { - expect({ - ...rrule[3], - byday: rrule[3].byday.sort(), - }).toEqual({ - ...expected, - byday: expected.byday.sort(), - }); - } else { - expect(rrule[3]).toEqual(expected); - } -} - -describe("RepeatEvent", () => { - it("renders with no repetition by default", () => { - setupRepeatEvent(); - expect(screen.getByLabelText(/repetition/i)).toBeInTheDocument(); - expect(screen.queryByText(/daily/i)).not.toBeInTheDocument(); - }); - - it("allows selecting repetition frequency", async () => { - const { setRepetition } = setupRepeatEvent(); - const select = screen.getByLabelText(/repetition/i); - act(() => { - userEvent.click(select); - }); - await waitFor(async () => - userEvent.click(await screen.findByText(/repeat weekly/i)) +describe("EventDuplication", () => { + it("opens EventPopover when button clicked", () => { + const handleClose = jest.fn(); + renderWithProviders( + , + preloadedState ); - expect(setRepetition).toHaveBeenCalledWith( - expect.objectContaining({ freq: "weekly" }) - ); + const button = screen.getByRole("button"); + fireEvent.click(button); + + expect(screen.getByText(/Duplicate Event/i)).toBeInTheDocument(); }); - it("renders interval input when frequency is selected", () => { - setupRepeatEvent({ freq: "daily", interval: 2 }); - expect(screen.getByText(/interval/i)).toBeInTheDocument(); - expect(screen.getByDisplayValue("2")).toBeInTheDocument(); - }); - - it("updates interval value", () => { - const { setRepetition } = setupRepeatEvent({ freq: "daily", interval: 1 }); - const input = screen.getByDisplayValue("1"); - fireEvent.change(input, { target: { value: "5" } }); - expect(setRepetition).toHaveBeenCalledWith( - expect.objectContaining({ interval: 5 }) + it("calls onClose when closing the popover", () => { + const handleClose = jest.fn(); + renderWithProviders( + , + preloadedState ); - }); - it("toggles day selection for weekly frequency", () => { - const { setRepetition } = setupRepeatEvent({ - freq: "weekly", - selectedDays: [], - }); - act(() => { - const mondayCheckbox = screen.getByLabelText("MO"); - fireEvent.click(mondayCheckbox); - }); - expect(setRepetition).toHaveBeenCalledWith( - expect.objectContaining({ selectedDays: ["MO"] }) - ); + fireEvent.click(screen.getByRole("button")); + + fireEvent.click(screen.getByRole("button", { name: /Cancel/i })); + + expect(handleClose).toHaveBeenCalled(); }); }); -describe("Repeat Event API calls", () => { - beforeEach(() => { - jest.clearAllMocks(); - jest.restoreAllMocks(); - }); - - it("sends correct CalendarEvent payload", async () => { - setupEventPopover(); - - userEvent.click(await screen.findByText(/repeat weekly/i)); - - const spy = jest - .spyOn(eventThunks, "putEventAsync") - .mockImplementation((payload) => () => Promise.resolve(payload) as any); - act(() => fireEvent.click(screen.getByText("Save"))); - await waitFor(() => expect(spy).toHaveBeenCalled()); - - const received = spy.mock.calls[0][0]; - expect(received.cal).toEqual( - preloadedState.calendars.list["667037022b752d0026472254/cal1"] +describe("EventPopover", () => { + it("renders with event data", () => { + renderWithProviders( + , + preloadedState ); - expect(received.newEvent.title).toBe("Meeting"); - expect( - formatDateToYYYYMMDDTHHMMSS(received.newEvent.start).split("T")[0] - ).toBe("20250718"); - expect( - formatDateToYYYYMMDDTHHMMSS(received.newEvent.end || new Date()).split( - "T" - )[0] - ).toBe("20250719"); - expect(received.newEvent.organizer).toEqual( - preloadedState.user.organiserData + + expect(screen.getByDisplayValue(/Test Event/i)).toBeInTheDocument(); + }); + + it("saves duplicated event when Save is clicked", () => { + const onClose = jest.fn(); + + renderWithProviders( + , + preloadedState ); - const day = new Date(received.newEvent.start) - .toLocaleString("en-UK", { - weekday: "short", - }) - .slice(0, 2) - .toUpperCase(); - expect(received.newEvent.repetition).toEqual({ - freq: "weekly", - selectedDays: [day], + fireEvent.change(screen.getByLabelText(/Title/i), { + target: { value: "Duplicated Event" }, }); - expect(received.newEvent.color).toEqual( - preloadedState.calendars.list["667037022b752d0026472254/cal1"].color - ); - expect(mockOnClose).toHaveBeenCalledWith({}, "backdropClick"); - }); + fireEvent.click(screen.getByRole("button", { name: /Save/i })); - it("sends correct API payload for repeat daily", async () => { - await setupEventPopover(); - - userEvent.click(await screen.findByText(/repeat daily/i)); - - await expectRRule({ freq: "daily" }); - expect(mockOnClose).toHaveBeenCalledWith({}, "backdropClick"); - }); - - it("sends correct API payload for repeat daily with 2 day interval", async () => { - await setupEventPopover(); - - userEvent.click(await screen.findByText(/repeat daily/i)); - const intervalInput = screen.getByDisplayValue("1"); - fireEvent.change(intervalInput, { target: { value: "2" } }); - - await expectRRule({ freq: "daily", interval: 2 }); - expect(mockOnClose).toHaveBeenCalledWith({}, "backdropClick"); - }); - - it("sends correct API payload for repeat daily for 5 repetitions", async () => { - await setupEventPopover(); - - userEvent.click(await screen.findByText(/repeat daily/i)); - userEvent.click(screen.getByLabelText(/after/i)); - const input = screen.getAllByRole("spinbutton")[1]; - fireEvent.change(input, { target: { value: "5" } }); - - await expectRRule({ freq: "daily", count: 5 }); - expect(mockOnClose).toHaveBeenCalledWith({}, "backdropClick"); - }); - - it("sends correct API payload for repeat daily until now+5days", async () => { - await setupEventPopover(); - - userEvent.click(await screen.findByText(/repeat daily/i)); - userEvent.click(screen.getAllByLabelText(/on/i)[3]); - - const untilInput = screen.getByTestId("end-date"); - const futureDate = new Date(); - futureDate.setDate(futureDate.getDate() + 5); - - fireEvent.change(untilInput, { - target: { value: futureDate.toISOString().split("T")[0] }, - }); - await expectRRule({ - freq: "daily", - until: futureDate.toISOString().split("T")[0], - }); - expect(mockOnClose).toHaveBeenCalledWith({}, "backdropClick"); - }); - - it("sends correct API payload for repeat weekly on Thursday and event day (Friday)", async () => { - await setupEventPopover(); - - userEvent.click(await screen.findByText(/repeat weekly/i)); - userEvent.click(screen.getByLabelText("TH")); - - await expectRRule({ freq: "weekly", byday: ["TH", "FR"] }); - expect(mockOnClose).toHaveBeenCalledWith({}, "backdropClick"); - }); - it("sends correct API payload for repeat weekly on Thursday and event day (Friday) and an interval of 3 weeks", async () => { - await setupEventPopover(); - - userEvent.click(await screen.findByText(/repeat weekly/i)); - - userEvent.click(screen.getByLabelText("TH")); - - const intervalInput = screen.getByDisplayValue("1"); - fireEvent.change(intervalInput, { target: { value: "3" } }); - - await expectRRule({ freq: "weekly", byday: ["TH", "FR"], interval: 3 }); - expect(mockOnClose).toHaveBeenCalledWith({}, "backdropClick"); - }); - it("sends correct API payload for repeat monthly", async () => { - await setupEventPopover(); - - userEvent.click(await screen.findByText(/repeat monthly/i)); - - await expectRRule({ freq: "monthly" }); - expect(mockOnClose).toHaveBeenCalledWith({}, "backdropClick"); - }); - it("sends correct API payload for repeat monthly and end after 5 occurrences", async () => { - await setupEventPopover(); - - userEvent.click(await screen.findByText(/repeat monthly/i)); - userEvent.click(screen.getByLabelText(/after/i)); - const input = screen.getAllByRole("spinbutton")[1]; - fireEvent.change(input, { target: { value: "5" } }); - await expectRRule({ freq: "monthly", count: 5 }); - expect(mockOnClose).toHaveBeenCalledWith({}, "backdropClick"); - }); - it("sends correct API payload for repeat yearly", async () => { - await setupEventPopover(); - - userEvent.click(await screen.findByText(/repeat yearly/i)); - - await expectRRule({ freq: "yearly" }); - expect(mockOnClose).toHaveBeenCalledWith({}, "backdropClick"); - }); - - it("sends correct API payload for repeat yearly, but user first choose to end after 5 occurrences then changed mind and chose to not end", async () => { - await setupEventPopover(); - - userEvent.click(await screen.findByText(/repeat yearly/i)); - userEvent.click(screen.getByLabelText(/after/i)); - const input = screen.getAllByRole("spinbutton")[1]; - fireEvent.change(input, { target: { value: "5" } }); - userEvent.click(screen.getByLabelText(/never/i)); - - await expectRRule({ freq: "yearly" }); - expect(mockOnClose).toHaveBeenCalledWith({}, "backdropClick"); + expect(onClose).toHaveBeenCalled(); + }); +}); + +describe("EventDisplayModal", () => { + it("shows duplication button and opens duplication form", () => { + renderWithProviders( + , + preloadedState + ); + + fireEvent.click(screen.getByTestId("AddToPhotosIcon")); + preview.debug(); + expect(screen.getByText(/Duplicate Event/i)).toBeInTheDocument(); + expect( + screen.getAllByDisplayValue( + preloadedState.calendars.list["667037022b752d0026472254/cal1"].events + .event1.title + ) + ).toHaveLength(2); }); }); diff --git a/src/features/Events/EventsTypes.ts b/src/features/Events/EventsTypes.ts index 93df014..94d1ee8 100644 --- a/src/features/Events/EventsTypes.ts +++ b/src/features/Events/EventsTypes.ts @@ -22,7 +22,7 @@ export interface CalendarEvent { status?: string; timezone: string; repetition?: RepetitionObject; - alarm: AlarmObject; + alarm?: AlarmObject; } export interface RepetitionObject { From 48c17b1a0f5d12cb1e31007d576918cce0b57f23 Mon Sep 17 00:00:00 2001 From: Camille Moussu <66134347+Eriikah@users.noreply.github.com> Date: Thu, 11 Sep 2025 11:14:43 +0200 Subject: [PATCH 3/5] [#72] removed debug tools from test --- __test__/components/DuplicateEvent.test.tsx | 2 -- 1 file changed, 2 deletions(-) diff --git a/__test__/components/DuplicateEvent.test.tsx b/__test__/components/DuplicateEvent.test.tsx index 6d25407..820819a 100644 --- a/__test__/components/DuplicateEvent.test.tsx +++ b/__test__/components/DuplicateEvent.test.tsx @@ -3,7 +3,6 @@ import EventDuplication from "../../src/components/Event/EventDuplicate"; import EventDisplayModal from "../../src/features/Events/EventDisplay"; import EventPopover from "../../src/features/Events/EventModal"; import { renderWithProviders } from "../utils/Renderwithproviders"; -import preview from "jest-preview"; const day = new Date(); const preloadedState = { @@ -164,7 +163,6 @@ describe("EventDisplayModal", () => { ); fireEvent.click(screen.getByTestId("AddToPhotosIcon")); - preview.debug(); expect(screen.getByText(/Duplicate Event/i)).toBeInTheDocument(); expect( screen.getAllByDisplayValue( From 7b5e03ac7e578422b1616c4356068d9072eca16e Mon Sep 17 00:00:00 2001 From: Camille Moussu Date: Thu, 11 Sep 2025 15:37:32 +0200 Subject: [PATCH 4/5] [#72] show event duplicator in the event preview --- __test__/components/DuplicateEvent.test.tsx | 8 +++++--- src/features/Events/EventDisplay.tsx | 1 - src/features/Events/EventDisplayPreview.tsx | 2 ++ 3 files changed, 7 insertions(+), 4 deletions(-) diff --git a/__test__/components/DuplicateEvent.test.tsx b/__test__/components/DuplicateEvent.test.tsx index 820819a..32fff1a 100644 --- a/__test__/components/DuplicateEvent.test.tsx +++ b/__test__/components/DuplicateEvent.test.tsx @@ -3,6 +3,7 @@ import EventDuplication from "../../src/components/Event/EventDuplicate"; import EventDisplayModal from "../../src/features/Events/EventDisplay"; import EventPopover from "../../src/features/Events/EventModal"; import { renderWithProviders } from "../utils/Renderwithproviders"; +import EventPreviewModal from "../../src/features/Events/EventDisplayPreview"; const day = new Date(); const preloadedState = { @@ -153,11 +154,12 @@ describe("EventPopover", () => { describe("EventDisplayModal", () => { it("shows duplication button and opens duplication form", () => { renderWithProviders( - , preloadedState ); @@ -165,10 +167,10 @@ describe("EventDisplayModal", () => { fireEvent.click(screen.getByTestId("AddToPhotosIcon")); expect(screen.getByText(/Duplicate Event/i)).toBeInTheDocument(); expect( - screen.getAllByDisplayValue( + screen.getByDisplayValue( preloadedState.calendars.list["667037022b752d0026472254/cal1"].events .event1.title ) - ).toHaveLength(2); + ).toBeInTheDocument(); }); }); diff --git a/src/features/Events/EventDisplay.tsx b/src/features/Events/EventDisplay.tsx index b28dd93..1aa235f 100644 --- a/src/features/Events/EventDisplay.tsx +++ b/src/features/Events/EventDisplay.tsx @@ -244,7 +244,6 @@ export default function EventDisplayModal({ {/* Close button */} - onClose({}, "backdropClick")} diff --git a/src/features/Events/EventDisplayPreview.tsx b/src/features/Events/EventDisplayPreview.tsx index a37e48c..7a58bb0 100644 --- a/src/features/Events/EventDisplayPreview.tsx +++ b/src/features/Events/EventDisplayPreview.tsx @@ -39,6 +39,7 @@ import EventDisplayModal, { } from "./EventDisplay"; import { getEvent } from "./EventApi"; import { CalendarEvent } from "./EventsTypes"; +import EventDuplication from "../../components/Event/EventDuplicate"; export default function EventPreviewModal({ eventId, @@ -122,6 +123,7 @@ export default function EventPreviewModal({ gap: 1, }} > + {mailSpaUrl && attendees.length > 0 && ( Date: Thu, 11 Sep 2025 15:42:35 +0200 Subject: [PATCH 5/5] [#72] removed unused import --- src/features/Events/EventDisplay.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/src/features/Events/EventDisplay.tsx b/src/features/Events/EventDisplay.tsx index 1aa235f..5bc56ab 100644 --- a/src/features/Events/EventDisplay.tsx +++ b/src/features/Events/EventDisplay.tsx @@ -47,7 +47,6 @@ import { CalendarEvent, RepetitionObject } from "./EventsTypes"; import { isValidUrl } from "../../utils/apiUtils"; import { formatLocalDateTime } from "./EventModal"; import RepeatEvent from "../../components/Event/EventRepeat"; -import EventDuplication from "../../components/Event/EventDuplicate"; export default function EventDisplayModal({ eventId,