refactor: usePrefetch hook (#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>
This commit is contained in:
Rajiv Sahal
2025-10-13 22:51:50 +09:00
committed by GitHub
co-authored by rajiv@cal.com Devin AI Alex van Andel
parent 44a3a9eabb
commit 1dc20e3aec
11 changed files with 573 additions and 28 deletions
@@ -1,6 +1,8 @@
import dayjs from "@calcom/dayjs";
import type { BookerState } from "@calcom/features/bookings/Booker/types";
import { BookerLayouts } from "@calcom/prisma/zod-utils";
import { getPrefetchMonthCount } from "../../utils/getPrefetchMonthCount";
import { isPrefetchNextMonthEnabled } from "../../utils/isPrefetchNextMonthEnabled";
interface UsePrefetchParams {
date: string;
@@ -21,33 +23,21 @@ export const usePrefetch = ({ date, month, bookerLayout, bookerState }: UsePrefe
.add(bookerLayout.columnViewExtraDays.current, "day")
.month();
const isValidDate = dayjs(date).isValid();
const twoWeeksAfter = dayjs(month).startOf("month").add(2, "week");
const isSameMonth = dayjs().isSame(dayjs(month), "month");
const isAfter2Weeks = dayjs().isAfter(twoWeeksAfter);
const prefetchNextMonth =
(bookerLayout.layout === BookerLayouts.WEEK_VIEW &&
!!bookerLayout.extraDays &&
!isNaN(dateMonth) &&
!isNaN(monthAfterAddingExtraDays) &&
dateMonth !== monthAfterAddingExtraDays) ||
(bookerLayout.layout === BookerLayouts.COLUMN_VIEW &&
!isNaN(dateMonth) &&
!isNaN(monthAfterAddingExtraDaysColumnView) &&
dateMonth !== monthAfterAddingExtraDaysColumnView) ||
((bookerLayout.layout === BookerLayouts.MONTH_VIEW || bookerLayout.layout === "mobile") &&
(!isValidDate || isSameMonth) &&
isAfter2Weeks);
const monthCount =
((bookerLayout.layout !== BookerLayouts.WEEK_VIEW && bookerState === "selecting_time") ||
bookerLayout.layout === BookerLayouts.COLUMN_VIEW) &&
!isNaN(monthAfterAdding1Month) &&
!isNaN(monthAfterAddingExtraDaysColumnView) &&
monthAfterAdding1Month !== monthAfterAddingExtraDaysColumnView
? 2
: undefined;
const prefetchNextMonth = isPrefetchNextMonthEnabled(
bookerLayout.layout,
date,
dateMonth,
monthAfterAddingExtraDays,
monthAfterAddingExtraDaysColumnView,
month,
bookerLayout.extraDays
);
const monthCount = getPrefetchMonthCount(
bookerLayout.layout,
bookerState,
monthAfterAdding1Month,
monthAfterAddingExtraDaysColumnView
);
return {
prefetchNextMonth,
@@ -0,0 +1,53 @@
import { describe, expect, it } from "vitest";
import { areDifferentValidMonths } from "./areDifferentValidMonths";
describe("areDifferentValidMonths", () => {
describe("valid different months", () => {
it("should return true for different valid month numbers", () => {
expect(areDifferentValidMonths(0, 1)).toBe(true);
expect(areDifferentValidMonths(5, 10)).toBe(true);
expect(areDifferentValidMonths(11, 0)).toBe(true);
});
});
describe("same month numbers", () => {
it("should return false for identical month numbers", () => {
expect(areDifferentValidMonths(0, 0)).toBe(false);
expect(areDifferentValidMonths(6, 6)).toBe(false);
expect(areDifferentValidMonths(11, 11)).toBe(false);
});
});
describe("invalid inputs", () => {
it("should return false for NaN values", () => {
expect(areDifferentValidMonths(NaN, 5)).toBe(false);
expect(areDifferentValidMonths(5, NaN)).toBe(false);
expect(areDifferentValidMonths(NaN, NaN)).toBe(false);
});
it("should return false for Infinity values", () => {
expect(areDifferentValidMonths(Infinity, 5)).toBe(false);
expect(areDifferentValidMonths(5, Infinity)).toBe(false);
expect(areDifferentValidMonths(Infinity, Infinity)).toBe(false);
});
it("should return false for -Infinity values", () => {
expect(areDifferentValidMonths(-Infinity, 5)).toBe(false);
expect(areDifferentValidMonths(5, -Infinity)).toBe(false);
expect(areDifferentValidMonths(-Infinity, -Infinity)).toBe(false);
});
});
describe("edge cases", () => {
it("should handle negative month numbers correctly", () => {
expect(areDifferentValidMonths(-1, 5)).toBe(true);
expect(areDifferentValidMonths(-1, -1)).toBe(false);
});
it("should handle month numbers beyond 11", () => {
expect(areDifferentValidMonths(12, 5)).toBe(true);
expect(areDifferentValidMonths(100, 200)).toBe(true);
});
});
});
@@ -0,0 +1,14 @@
export const areDifferentValidMonths = (firstMonth: number, secondMonth: number) => {
const isFirstMonthValid = Number.isFinite(firstMonth);
const isSecondMonthValid = Number.isFinite(secondMonth);
if (!isFirstMonthValid || !isSecondMonthValid) {
return false;
}
if (firstMonth === secondMonth) {
return false;
}
return true;
};
@@ -0,0 +1,94 @@
import { describe, expect, it } from "vitest";
import { BookerLayouts } from "@calcom/prisma/zod-utils";
import { getPrefetchMonthCount } from "./getPrefetchMonthCount";
describe("getPrefetchMonthCount", () => {
describe("COLUMN_VIEW layout", () => {
it("should return 2 when months are different", () => {
const result = getPrefetchMonthCount(BookerLayouts.COLUMN_VIEW, "selecting_date", 1, 2);
expect(result).toBe(2);
});
it("should return undefined when months are the same", () => {
const result = getPrefetchMonthCount(BookerLayouts.COLUMN_VIEW, "selecting_date", 5, 5);
expect(result).toBe(undefined);
});
it("should return 2 regardless of bookerState when months are different", () => {
expect(getPrefetchMonthCount(BookerLayouts.COLUMN_VIEW, "selecting_time", 3, 4)).toBe(2);
expect(getPrefetchMonthCount(BookerLayouts.COLUMN_VIEW, "booking", 7, 8)).toBe(2);
});
});
describe("WEEK_VIEW layout", () => {
it("should return undefined when state is selecting_time", () => {
const result = getPrefetchMonthCount(BookerLayouts.WEEK_VIEW, "selecting_time", 1, 2);
expect(result).toBe(undefined);
});
it("should return undefined when state is not selecting_time", () => {
const result = getPrefetchMonthCount(BookerLayouts.WEEK_VIEW, "selecting_date", 1, 2);
expect(result).toBe(undefined);
});
it("should return undefined even with different months", () => {
const result = getPrefetchMonthCount(BookerLayouts.WEEK_VIEW, "booking", 0, 11);
expect(result).toBe(undefined);
});
});
describe("MONTH_VIEW layout with selecting_time state", () => {
it("should return 2 when months are different", () => {
const result = getPrefetchMonthCount(BookerLayouts.MONTH_VIEW, "selecting_time", 3, 4);
expect(result).toBe(2);
});
it("should return undefined when months are the same", () => {
const result = getPrefetchMonthCount(BookerLayouts.MONTH_VIEW, "selecting_time", 6, 6);
expect(result).toBe(undefined);
});
});
describe("MONTH_VIEW layout with other states", () => {
it("should return undefined when state is selecting_date", () => {
const result = getPrefetchMonthCount(BookerLayouts.MONTH_VIEW, "selecting_date", 1, 2);
expect(result).toBe(undefined);
});
it("should return undefined when state is booking", () => {
const result = getPrefetchMonthCount(BookerLayouts.MONTH_VIEW, "booking", 1, 2);
expect(result).toBe(undefined);
});
});
describe("invalid month inputs", () => {
it("should return undefined when first month is NaN", () => {
const result = getPrefetchMonthCount(BookerLayouts.COLUMN_VIEW, "selecting_date", NaN, 5);
expect(result).toBe(undefined);
});
it("should return undefined when second month is NaN", () => {
const result = getPrefetchMonthCount(BookerLayouts.COLUMN_VIEW, "selecting_date", 5, NaN);
expect(result).toBe(undefined);
});
it("should return undefined when both months are invalid", () => {
const result = getPrefetchMonthCount(BookerLayouts.COLUMN_VIEW, "selecting_time", Infinity, -Infinity);
expect(result).toBe(undefined);
});
});
describe("edge cases", () => {
it("should handle month transitions (11 to 0)", () => {
const result = getPrefetchMonthCount(BookerLayouts.COLUMN_VIEW, "selecting_date", 11, 0);
expect(result).toBe(2);
});
it("should handle negative month numbers if they are different", () => {
const result = getPrefetchMonthCount(BookerLayouts.COLUMN_VIEW, "selecting_date", -1, 5);
expect(result).toBe(2);
});
});
});
@@ -0,0 +1,23 @@
import { BookerLayouts } from "@calcom/prisma/zod-utils";
import type { BookerState } from "../types";
import { areDifferentValidMonths } from "./areDifferentValidMonths";
export const getPrefetchMonthCount = (
bookerLayout: string,
bookerState: BookerState,
firstMonth: number,
secondMonth: number
) => {
const isDifferentMonth = areDifferentValidMonths(firstMonth, secondMonth);
const isWeekView = bookerLayout === BookerLayouts.WEEK_VIEW;
const isColumnView = bookerLayout === BookerLayouts.COLUMN_VIEW;
const isSelectingTime = bookerState === "selecting_time";
if (!isDifferentMonth) return undefined;
if (isColumnView || (!isWeekView && isSelectingTime)) return 2;
return undefined;
};
@@ -0,0 +1,44 @@
import { describe, expect, it } from "vitest";
import { isMonthChange } from "./isMonthChange";
describe("isMonthChange", () => {
describe("when months are different", () => {
it("should return true for consecutive months", () => {
expect(isMonthChange(0, 1)).toBe(true);
expect(isMonthChange(5, 6)).toBe(true);
expect(isMonthChange(10, 11)).toBe(true);
});
it("should return true for non-consecutive months", () => {
expect(isMonthChange(0, 11)).toBe(true);
expect(isMonthChange(3, 9)).toBe(true);
});
it("should return true for year transition (11 to 0)", () => {
expect(isMonthChange(11, 0)).toBe(true);
});
});
describe("when months are the same", () => {
it("should return false for same month values", () => {
expect(isMonthChange(0, 0)).toBe(false);
expect(isMonthChange(5, 5)).toBe(false);
expect(isMonthChange(11, 11)).toBe(false);
});
});
describe("when inputs are invalid", () => {
it("should return false when currentMonth is NaN", () => {
expect(isMonthChange(NaN, 5)).toBe(false);
});
it("should return false when nextMonth is NaN", () => {
expect(isMonthChange(5, NaN)).toBe(false);
});
it("should return false when both inputs are NaN", () => {
expect(isMonthChange(NaN, NaN)).toBe(false);
});
});
});
@@ -0,0 +1,11 @@
export const isMonthChange = (currentMonth: number, nextMonth: number) => {
if (isNaN(currentMonth) || isNaN(nextMonth)) {
return false;
}
if (currentMonth === nextMonth) {
return false;
}
return true;
};
@@ -0,0 +1,65 @@
import { describe, expect, it, vi, beforeEach, afterEach } from "vitest";
import { isMonthViewPrefetchEnabled } from "./isMonthViewPrefetchEnabled";
describe("isMonthViewPrefetchEnabled", () => {
beforeEach(() => {
vi.useFakeTimers();
});
afterEach(() => {
vi.useRealTimers();
});
describe("when current time is after 2 weeks into the month", () => {
it("should return true when date is invalid and same month", () => {
vi.setSystemTime(new Date("2024-01-20T12:00:00Z"));
expect(isMonthViewPrefetchEnabled("invalid-date", "2024-01-01")).toBe(true);
});
it("should return true when date is valid but same month", () => {
vi.setSystemTime(new Date("2024-01-20T12:00:00Z"));
expect(isMonthViewPrefetchEnabled("2024-02-15", "2024-01-01")).toBe(true);
});
it("should return false when date is valid and different month", () => {
vi.setSystemTime(new Date("2024-01-20T12:00:00Z"));
expect(isMonthViewPrefetchEnabled("2024-02-15", "2024-02-01")).toBe(false);
});
});
describe("when current time is before 2 weeks threshold", () => {
it("should return false regardless of date validity", () => {
vi.setSystemTime(new Date("2024-01-10T12:00:00Z"));
expect(isMonthViewPrefetchEnabled("invalid-date", "2024-01-01")).toBe(false);
expect(isMonthViewPrefetchEnabled("2024-02-15", "2024-01-01")).toBe(false);
});
});
describe("edge cases", () => {
it("should handle null month parameter", () => {
vi.setSystemTime(new Date("2024-01-20T12:00:00Z"));
const result = isMonthViewPrefetchEnabled("2024-02-15", null);
expect(result).toBe(false);
});
it("should handle exactly at 2 weeks threshold", () => {
vi.setSystemTime(new Date("2024-01-15T00:00:00Z"));
const result = isMonthViewPrefetchEnabled("invalid-date", "2024-01-01");
expect(result).toBe(false);
});
it("should work with different months throughout the year", () => {
vi.setSystemTime(new Date("2024-06-20T12:00:00Z"));
expect(isMonthViewPrefetchEnabled("invalid-date", "2024-06-01")).toBe(true);
expect(isMonthViewPrefetchEnabled("2024-07-15", "2024-06-01")).toBe(true);
});
});
});
@@ -0,0 +1,14 @@
import dayjs from "@calcom/dayjs";
export const isMonthViewPrefetchEnabled = (date: string, month: string | null) => {
const isValidDate = dayjs(date).isValid();
const twoWeeksAfter = dayjs(month).startOf("month").add(2, "week");
const isSameMonth = dayjs().isSame(dayjs(month), "month");
const isAfter2Weeks = dayjs().isAfter(twoWeeksAfter);
if (isAfter2Weeks && (!isValidDate || isSameMonth)) {
return true;
}
return false;
};
@@ -0,0 +1,208 @@
import { describe, expect, it, vi, beforeEach, afterEach } from "vitest";
import dayjs from "@calcom/dayjs";
import { BookerLayouts } from "@calcom/prisma/zod-utils";
import { isPrefetchNextMonthEnabled } from "./isPrefetchNextMonthEnabled";
describe("isPrefetchNextMonthEnabled", () => {
beforeEach(() => {
vi.useFakeTimers();
});
afterEach(() => {
vi.useRealTimers();
});
describe("WEEK_VIEW layout", () => {
it("should return true when extraDays is provided and months change", () => {
const date = "2024-01-28";
const dateMonth = dayjs(date).month();
const extraDays = 7;
const monthAfterAddingExtraDays = dayjs(date).add(extraDays, "day").month();
const monthAfterAddingExtraDaysColumnView = dayjs(date).add(5, "day").month();
const result = isPrefetchNextMonthEnabled(
BookerLayouts.WEEK_VIEW,
date,
dateMonth,
monthAfterAddingExtraDays,
monthAfterAddingExtraDaysColumnView,
"2024-01-01",
extraDays
);
expect(result).toBe(true);
});
it("should return false when extraDays is provided but months don't change", () => {
const date = "2024-01-15";
const dateMonth = dayjs(date).month();
const extraDays = 7;
const monthAfterAddingExtraDays = dayjs(date).add(extraDays, "day").month();
const monthAfterAddingExtraDaysColumnView = dayjs(date).add(5, "day").month();
const result = isPrefetchNextMonthEnabled(
BookerLayouts.WEEK_VIEW,
date,
dateMonth,
monthAfterAddingExtraDays,
monthAfterAddingExtraDaysColumnView,
"2024-01-01",
extraDays
);
expect(result).toBe(false);
});
it("should return false when extraDays is not provided", () => {
const date = "2024-01-28";
const dateMonth = dayjs(date).month();
const monthAfterAddingExtraDays = dayjs(date).add(7, "day").month();
const monthAfterAddingExtraDaysColumnView = dayjs(date).add(5, "day").month();
const result = isPrefetchNextMonthEnabled(
BookerLayouts.WEEK_VIEW,
date,
dateMonth,
monthAfterAddingExtraDays,
monthAfterAddingExtraDaysColumnView,
"2024-01-01",
undefined
);
expect(result).toBe(false);
});
it("should return false when extraDays is 0", () => {
const date = "2024-01-28";
const dateMonth = dayjs(date).month();
const monthAfterAddingExtraDays = dayjs(date).month();
const monthAfterAddingExtraDaysColumnView = dayjs(date).add(5, "day").month();
const result = isPrefetchNextMonthEnabled(
BookerLayouts.WEEK_VIEW,
date,
dateMonth,
monthAfterAddingExtraDays,
monthAfterAddingExtraDaysColumnView,
"2024-01-01",
0
);
expect(result).toBe(false);
});
});
describe("COLUMN_VIEW layout", () => {
it("should return true when months change after adding columnView extra days", () => {
const date = "2024-01-30";
const dateMonth = dayjs(date).month();
const monthAfterAddingExtraDays = dayjs(date).add(7, "day").month();
const monthAfterAddingExtraDaysColumnView = dayjs(date).add(5, "day").month();
const result = isPrefetchNextMonthEnabled(
BookerLayouts.COLUMN_VIEW,
date,
dateMonth,
monthAfterAddingExtraDays,
monthAfterAddingExtraDaysColumnView,
"2024-01-01"
);
expect(result).toBe(true);
});
it("should return false when months don't change after adding columnView extra days", () => {
const date = "2024-01-10";
const dateMonth = dayjs(date).month();
const monthAfterAddingExtraDays = dayjs(date).add(7, "day").month();
const monthAfterAddingExtraDaysColumnView = dayjs(date).add(5, "day").month();
const result = isPrefetchNextMonthEnabled(
BookerLayouts.COLUMN_VIEW,
date,
dateMonth,
monthAfterAddingExtraDays,
monthAfterAddingExtraDaysColumnView,
"2024-01-01"
);
expect(result).toBe(false);
});
});
describe("MONTH_VIEW layout", () => {
it("should return true when conditions for month view prefetch are met", () => {
vi.setSystemTime(new Date("2024-01-20T12:00:00Z"));
const date = "invalid-date";
const dateMonth = dayjs(date).month();
const monthAfterAddingExtraDays = dayjs(date).add(7, "day").month();
const monthAfterAddingExtraDaysColumnView = dayjs(date).add(5, "day").month();
const result = isPrefetchNextMonthEnabled(
BookerLayouts.MONTH_VIEW,
date,
dateMonth,
monthAfterAddingExtraDays,
monthAfterAddingExtraDaysColumnView,
"2024-01-01"
);
expect(result).toBe(true);
});
it("should return false when current time is before 2 weeks threshold", () => {
vi.setSystemTime(new Date("2024-01-10T12:00:00Z"));
const date = "invalid-date";
const dateMonth = dayjs(date).month();
const monthAfterAddingExtraDays = dayjs(date).add(7, "day").month();
const monthAfterAddingExtraDaysColumnView = dayjs(date).add(5, "day").month();
const result = isPrefetchNextMonthEnabled(
BookerLayouts.MONTH_VIEW,
date,
dateMonth,
monthAfterAddingExtraDays,
monthAfterAddingExtraDaysColumnView,
"2024-01-01"
);
expect(result).toBe(false);
});
});
describe("mobile layout", () => {
it("should return true when conditions for month view prefetch are met", () => {
vi.setSystemTime(new Date("2024-01-20T12:00:00Z"));
const date = "invalid-date";
const dateMonth = dayjs(date).month();
const monthAfterAddingExtraDays = dayjs(date).add(7, "day").month();
const monthAfterAddingExtraDaysColumnView = dayjs(date).add(5, "day").month();
const result = isPrefetchNextMonthEnabled(
"mobile",
date,
dateMonth,
monthAfterAddingExtraDays,
monthAfterAddingExtraDaysColumnView,
"2024-01-01"
);
expect(result).toBe(true);
});
});
describe("unknown layout", () => {
it("should return false for unrecognized layout", () => {
const date = "2024-01-15";
const dateMonth = dayjs(date).month();
const monthAfterAddingExtraDays = dayjs(date).add(7, "day").month();
const monthAfterAddingExtraDaysColumnView = dayjs(date).add(5, "day").month();
const result = isPrefetchNextMonthEnabled(
"unknown-layout",
date,
dateMonth,
monthAfterAddingExtraDays,
monthAfterAddingExtraDaysColumnView,
"2024-01-01"
);
expect(result).toBe(false);
});
});
});
@@ -0,0 +1,29 @@
import { BookerLayouts } from "@calcom/prisma/zod-utils";
import { isMonthChange } from "./isMonthChange";
import { isMonthViewPrefetchEnabled } from "./isMonthViewPrefetchEnabled";
export const isPrefetchNextMonthEnabled = (
bookerLayout: string,
date: string,
dateMonth: number,
monthAfterAddingExtraDays: number,
monthAfterAddingExtraDaysColumnView: number,
month: string | null,
extraDays?: number
) => {
if (bookerLayout === BookerLayouts.WEEK_VIEW) {
return !!extraDays && isMonthChange(dateMonth, monthAfterAddingExtraDays);
}
if (bookerLayout === BookerLayouts.COLUMN_VIEW) {
return isMonthChange(dateMonth, monthAfterAddingExtraDaysColumnView);
}
if (bookerLayout === BookerLayouts.MONTH_VIEW || bookerLayout === "mobile") {
const shouldPrefetchNextMonth = isMonthViewPrefetchEnabled(date, month);
return shouldPrefetchNextMonth;
}
return false;
};