Skip to content

Commit 1dc20e3

Browse files
Ryukemeisterdevin-ai-integration[bot]emrysal
authored
refactor: usePrefetch hook (calcom#24123)
* init: refactor usePrefetch hook to make it readable * fix: move over logic to determine prefetch next month into its own function * update parameters * fix: better naming * fix: type check * test: add comprehensive test coverage for usePrefetch utility functions - Add tests for areDifferentValidMonths: validates month comparison logic with edge cases - Add tests for isLastWeekOfMonth: covers week boundaries and date edge cases - Add tests for isMonthViewPrefetchEnabled: tests time-based prefetch logic with mocked time - Add tests for getPrefetchMonthCount: validates conditional month count logic for different layouts - Add tests for isPrefetchNextMonthEnabled: integration tests for orchestration function All tests use vitest framework with 51 test cases covering key scenarios and edge cases. Tests follow existing patterns from Cal.com codebase and use integration approach. Co-Authored-By: rajiv@cal.com <sahalrajiv6900@gmail.com> * fix: implement coderabbit feedback * fix: tests * fix: use isoWeek plugin from day to maintain consistency * fix: type errors * fix: dont use isoWeek from dayjs * revert isoWeek import for dayjs * add helper function * fix: bring back the orginal logic we had in usePrefetch but in a simplified way * remove unused logic * test: update tests for usePrefetch refactoring - Add tests for new isMonthChange helper function - Update isPrefetchNextMonthEnabled tests to match new signature - Tests now use pre-calculated month values instead of date strings All 39 tests passing locally Co-Authored-By: rajiv@cal.com <sahalrajiv6900@gmail.com> --------- Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Co-authored-by: Alex van Andel <me@alexvanandel.com>
1 parent 44a3a9e commit 1dc20e3

11 files changed

Lines changed: 573 additions & 28 deletions

packages/features/bookings/Booker/components/hooks/usePrefetch.ts

Lines changed: 18 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
import dayjs from "@calcom/dayjs";
22
import type { BookerState } from "@calcom/features/bookings/Booker/types";
3-
import { BookerLayouts } from "@calcom/prisma/zod-utils";
3+
4+
import { getPrefetchMonthCount } from "../../utils/getPrefetchMonthCount";
5+
import { isPrefetchNextMonthEnabled } from "../../utils/isPrefetchNextMonthEnabled";
46

57
interface UsePrefetchParams {
68
date: string;
@@ -21,33 +23,21 @@ export const usePrefetch = ({ date, month, bookerLayout, bookerState }: UsePrefe
2123
.add(bookerLayout.columnViewExtraDays.current, "day")
2224
.month();
2325

24-
const isValidDate = dayjs(date).isValid();
25-
const twoWeeksAfter = dayjs(month).startOf("month").add(2, "week");
26-
const isSameMonth = dayjs().isSame(dayjs(month), "month");
27-
const isAfter2Weeks = dayjs().isAfter(twoWeeksAfter);
28-
29-
const prefetchNextMonth =
30-
(bookerLayout.layout === BookerLayouts.WEEK_VIEW &&
31-
!!bookerLayout.extraDays &&
32-
!isNaN(dateMonth) &&
33-
!isNaN(monthAfterAddingExtraDays) &&
34-
dateMonth !== monthAfterAddingExtraDays) ||
35-
(bookerLayout.layout === BookerLayouts.COLUMN_VIEW &&
36-
!isNaN(dateMonth) &&
37-
!isNaN(monthAfterAddingExtraDaysColumnView) &&
38-
dateMonth !== monthAfterAddingExtraDaysColumnView) ||
39-
((bookerLayout.layout === BookerLayouts.MONTH_VIEW || bookerLayout.layout === "mobile") &&
40-
(!isValidDate || isSameMonth) &&
41-
isAfter2Weeks);
42-
43-
const monthCount =
44-
((bookerLayout.layout !== BookerLayouts.WEEK_VIEW && bookerState === "selecting_time") ||
45-
bookerLayout.layout === BookerLayouts.COLUMN_VIEW) &&
46-
!isNaN(monthAfterAdding1Month) &&
47-
!isNaN(monthAfterAddingExtraDaysColumnView) &&
48-
monthAfterAdding1Month !== monthAfterAddingExtraDaysColumnView
49-
? 2
50-
: undefined;
26+
const prefetchNextMonth = isPrefetchNextMonthEnabled(
27+
bookerLayout.layout,
28+
date,
29+
dateMonth,
30+
monthAfterAddingExtraDays,
31+
monthAfterAddingExtraDaysColumnView,
32+
month,
33+
bookerLayout.extraDays
34+
);
35+
const monthCount = getPrefetchMonthCount(
36+
bookerLayout.layout,
37+
bookerState,
38+
monthAfterAdding1Month,
39+
monthAfterAddingExtraDaysColumnView
40+
);
5141

5242
return {
5343
prefetchNextMonth,
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
import { describe, expect, it } from "vitest";
2+
3+
import { areDifferentValidMonths } from "./areDifferentValidMonths";
4+
5+
describe("areDifferentValidMonths", () => {
6+
describe("valid different months", () => {
7+
it("should return true for different valid month numbers", () => {
8+
expect(areDifferentValidMonths(0, 1)).toBe(true);
9+
expect(areDifferentValidMonths(5, 10)).toBe(true);
10+
expect(areDifferentValidMonths(11, 0)).toBe(true);
11+
});
12+
});
13+
14+
describe("same month numbers", () => {
15+
it("should return false for identical month numbers", () => {
16+
expect(areDifferentValidMonths(0, 0)).toBe(false);
17+
expect(areDifferentValidMonths(6, 6)).toBe(false);
18+
expect(areDifferentValidMonths(11, 11)).toBe(false);
19+
});
20+
});
21+
22+
describe("invalid inputs", () => {
23+
it("should return false for NaN values", () => {
24+
expect(areDifferentValidMonths(NaN, 5)).toBe(false);
25+
expect(areDifferentValidMonths(5, NaN)).toBe(false);
26+
expect(areDifferentValidMonths(NaN, NaN)).toBe(false);
27+
});
28+
29+
it("should return false for Infinity values", () => {
30+
expect(areDifferentValidMonths(Infinity, 5)).toBe(false);
31+
expect(areDifferentValidMonths(5, Infinity)).toBe(false);
32+
expect(areDifferentValidMonths(Infinity, Infinity)).toBe(false);
33+
});
34+
35+
it("should return false for -Infinity values", () => {
36+
expect(areDifferentValidMonths(-Infinity, 5)).toBe(false);
37+
expect(areDifferentValidMonths(5, -Infinity)).toBe(false);
38+
expect(areDifferentValidMonths(-Infinity, -Infinity)).toBe(false);
39+
});
40+
});
41+
42+
describe("edge cases", () => {
43+
it("should handle negative month numbers correctly", () => {
44+
expect(areDifferentValidMonths(-1, 5)).toBe(true);
45+
expect(areDifferentValidMonths(-1, -1)).toBe(false);
46+
});
47+
48+
it("should handle month numbers beyond 11", () => {
49+
expect(areDifferentValidMonths(12, 5)).toBe(true);
50+
expect(areDifferentValidMonths(100, 200)).toBe(true);
51+
});
52+
});
53+
});
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
export const areDifferentValidMonths = (firstMonth: number, secondMonth: number) => {
2+
const isFirstMonthValid = Number.isFinite(firstMonth);
3+
const isSecondMonthValid = Number.isFinite(secondMonth);
4+
5+
if (!isFirstMonthValid || !isSecondMonthValid) {
6+
return false;
7+
}
8+
9+
if (firstMonth === secondMonth) {
10+
return false;
11+
}
12+
13+
return true;
14+
};
Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
import { describe, expect, it } from "vitest";
2+
3+
import { BookerLayouts } from "@calcom/prisma/zod-utils";
4+
5+
import { getPrefetchMonthCount } from "./getPrefetchMonthCount";
6+
7+
describe("getPrefetchMonthCount", () => {
8+
describe("COLUMN_VIEW layout", () => {
9+
it("should return 2 when months are different", () => {
10+
const result = getPrefetchMonthCount(BookerLayouts.COLUMN_VIEW, "selecting_date", 1, 2);
11+
expect(result).toBe(2);
12+
});
13+
14+
it("should return undefined when months are the same", () => {
15+
const result = getPrefetchMonthCount(BookerLayouts.COLUMN_VIEW, "selecting_date", 5, 5);
16+
expect(result).toBe(undefined);
17+
});
18+
19+
it("should return 2 regardless of bookerState when months are different", () => {
20+
expect(getPrefetchMonthCount(BookerLayouts.COLUMN_VIEW, "selecting_time", 3, 4)).toBe(2);
21+
expect(getPrefetchMonthCount(BookerLayouts.COLUMN_VIEW, "booking", 7, 8)).toBe(2);
22+
});
23+
});
24+
25+
describe("WEEK_VIEW layout", () => {
26+
it("should return undefined when state is selecting_time", () => {
27+
const result = getPrefetchMonthCount(BookerLayouts.WEEK_VIEW, "selecting_time", 1, 2);
28+
expect(result).toBe(undefined);
29+
});
30+
31+
it("should return undefined when state is not selecting_time", () => {
32+
const result = getPrefetchMonthCount(BookerLayouts.WEEK_VIEW, "selecting_date", 1, 2);
33+
expect(result).toBe(undefined);
34+
});
35+
36+
it("should return undefined even with different months", () => {
37+
const result = getPrefetchMonthCount(BookerLayouts.WEEK_VIEW, "booking", 0, 11);
38+
expect(result).toBe(undefined);
39+
});
40+
});
41+
42+
describe("MONTH_VIEW layout with selecting_time state", () => {
43+
it("should return 2 when months are different", () => {
44+
const result = getPrefetchMonthCount(BookerLayouts.MONTH_VIEW, "selecting_time", 3, 4);
45+
expect(result).toBe(2);
46+
});
47+
48+
it("should return undefined when months are the same", () => {
49+
const result = getPrefetchMonthCount(BookerLayouts.MONTH_VIEW, "selecting_time", 6, 6);
50+
expect(result).toBe(undefined);
51+
});
52+
});
53+
54+
describe("MONTH_VIEW layout with other states", () => {
55+
it("should return undefined when state is selecting_date", () => {
56+
const result = getPrefetchMonthCount(BookerLayouts.MONTH_VIEW, "selecting_date", 1, 2);
57+
expect(result).toBe(undefined);
58+
});
59+
60+
it("should return undefined when state is booking", () => {
61+
const result = getPrefetchMonthCount(BookerLayouts.MONTH_VIEW, "booking", 1, 2);
62+
expect(result).toBe(undefined);
63+
});
64+
});
65+
66+
describe("invalid month inputs", () => {
67+
it("should return undefined when first month is NaN", () => {
68+
const result = getPrefetchMonthCount(BookerLayouts.COLUMN_VIEW, "selecting_date", NaN, 5);
69+
expect(result).toBe(undefined);
70+
});
71+
72+
it("should return undefined when second month is NaN", () => {
73+
const result = getPrefetchMonthCount(BookerLayouts.COLUMN_VIEW, "selecting_date", 5, NaN);
74+
expect(result).toBe(undefined);
75+
});
76+
77+
it("should return undefined when both months are invalid", () => {
78+
const result = getPrefetchMonthCount(BookerLayouts.COLUMN_VIEW, "selecting_time", Infinity, -Infinity);
79+
expect(result).toBe(undefined);
80+
});
81+
});
82+
83+
describe("edge cases", () => {
84+
it("should handle month transitions (11 to 0)", () => {
85+
const result = getPrefetchMonthCount(BookerLayouts.COLUMN_VIEW, "selecting_date", 11, 0);
86+
expect(result).toBe(2);
87+
});
88+
89+
it("should handle negative month numbers if they are different", () => {
90+
const result = getPrefetchMonthCount(BookerLayouts.COLUMN_VIEW, "selecting_date", -1, 5);
91+
expect(result).toBe(2);
92+
});
93+
});
94+
});
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
import { BookerLayouts } from "@calcom/prisma/zod-utils";
2+
3+
import type { BookerState } from "../types";
4+
import { areDifferentValidMonths } from "./areDifferentValidMonths";
5+
6+
export const getPrefetchMonthCount = (
7+
bookerLayout: string,
8+
bookerState: BookerState,
9+
firstMonth: number,
10+
secondMonth: number
11+
) => {
12+
const isDifferentMonth = areDifferentValidMonths(firstMonth, secondMonth);
13+
14+
const isWeekView = bookerLayout === BookerLayouts.WEEK_VIEW;
15+
const isColumnView = bookerLayout === BookerLayouts.COLUMN_VIEW;
16+
const isSelectingTime = bookerState === "selecting_time";
17+
18+
if (!isDifferentMonth) return undefined;
19+
20+
if (isColumnView || (!isWeekView && isSelectingTime)) return 2;
21+
22+
return undefined;
23+
};
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
import { describe, expect, it } from "vitest";
2+
3+
import { isMonthChange } from "./isMonthChange";
4+
5+
describe("isMonthChange", () => {
6+
describe("when months are different", () => {
7+
it("should return true for consecutive months", () => {
8+
expect(isMonthChange(0, 1)).toBe(true);
9+
expect(isMonthChange(5, 6)).toBe(true);
10+
expect(isMonthChange(10, 11)).toBe(true);
11+
});
12+
13+
it("should return true for non-consecutive months", () => {
14+
expect(isMonthChange(0, 11)).toBe(true);
15+
expect(isMonthChange(3, 9)).toBe(true);
16+
});
17+
18+
it("should return true for year transition (11 to 0)", () => {
19+
expect(isMonthChange(11, 0)).toBe(true);
20+
});
21+
});
22+
23+
describe("when months are the same", () => {
24+
it("should return false for same month values", () => {
25+
expect(isMonthChange(0, 0)).toBe(false);
26+
expect(isMonthChange(5, 5)).toBe(false);
27+
expect(isMonthChange(11, 11)).toBe(false);
28+
});
29+
});
30+
31+
describe("when inputs are invalid", () => {
32+
it("should return false when currentMonth is NaN", () => {
33+
expect(isMonthChange(NaN, 5)).toBe(false);
34+
});
35+
36+
it("should return false when nextMonth is NaN", () => {
37+
expect(isMonthChange(5, NaN)).toBe(false);
38+
});
39+
40+
it("should return false when both inputs are NaN", () => {
41+
expect(isMonthChange(NaN, NaN)).toBe(false);
42+
});
43+
});
44+
});
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
export const isMonthChange = (currentMonth: number, nextMonth: number) => {
2+
if (isNaN(currentMonth) || isNaN(nextMonth)) {
3+
return false;
4+
}
5+
6+
if (currentMonth === nextMonth) {
7+
return false;
8+
}
9+
10+
return true;
11+
};
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
import { describe, expect, it, vi, beforeEach, afterEach } from "vitest";
2+
3+
import { isMonthViewPrefetchEnabled } from "./isMonthViewPrefetchEnabled";
4+
5+
describe("isMonthViewPrefetchEnabled", () => {
6+
beforeEach(() => {
7+
vi.useFakeTimers();
8+
});
9+
10+
afterEach(() => {
11+
vi.useRealTimers();
12+
});
13+
14+
describe("when current time is after 2 weeks into the month", () => {
15+
it("should return true when date is invalid and same month", () => {
16+
vi.setSystemTime(new Date("2024-01-20T12:00:00Z"));
17+
18+
expect(isMonthViewPrefetchEnabled("invalid-date", "2024-01-01")).toBe(true);
19+
});
20+
21+
it("should return true when date is valid but same month", () => {
22+
vi.setSystemTime(new Date("2024-01-20T12:00:00Z"));
23+
24+
expect(isMonthViewPrefetchEnabled("2024-02-15", "2024-01-01")).toBe(true);
25+
});
26+
27+
it("should return false when date is valid and different month", () => {
28+
vi.setSystemTime(new Date("2024-01-20T12:00:00Z"));
29+
30+
expect(isMonthViewPrefetchEnabled("2024-02-15", "2024-02-01")).toBe(false);
31+
});
32+
});
33+
34+
describe("when current time is before 2 weeks threshold", () => {
35+
it("should return false regardless of date validity", () => {
36+
vi.setSystemTime(new Date("2024-01-10T12:00:00Z"));
37+
38+
expect(isMonthViewPrefetchEnabled("invalid-date", "2024-01-01")).toBe(false);
39+
expect(isMonthViewPrefetchEnabled("2024-02-15", "2024-01-01")).toBe(false);
40+
});
41+
});
42+
43+
describe("edge cases", () => {
44+
it("should handle null month parameter", () => {
45+
vi.setSystemTime(new Date("2024-01-20T12:00:00Z"));
46+
47+
const result = isMonthViewPrefetchEnabled("2024-02-15", null);
48+
expect(result).toBe(false);
49+
});
50+
51+
it("should handle exactly at 2 weeks threshold", () => {
52+
vi.setSystemTime(new Date("2024-01-15T00:00:00Z"));
53+
54+
const result = isMonthViewPrefetchEnabled("invalid-date", "2024-01-01");
55+
expect(result).toBe(false);
56+
});
57+
58+
it("should work with different months throughout the year", () => {
59+
vi.setSystemTime(new Date("2024-06-20T12:00:00Z"));
60+
61+
expect(isMonthViewPrefetchEnabled("invalid-date", "2024-06-01")).toBe(true);
62+
expect(isMonthViewPrefetchEnabled("2024-07-15", "2024-06-01")).toBe(true);
63+
});
64+
});
65+
});
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
import dayjs from "@calcom/dayjs";
2+
3+
export const isMonthViewPrefetchEnabled = (date: string, month: string | null) => {
4+
const isValidDate = dayjs(date).isValid();
5+
const twoWeeksAfter = dayjs(month).startOf("month").add(2, "week");
6+
const isSameMonth = dayjs().isSame(dayjs(month), "month");
7+
const isAfter2Weeks = dayjs().isAfter(twoWeeksAfter);
8+
9+
if (isAfter2Weeks && (!isValidDate || isSameMonth)) {
10+
return true;
11+
}
12+
13+
return false;
14+
};

0 commit comments

Comments
 (0)