refactor: stabilize calendar loading effects and menubar redirect
- Memoize calendar range strings and stabilize effect deps - Guard updateDarkColor to avoid dispatch loops - Add resilient cache-clear handling with refs - Memoize calendar/temp ids to prevent rerenders - Update event handler hooks with missing deps - Add guards and dependency fixes across Calendar effects
This commit is contained in:
committed by
Benoit TELLIER
parent
990e290066
commit
ffe204d60b
@@ -581,4 +581,188 @@ describe("calendar Availability search", () => {
|
||||
expect(startDate.getTime()).toBeLessThanOrEqual(viewStart.getTime());
|
||||
expect(endDate.getTime()).toBeGreaterThanOrEqual(viewEnd.getTime());
|
||||
});
|
||||
|
||||
describe("Batch loading and prefetching", () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
it("loads active calendars in batches of 5", async () => {
|
||||
const manyCalendars = Array.from({ length: 12 }, (_, i) => ({
|
||||
[`user1/cal${i + 1}`]: {
|
||||
name: `Calendar ${i + 1}`,
|
||||
id: `user1/cal${i + 1}`,
|
||||
color: { light: "#FF0000", dark: "#000" },
|
||||
events: {},
|
||||
},
|
||||
})).reduce((acc, cal) => ({ ...acc, ...cal }), {});
|
||||
|
||||
const stateWithManyCalendars = {
|
||||
...preloadedState,
|
||||
calendars: {
|
||||
...preloadedState.calendars,
|
||||
list: manyCalendars,
|
||||
},
|
||||
};
|
||||
|
||||
const spy = jest
|
||||
.spyOn(calendarThunks, "getCalendarDetailAsync")
|
||||
.mockImplementation(() => ({
|
||||
type: "getCalendarDetailAsync",
|
||||
unwrap: () => Promise.resolve({}),
|
||||
})) as any;
|
||||
|
||||
await act(async () => {
|
||||
renderWithProviders(
|
||||
<CalendarApp calendarRef={{ current: null }} />,
|
||||
stateWithManyCalendars
|
||||
);
|
||||
});
|
||||
|
||||
await waitFor(
|
||||
() => {
|
||||
expect(spy).toHaveBeenCalled();
|
||||
},
|
||||
{ timeout: 3000 }
|
||||
);
|
||||
|
||||
const callCount = spy.mock.calls.length;
|
||||
expect(callCount).toBeGreaterThan(0);
|
||||
expect(callCount).toBeLessThanOrEqual(24);
|
||||
});
|
||||
|
||||
it("prefetches hidden calendars only after active load completes", async () => {
|
||||
const calendarsWithSelected = {
|
||||
...preloadedState,
|
||||
calendars: {
|
||||
...preloadedState.calendars,
|
||||
list: {
|
||||
"user1/cal1": {
|
||||
name: "Selected Calendar",
|
||||
id: "user1/cal1",
|
||||
color: { light: "#FF0000", dark: "#000" },
|
||||
events: {},
|
||||
},
|
||||
"user1/cal2": {
|
||||
name: "Hidden Calendar 1",
|
||||
id: "user1/cal2",
|
||||
color: { light: "#00FF00", dark: "#000" },
|
||||
events: {},
|
||||
},
|
||||
"user1/cal3": {
|
||||
name: "Hidden Calendar 2",
|
||||
id: "user1/cal3",
|
||||
color: { light: "#0000FF", dark: "#000" },
|
||||
events: {},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const spy = jest
|
||||
.spyOn(calendarThunks, "getCalendarDetailAsync")
|
||||
.mockImplementation(() => ({
|
||||
type: "getCalendarDetailAsync",
|
||||
unwrap: () => Promise.resolve({}),
|
||||
})) as any;
|
||||
|
||||
await act(async () => {
|
||||
renderWithProviders(
|
||||
<CalendarApp calendarRef={{ current: null }} />,
|
||||
calendarsWithSelected
|
||||
);
|
||||
});
|
||||
|
||||
await waitFor(
|
||||
() => {
|
||||
expect(spy).toHaveBeenCalled();
|
||||
},
|
||||
{ timeout: 3000 }
|
||||
);
|
||||
|
||||
const selectedCalls = spy.mock.calls.filter(
|
||||
(call) => call[0].calId === "user1/cal1"
|
||||
);
|
||||
const hiddenCalls = spy.mock.calls.filter(
|
||||
(call) =>
|
||||
call[0].calId === "user1/cal2" || call[0].calId === "user1/cal3"
|
||||
);
|
||||
|
||||
expect(selectedCalls.length).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
it("does not make duplicate API calls for same calendar and range", async () => {
|
||||
const spy = jest
|
||||
.spyOn(calendarThunks, "getCalendarDetailAsync")
|
||||
.mockImplementation(() => ({
|
||||
type: "getCalendarDetailAsync",
|
||||
unwrap: () => Promise.resolve({}),
|
||||
})) as any;
|
||||
|
||||
await act(async () => {
|
||||
renderWithProviders(
|
||||
<CalendarApp calendarRef={{ current: null }} />,
|
||||
preloadedState
|
||||
);
|
||||
});
|
||||
|
||||
await waitFor(
|
||||
() => {
|
||||
expect(spy).toHaveBeenCalled();
|
||||
},
|
||||
{ timeout: 3000 }
|
||||
);
|
||||
|
||||
const callsForCal1 = spy.mock.calls.filter(
|
||||
(call) => call[0].calId === "user1/cal1"
|
||||
);
|
||||
|
||||
const uniqueRanges = new Set(
|
||||
callsForCal1.map(
|
||||
(call) => `${call[0].match.start}_${call[0].match.end}`
|
||||
)
|
||||
);
|
||||
|
||||
expect(uniqueRanges.size).toBeLessThanOrEqual(callsForCal1.length);
|
||||
});
|
||||
|
||||
it("handles undefined calendars gracefully", async () => {
|
||||
const stateWithUndefinedCalendars = {
|
||||
...preloadedState,
|
||||
calendars: {
|
||||
list: undefined as any,
|
||||
templist: undefined as any,
|
||||
pending: false,
|
||||
},
|
||||
};
|
||||
|
||||
await act(async () => {
|
||||
renderWithProviders(
|
||||
<CalendarApp calendarRef={{ current: null }} />,
|
||||
stateWithUndefinedCalendars
|
||||
);
|
||||
});
|
||||
|
||||
expect(screen.getByText("calendar.personal")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("handles undefined tempcalendars gracefully", async () => {
|
||||
const stateWithUndefinedTemp = {
|
||||
...preloadedState,
|
||||
calendars: {
|
||||
...preloadedState.calendars,
|
||||
templist: undefined as any,
|
||||
},
|
||||
};
|
||||
|
||||
await act(async () => {
|
||||
renderWithProviders(
|
||||
<CalendarApp calendarRef={{ current: null }} />,
|
||||
stateWithUndefinedTemp
|
||||
);
|
||||
});
|
||||
|
||||
expect(screen.getByText("calendar.personal")).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -140,6 +140,203 @@ describe("CalendarSlice", () => {
|
||||
expect(state.list).toEqual({});
|
||||
});
|
||||
|
||||
it("getCalendarsListAsync loads user details in parallel for multiple owners", async () => {
|
||||
const mockCalendars = [
|
||||
{
|
||||
_links: { self: { href: "/calendars/u1/cal1.json" } },
|
||||
"dav:name": "Calendar 1",
|
||||
"apple:color": "#FF0000",
|
||||
acl: [],
|
||||
},
|
||||
{
|
||||
_links: { self: { href: "/calendars/u2/cal2.json" } },
|
||||
"dav:name": "Calendar 2",
|
||||
"apple:color": "#00FF00",
|
||||
acl: [],
|
||||
},
|
||||
{
|
||||
_links: { self: { href: "/calendars/u3/cal3.json" } },
|
||||
"dav:name": "Calendar 3",
|
||||
"apple:color": "#0000FF",
|
||||
acl: [],
|
||||
},
|
||||
];
|
||||
|
||||
(userAPI.getOpenPaasUser as jest.Mock).mockResolvedValue({ id: "u1" });
|
||||
(calAPI.getCalendars as jest.Mock).mockResolvedValue({
|
||||
_embedded: { "dav:calendar": mockCalendars },
|
||||
});
|
||||
|
||||
const getUserDetailsMock = userAPI.getUserDetails as jest.Mock;
|
||||
getUserDetailsMock
|
||||
.mockResolvedValueOnce({
|
||||
firstname: "Alice",
|
||||
lastname: "Smith",
|
||||
emails: ["alice@example.com"],
|
||||
})
|
||||
.mockResolvedValueOnce({
|
||||
firstname: "Bob",
|
||||
lastname: "Jones",
|
||||
emails: ["bob@example.com"],
|
||||
})
|
||||
.mockResolvedValueOnce({
|
||||
firstname: "Charlie",
|
||||
lastname: "Brown",
|
||||
emails: ["charlie@example.com"],
|
||||
});
|
||||
|
||||
const store = storeFactory();
|
||||
await store.dispatch(getCalendarsListAsync() as any);
|
||||
|
||||
expect(getUserDetailsMock).toHaveBeenCalledTimes(3);
|
||||
expect(getUserDetailsMock).toHaveBeenCalledWith("u1");
|
||||
expect(getUserDetailsMock).toHaveBeenCalledWith("u2");
|
||||
expect(getUserDetailsMock).toHaveBeenCalledWith("u3");
|
||||
|
||||
const state = store.getState().calendars;
|
||||
expect(state.list["u1/cal1"].owner).toContain("Alice");
|
||||
expect(state.list["u2/cal2"].owner).toContain("Bob");
|
||||
expect(state.list["u3/cal3"].owner).toContain("Charlie");
|
||||
});
|
||||
|
||||
it("getCalendarsListAsync deduplicates getUserDetails calls for same ownerId", async () => {
|
||||
const mockCalendars = [
|
||||
{
|
||||
_links: { self: { href: "/calendars/u1/cal1.json" } },
|
||||
"dav:name": "Calendar 1",
|
||||
"apple:color": "#FF0000",
|
||||
acl: [],
|
||||
},
|
||||
{
|
||||
_links: { self: { href: "/calendars/u1/cal2.json" } },
|
||||
"dav:name": "Calendar 2",
|
||||
"apple:color": "#00FF00",
|
||||
acl: [],
|
||||
},
|
||||
{
|
||||
_links: { self: { href: "/calendars/u1/cal3.json" } },
|
||||
"dav:name": "Calendar 3",
|
||||
"apple:color": "#0000FF",
|
||||
acl: [],
|
||||
},
|
||||
];
|
||||
|
||||
(userAPI.getOpenPaasUser as jest.Mock).mockResolvedValue({ id: "u1" });
|
||||
(calAPI.getCalendars as jest.Mock).mockResolvedValue({
|
||||
_embedded: { "dav:calendar": mockCalendars },
|
||||
});
|
||||
|
||||
const getUserDetailsMock = userAPI.getUserDetails as jest.Mock;
|
||||
getUserDetailsMock.mockResolvedValue({
|
||||
firstname: "Alice",
|
||||
lastname: "Smith",
|
||||
emails: ["alice@example.com"],
|
||||
});
|
||||
|
||||
const store = storeFactory();
|
||||
await store.dispatch(getCalendarsListAsync() as any);
|
||||
|
||||
expect(getUserDetailsMock).toHaveBeenCalledTimes(1);
|
||||
expect(getUserDetailsMock).toHaveBeenCalledWith("u1");
|
||||
});
|
||||
|
||||
it("getCalendarsListAsync processes owners in batches of 20", async () => {
|
||||
const mockCalendars = Array.from({ length: 45 }, (_, i) => ({
|
||||
_links: { self: { href: `/calendars/u${i + 1}/cal${i + 1}.json` } },
|
||||
"dav:name": `Calendar ${i + 1}`,
|
||||
"apple:color": "#FF0000",
|
||||
acl: [],
|
||||
}));
|
||||
|
||||
(userAPI.getOpenPaasUser as jest.Mock).mockResolvedValue({ id: "u1" });
|
||||
(calAPI.getCalendars as jest.Mock).mockResolvedValue({
|
||||
_embedded: { "dav:calendar": mockCalendars },
|
||||
});
|
||||
|
||||
const getUserDetailsMock = userAPI.getUserDetails as jest.Mock;
|
||||
getUserDetailsMock.mockImplementation((ownerId: string) =>
|
||||
Promise.resolve({
|
||||
firstname: "User",
|
||||
lastname: ownerId,
|
||||
emails: [`${ownerId}@example.com`],
|
||||
})
|
||||
);
|
||||
|
||||
const store = storeFactory();
|
||||
await store.dispatch(getCalendarsListAsync() as any);
|
||||
|
||||
expect(getUserDetailsMock).toHaveBeenCalledTimes(45);
|
||||
const state = store.getState().calendars;
|
||||
expect(Object.keys(state.list)).toHaveLength(45);
|
||||
});
|
||||
|
||||
it("getCalendarsListAsync returns early if calendars already exist in store", async () => {
|
||||
const existingCalendars = {
|
||||
"u1/cal1": {
|
||||
id: "u1/cal1",
|
||||
name: "Existing Calendar",
|
||||
events: {},
|
||||
} as Calendars,
|
||||
};
|
||||
|
||||
const store = storeFactory();
|
||||
store.dispatch({
|
||||
type: "calendars/getCalendars/fulfilled",
|
||||
payload: { importedCalendars: existingCalendars, errors: "" },
|
||||
});
|
||||
|
||||
const getUserDetailsMock = userAPI.getUserDetails as jest.Mock;
|
||||
const getCalendarsMock = calAPI.getCalendars as jest.Mock;
|
||||
|
||||
await store.dispatch(getCalendarsListAsync() as any);
|
||||
|
||||
expect(getCalendarsMock).not.toHaveBeenCalled();
|
||||
expect(getUserDetailsMock).not.toHaveBeenCalled();
|
||||
|
||||
const state = store.getState().calendars;
|
||||
expect(state.list).toEqual(existingCalendars);
|
||||
});
|
||||
|
||||
it("getCalendarsListAsync handles errors in getUserDetails gracefully", async () => {
|
||||
const mockCalendars = [
|
||||
{
|
||||
_links: { self: { href: "/calendars/u1/cal1.json" } },
|
||||
"dav:name": "Calendar 1",
|
||||
"apple:color": "#FF0000",
|
||||
acl: [],
|
||||
},
|
||||
{
|
||||
_links: { self: { href: "/calendars/u2/cal2.json" } },
|
||||
"dav:name": "Calendar 2",
|
||||
"apple:color": "#00FF00",
|
||||
acl: [],
|
||||
},
|
||||
];
|
||||
|
||||
(userAPI.getOpenPaasUser as jest.Mock).mockResolvedValue({ id: "u1" });
|
||||
(calAPI.getCalendars as jest.Mock).mockResolvedValue({
|
||||
_embedded: { "dav:calendar": mockCalendars },
|
||||
});
|
||||
|
||||
const getUserDetailsMock = userAPI.getUserDetails as jest.Mock;
|
||||
getUserDetailsMock
|
||||
.mockResolvedValueOnce({
|
||||
firstname: "Alice",
|
||||
lastname: "Smith",
|
||||
emails: ["alice@example.com"],
|
||||
})
|
||||
.mockRejectedValueOnce(new Error("Failed to fetch user"));
|
||||
|
||||
const store = storeFactory();
|
||||
const result = await store.dispatch(getCalendarsListAsync() as any);
|
||||
|
||||
expect(getUserDetailsMock).toHaveBeenCalledTimes(2);
|
||||
const state = store.getState().calendars;
|
||||
expect(state.list["u1/cal1"].owner).toContain("Alice");
|
||||
expect(state.list["u2/cal2"].owner).toContain("Unknown User");
|
||||
expect(result.payload.errors).toBeTruthy();
|
||||
});
|
||||
|
||||
it("patchCalendarAsync.fulfilled updates calendar fields", () => {
|
||||
const calId = "c1";
|
||||
const prev = {
|
||||
|
||||
Reference in New Issue
Block a user