From 2f071303de3c5477a6e0dd26e8ee3a2df40fca05 Mon Sep 17 00:00:00 2001 From: Camille Moussu Date: Fri, 5 Sep 2025 16:41:05 +0200 Subject: [PATCH] [#77] fixed delete refresh issue and added more tests --- __test__/components/RepeatEvent.test.tsx | 544 +++++++++---------- __test__/features/Events/EventModal.test.tsx | 1 - src/components/Event/EventRepeat.tsx | 1 + src/features/Calendars/CalendarSlice.ts | 15 +- 4 files changed, 287 insertions(+), 274 deletions(-) diff --git a/__test__/components/RepeatEvent.test.tsx b/__test__/components/RepeatEvent.test.tsx index 4c1eafb..065d331 100644 --- a/__test__/components/RepeatEvent.test.tsx +++ b/__test__/components/RepeatEvent.test.tsx @@ -1,63 +1,171 @@ -import { screen, fireEvent, waitFor } from "@testing-library/react"; -import RepeatEvent from "../../src/components/Event/EventRepeat"; -import { RepetitionObject } from "../../src/features/Events/EventsTypes"; +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"); + expect(rrule[3]).toEqual(expected); +} + describe("RepeatEvent", () => { - const baseRepetition: RepetitionObject = { - freq: "", - interval: 1, - occurrences: 0, - endDate: "", - selectedDays: [], - }; - - const setup = (props?: Partial, preloadedState?: any) => { - const setRepetition = jest.fn(); - renderWithProviders( - , - preloadedState - ); - return { setRepetition }; - }; - it("renders with no repetition by default", () => { - setup(); + setupRepeatEvent(); expect(screen.getByLabelText(/repetition/i)).toBeInTheDocument(); expect(screen.queryByText(/daily/i)).not.toBeInTheDocument(); - expect(screen.queryByText(/weekly/i)).not.toBeInTheDocument(); - expect(screen.queryByText(/monthly/i)).not.toBeInTheDocument(); - expect(screen.queryByText(/yearly/i)).not.toBeInTheDocument(); }); it("allows selecting repetition frequency", async () => { - const { setRepetition } = setup(); + const { setRepetition } = setupRepeatEvent(); const select = screen.getByLabelText(/repetition/i); - userEvent.click(select); - userEvent.click(await screen.findByText(/repeat weekly/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", () => { - setup({ freq: "daily", interval: 2 }); + setupRepeatEvent({ freq: "daily", interval: 2 }); expect(screen.getByText(/interval/i)).toBeInTheDocument(); expect(screen.getByDisplayValue("2")).toBeInTheDocument(); }); it("updates interval value", () => { - const { setRepetition } = setup({ freq: "daily", interval: 1 }); + const { setRepetition } = setupRepeatEvent({ freq: "daily", interval: 1 }); const input = screen.getByDisplayValue("1"); fireEvent.change(input, { target: { value: "5" } }); expect(setRepetition).toHaveBeenCalledWith( @@ -66,281 +174,173 @@ describe("RepeatEvent", () => { }); it("toggles day selection for weekly frequency", () => { - const { setRepetition } = setup({ freq: "weekly", selectedDays: [] }); - const mondayCheckbox = screen.getByLabelText("MO"); - userEvent.click(mondayCheckbox); + 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", () => { - 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; +describe("Repeat Event API calls", () => { beforeEach(() => { jest.clearAllMocks(); jest.restoreAllMocks(); }); - it("Given XYZ input in the component, data in CalendarEvent is correct", async () => { - renderWithProviders( - , - preloadedState - ); - const newEvent = { - title: "Meeting", - start: "2025-07-18T00:00:00.000Z", - end: "2025-07-19T00:00:00.000Z", - allday: false, - uid: "6045c603-11ab-43c5-bc30-0641420bb3a8", - organizer: { cn: "test", cal_address: "test@test.com" }, - repetition: { freq: "weekly" }, - }; - // Fill inputs - fireEvent.change(screen.getByLabelText("Title"), { - target: { value: newEvent.title }, - }); - fireEvent.click(screen.getByLabelText("All day")); - fireEvent.change(screen.getByLabelText("Start"), { - target: { value: newEvent.start.split("T")[0] }, - }); - fireEvent.change(screen.getByLabelText("End"), { - target: { value: newEvent.end.split("T")[0] }, - }); + it("sends correct CalendarEvent payload", async () => { + setupEventPopover(); - fireEvent.click(screen.getByText("Show More")); - - const select = screen.getByLabelText(/repetition/i); - userEvent.click(select); userEvent.click(await screen.findByText(/repeat weekly/i)); const spy = jest .spyOn(eventThunks, "putEventAsync") - .mockImplementation((payload) => { - return () => Promise.resolve(payload) as any; - }); + .mockImplementation((payload) => () => Promise.resolve(payload) as any); + act(() => fireEvent.click(screen.getByText("Save"))); + await waitFor(() => expect(spy).toHaveBeenCalled()); - fireEvent.click(screen.getByText("Save")); - - await waitFor(() => { - expect(spy).toHaveBeenCalled(); - }); - - const receivedPayload = spy.mock.calls[0][0]; - expect(receivedPayload.cal).toEqual( + const received = spy.mock.calls[0][0]; + expect(received.cal).toEqual( preloadedState.calendars.list["667037022b752d0026472254/cal1"] ); - - expect(receivedPayload.newEvent.title).toBe(newEvent.title); + expect(received.newEvent.title).toBe("Meeting"); expect( - formatDateToYYYYMMDDTHHMMSS(receivedPayload.newEvent.start).split("T")[0] - ).toBe(formatDateToYYYYMMDDTHHMMSS(new Date(newEvent.start)).split("T")[0]); + formatDateToYYYYMMDDTHHMMSS(received.newEvent.start).split("T")[0] + ).toBe("20250718"); expect( - formatDateToYYYYMMDDTHHMMSS( - receivedPayload.newEvent.end || new Date() - ).split("T")[0] - ).toBe(formatDateToYYYYMMDDTHHMMSS(new Date(newEvent.end)).split("T")[0]); - expect(receivedPayload.newEvent.organizer).toEqual(newEvent.organizer); - expect(receivedPayload.newEvent.repetition).toEqual(newEvent.repetition); - expect(receivedPayload.newEvent.color).toEqual( + formatDateToYYYYMMDDTHHMMSS(received.newEvent.end || new Date()).split( + "T" + )[0] + ).toBe("20250719"); + expect(received.newEvent.organizer).toEqual( + preloadedState.user.organiserData + ); + expect(received.newEvent.repetition).toEqual({ freq: "weekly" }); + expect(received.newEvent.color).toEqual( preloadedState.calendars.list["667037022b752d0026472254/cal1"].color ); - - // onClose should be called expect(mockOnClose).toHaveBeenCalledWith({}, "backdropClick"); }); - it("Given XYZ input in the component, data in api call is correct", async () => { - jest - .spyOn(crypto, "randomUUID") - .mockReturnValue("bfe2c579-acce-456b-8b91-c557bb3b4f89"); - renderWithProviders( - , - preloadedState - ); - const newEvent = { - title: "Meeting", - start: "2025-07-18T00:00:00.000Z", - end: "2025-07-19T00:00:00.000Z", - allday: false, - uid: "6045c603-11ab-43c5-bc30-0641420bb3a8", - organizer: { cn: "test", cal_address: "test@test.com" }, - repetition: { freq: "weekly" }, - }; + it("sends correct API payload for repeat daily", async () => { + await setupEventPopover(); - // Fill inputs - fireEvent.change(screen.getByLabelText("Title"), { - target: { value: newEvent.title }, - }); - fireEvent.click(screen.getByLabelText("All day")); - fireEvent.change(screen.getByLabelText("Start"), { - target: { value: newEvent.start.split("T")[0] }, - }); - fireEvent.change(screen.getByLabelText("End"), { - target: { value: newEvent.end.split("T")[0] }, - }); + userEvent.click(await screen.findByText(/repeat daily/i)); - fireEvent.click(screen.getByText("Show More")); + await expectRRule({ freq: "daily" }); + expect(mockOnClose).toHaveBeenCalledWith({}, "backdropClick"); + }); - const select = screen.getByLabelText(/repetition/i); - userEvent.click(select); - userEvent.click(await screen.findByText(/repeat weekly/i)); - const mondayCheckbox = screen.getByLabelText("MO"); - userEvent.click(mondayCheckbox); + it("sends correct API payload for repeat daily with 2 day interval", async () => { + await setupEventPopover(); - const radio = screen.getByLabelText(/after/i); - userEvent.click(radio); + 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: "3" } }); - const spyAPi = jest.spyOn(apiUtils, "api"); + fireEvent.change(input, { target: { value: "5" } }); - fireEvent.click(screen.getByText("Save")); + await expectRRule({ freq: "daily", count: 5 }); + expect(mockOnClose).toHaveBeenCalledWith({}, "backdropClick"); + }); - await waitFor(() => { - expect(spyAPi).toHaveBeenCalled(); + 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"); + }); - const receivedPayload = spyAPi.mock.calls[0][1]?.body; - expect(receivedPayload).toEqual( - JSON.stringify([ - "vcalendar", - [], - [ - [ - "vevent", - [ - ["uid", {}, "text", "bfe2c579-acce-456b-8b91-c557bb3b4f89"], - ["transp", {}, "text", "OPAQUE"], - ["dtstart", { tzid: "Europe/Paris" }, "date", "2025-07-18"], - ["class", {}, "text", "PUBLIC"], - ["x-openpaas-videoconference", {}, "unknown", null], - ["summary", {}, "text", "Meeting"], - ["dtend", { tzid: "Europe/Paris" }, "date", "2025-07-19"], - [ - "organizer", - { cn: "test" }, - "cal-address", - "mailto:test@test.com", - ], - [ - "rrule", - {}, - "recur", - { freq: "weekly", count: 3, byday: ["MO"] }, - ], - [ - "attendee", - { - partstat: "ACCEPTED", - rsvp: "FALSE", - role: "CHAIR", - cutype: "INDIVIDUAL", - cn: "test", - }, - "cal-address", - "mailto:test@test.com", - ], - ], - [], - ], - [ - "vtimezone", - [["tzid", {}, "text", "Europe/Paris"]], - [ - [ - "daylight", - [ - ["tzoffsetfrom", {}, "utc-offset", "+01:00"], - ["tzoffsetto", {}, "utc-offset", "+02:00"], - ["tzname", {}, "text", "CEST"], - ["dtstart", {}, "date-time", "1970-03-29T02:00:00"], - [ - "rrule", - {}, - "recur", - { freq: "YEARLY", bymonth: 3, byday: "-1SU" }, - ], - ], - [], - ], - [ - "standard", - [ - ["tzoffsetfrom", {}, "utc-offset", "+02:00"], - ["tzoffsetto", {}, "utc-offset", "+01:00"], - ["tzname", {}, "text", "CET"], - ["dtstart", {}, "date-time", "1970-10-25T03:00:00"], - [ - "rrule", - {}, - "recur", - { freq: "YEARLY", bymonth: 10, byday: "-1SU" }, - ], - ], - [], - ], - ], - ], - ], - ]) - ); + it("sends correct API payload for repeat weekly on Thursday and Friday", async () => { + await setupEventPopover(); - // onClose should be called + userEvent.click(await screen.findByText(/repeat weekly/i)); + userEvent.click(screen.getByLabelText("TH")); + userEvent.click(screen.getByLabelText("FR")); + + await expectRRule({ freq: "weekly", byday: ["TH", "FR"] }); + expect(mockOnClose).toHaveBeenCalledWith({}, "backdropClick"); + }); + it("sends correct API payload for repeat weekly on Thursday and Friday and an interval of 3 weeks", async () => { + await setupEventPopover(); + + userEvent.click(await screen.findByText(/repeat weekly/i)); + + userEvent.click(screen.getByLabelText("TH")); + userEvent.click(screen.getByLabelText("FR")); + + 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/EventModal.test.tsx b/__test__/features/Events/EventModal.test.tsx index 9016a34..c8d0db2 100644 --- a/__test__/features/Events/EventModal.test.tsx +++ b/__test__/features/Events/EventModal.test.tsx @@ -286,7 +286,6 @@ describe("EventPopover", () => { preloadedState.calendars.list["667037022b752d0026472254/cal1"] ); - console.log(receivedPayload.newEvent.start); expect(receivedPayload.newEvent.title).toBe(newEvent.title); expect(receivedPayload.newEvent.description).toBe(newEvent.description); expect( diff --git a/src/components/Event/EventRepeat.tsx b/src/components/Event/EventRepeat.tsx index 7e5843e..5771764 100644 --- a/src/components/Event/EventRepeat.tsx +++ b/src/components/Event/EventRepeat.tsx @@ -193,6 +193,7 @@ export default function RepeatEvent({ On diff --git a/src/features/Calendars/CalendarSlice.ts b/src/features/Calendars/CalendarSlice.ts index f36b427..e8273fe 100644 --- a/src/features/Calendars/CalendarSlice.ts +++ b/src/features/Calendars/CalendarSlice.ts @@ -298,7 +298,20 @@ const CalendarSlice = createSlice({ ) .addCase(deleteEventAsync.fulfilled, (state, action) => { state.pending = false; - delete state.list[action.payload.calId].events[action.payload.eventId]; + const [baseId, recurrenceId] = action.payload.eventId.split("/"); + if (recurrenceId) { + Object.keys(state.list[action.payload.calId].events).forEach( + (element) => { + if (element.split("/")[0] === baseId) { + delete state.list[action.payload.calId].events[element]; + } + } + ); + } else { + delete state.list[action.payload.calId].events[ + action.payload.eventId + ]; + } }) .addCase(getCalendarDetailAsync.pending, (state) => { state.pending = true;