import { describe, expect, test } from "@jest/globals" import { act, renderHook, waitFor } from "@testing-library/react" import { type PropsWithChildren } from "react" import { Lang } from "@/constants/languages" import { bedType, booking, breakfastPackage, guestDetailsMember, guestDetailsNonMember, roomPrice, roomRate, } from "@/__mocks__/hotelReservation" import EnterDetailsProvider from "@/providers/EnterDetailsProvider" import { selectRoom, selectRoomStatus } from "./helpers" import { detailsStorageName, useEnterDetailsStore } from "." import type { BedTypeSelection } from "@/types/components/hotelReservation/enterDetails/bedType" import type { SelectRateSearchParams } from "@/types/components/hotelReservation/selectRate/selectRate" import { StepEnum } from "@/types/enums/step" import type { PersistedState } from "@/types/stores/enter-details" jest.mock("react", () => ({ ...jest.requireActual("react"), cache: jest.fn(), })) jest.mock("@/server/utils", () => ({ toLang: () => Lang.en, })) jest.mock("@/lib/api", () => ({ fetchRetry: jest.fn((fn) => fn), })) interface CreateWrapperParams { showBreakfastStep?: boolean breakfastIncluded?: boolean mustBeGuaranteed?: boolean bookingParams?: SelectRateSearchParams bedTypes?: BedTypeSelection[] } function createWrapper(params: Partial = {}) { const { showBreakfastStep = true, breakfastIncluded = false, mustBeGuaranteed = false, bookingParams = booking, bedTypes = [bedType.king, bedType.queen], } = params return function Wrapper({ children }: PropsWithChildren) { return ( {children} ) } } describe("Enter Details Store", () => { beforeEach(() => { window.sessionStorage.clear() }) describe("initial state", () => { test("initialize with correct default values", () => { const { result } = renderHook( () => useEnterDetailsStore((state) => state), { wrapper: createWrapper(), } ) const state = result.current expect(state.booking).toEqual(booking) expect(state.breakfast).toEqual(undefined) // room 1 const room1Status = selectRoomStatus(result.current, 0) const room1 = selectRoom(result.current, 0) expect(room1Status.currentStep).toBe(StepEnum.selectBed) expect(room1.roomPrice.perNight.local.price).toEqual( roomRate.publicRate.localPrice.pricePerNight ) expect(room1.bedType).toEqual(undefined) expect(Object.values(room1.guest).every((value) => value === "")) // room 2 const room2Status = selectRoomStatus(result.current, 1) const room2 = selectRoom(result.current, 1) expect(room2Status.currentStep).toBe(null) expect(room2.roomPrice.perNight.local.price).toEqual( room2.roomRate.publicRate.localPrice.pricePerNight ) expect(room2.bedType).toEqual(undefined) expect(Object.values(room2.guest).every((value) => value === "")) }) test("initialize with correct values from session storage", () => { const storage: PersistedState = { booking: booking, bookingProgress: { currentRoomIndex: 0, canProceedToPayment: true, roomStatuses: [ { isComplete: false, currentStep: StepEnum.selectBed, lastCompletedStep: undefined, steps: { [StepEnum.selectBed]: { step: StepEnum.selectBed, isValid: true, }, [StepEnum.breakfast]: { step: StepEnum.breakfast, isValid: true, }, [StepEnum.details]: { step: StepEnum.details, isValid: true, }, }, }, ], }, rooms: [ { roomFeatures: null, roomRate: roomRate, roomType: "Classic Double", cancellationText: "Non-refundable", rateDetails: [], bedType: { roomTypeCode: bedType.king.value, description: bedType.king.description, }, adults: 1, childrenInRoom: [], breakfast: breakfastPackage, guest: guestDetailsNonMember, roomPrice: roomPrice, }, ], } window.sessionStorage.setItem(detailsStorageName, JSON.stringify(storage)) const { result } = renderHook( () => useEnterDetailsStore((state) => state), { wrapper: createWrapper(), } ) expect(result.current.booking).toEqual(storage.booking) expect(result.current.rooms[0]).toEqual(storage.rooms[0]) expect(result.current.bookingProgress).toEqual(storage.bookingProgress) }) }) test("add bedtype and proceed to next step", async () => { const { result } = renderHook( () => useEnterDetailsStore((state) => state), { wrapper: createWrapper(), } ) let roomStatus = selectRoomStatus(result.current) expect(roomStatus.currentStep).toEqual(StepEnum.selectBed) const selectedBedType = { roomTypeCode: bedType.king.value, description: bedType.king.description, } await act(async () => { result.current.actions.updateBedType(selectedBedType) }) roomStatus = selectRoomStatus(result.current) const room = selectRoom(result.current) expect(roomStatus.steps[StepEnum.selectBed].isValid).toEqual(true) expect(room.bedType).toEqual(selectedBedType) expect(roomStatus.currentStep).toEqual(StepEnum.breakfast) }) test("complete step and navigate to next step", async () => { const { result } = renderHook( () => useEnterDetailsStore((state) => state), { wrapper: createWrapper(), } ) // Room 1 expect(result.current.bookingProgress.currentRoomIndex).toEqual(0) let roomStatus = selectRoomStatus(result.current) expect(roomStatus.currentStep).toEqual(StepEnum.selectBed) await act(async () => { result.current.actions.updateBedType({ roomTypeCode: bedType.king.value, description: bedType.king.description, }) }) roomStatus = selectRoomStatus(result.current) expect(roomStatus.steps[StepEnum.selectBed].isValid).toEqual(true) expect(roomStatus.currentStep).toEqual(StepEnum.breakfast) await act(async () => { result.current.actions.updateBreakfast(breakfastPackage) }) roomStatus = selectRoomStatus(result.current) expect(roomStatus.steps[StepEnum.breakfast]?.isValid).toEqual(true) expect(roomStatus.currentStep).toEqual(StepEnum.details) await act(async () => { result.current.actions.updateDetails(guestDetailsNonMember) }) expect(result.current.bookingProgress.canProceedToPayment).toBe(false) // Room 2 expect(result.current.bookingProgress.currentRoomIndex).toEqual(1) roomStatus = selectRoomStatus(result.current) expect(roomStatus.currentStep).toEqual(StepEnum.selectBed) await act(async () => { const selectedBedType = { roomTypeCode: bedType.king.value, description: bedType.king.description, } result.current.actions.updateBedType(selectedBedType) }) roomStatus = selectRoomStatus(result.current) expect(roomStatus.steps[StepEnum.selectBed].isValid).toEqual(true) expect(roomStatus.currentStep).toEqual(StepEnum.breakfast) await act(async () => { result.current.actions.updateBreakfast(breakfastPackage) }) roomStatus = selectRoomStatus(result.current) expect(roomStatus.steps[StepEnum.breakfast]?.isValid).toEqual(true) expect(roomStatus.currentStep).toEqual(StepEnum.details) await act(async () => { result.current.actions.updateDetails(guestDetailsNonMember) }) expect(result.current.bookingProgress.canProceedToPayment).toBe(true) }) test("all steps needs to be completed before going to next room", async () => { const { result } = renderHook( () => useEnterDetailsStore((state) => state), { wrapper: createWrapper(), } ) await act(async () => { result.current.actions.updateDetails(guestDetailsNonMember) }) expect(result.current.bookingProgress.currentRoomIndex).toEqual(0) await act(async () => { result.current.actions.setStep(StepEnum.breakfast, 1) }) expect(result.current.bookingProgress.currentRoomIndex).toEqual(0) }) test("can go back and modify room 1 after completion", async () => { const { result } = renderHook( () => useEnterDetailsStore((state) => state), { wrapper: createWrapper(), } ) await act(async () => { result.current.actions.updateBedType({ roomTypeCode: bedType.king.value, description: bedType.king.description, }) result.current.actions.updateBreakfast(breakfastPackage) result.current.actions.updateDetails(guestDetailsNonMember) }) // now we are at room 2 expect(result.current.bookingProgress.currentRoomIndex).toEqual(1) await act(async () => { result.current.actions.setStep(StepEnum.breakfast, 0) // click "modify" }) expect(result.current.bookingProgress.currentRoomIndex).toEqual(0) await act(async () => { result.current.actions.updateBreakfast(breakfastPackage) }) // going back to room 2 expect(result.current.bookingProgress.currentRoomIndex).toEqual(1) }) test("breakfast step should be hidden when breakfast is included", async () => { const { result } = renderHook( () => useEnterDetailsStore((state) => state), { wrapper: createWrapper({ showBreakfastStep: false }), } ) const room1Status = selectRoomStatus(result.current, 0) expect(Object.keys(room1Status.steps)).not.toContain(StepEnum.breakfast) const room2Status = selectRoomStatus(result.current, 1) expect(Object.keys(room2Status.steps)).not.toContain(StepEnum.breakfast) }) test("select bed step should be skipped when there is only one bedtype", async () => { const { result } = renderHook( () => useEnterDetailsStore((state) => state), { wrapper: createWrapper({ bedTypes: [bedType.queen] }), } ) const room1Status = selectRoomStatus(result.current, 0) expect(room1Status.steps[StepEnum.selectBed].isValid).toEqual(true) expect(room1Status.currentStep).toEqual(StepEnum.breakfast) const room2Status = selectRoomStatus(result.current, 1) expect(room2Status.steps[StepEnum.selectBed].isValid).toEqual(true) expect(room2Status.currentStep).toEqual(null) }) describe("price calculation", () => { test("total price should be set properly", async () => { const { result } = renderHook( () => useEnterDetailsStore((state) => state), { wrapper: createWrapper(), } ) const publicRate = roomRate.publicRate.localPrice.pricePerStay const memberRate = roomRate.memberRate?.localPrice.pricePerStay ?? 0 const initialTotalPrice = publicRate * result.current.rooms.length expect(result.current.totalPrice.local.price).toEqual(initialTotalPrice) // room 1 await act(async () => { result.current.actions.updateBedType({ roomTypeCode: bedType.king.value, description: bedType.king.description, }) result.current.actions.updateBreakfast(breakfastPackage) }) let expectedTotalPrice = initialTotalPrice + Number(breakfastPackage.localPrice.price) expect(result.current.totalPrice.local.price).toEqual(expectedTotalPrice) await act(async () => { result.current.actions.updateDetails(guestDetailsMember) }) expectedTotalPrice = memberRate + publicRate + Number(breakfastPackage.localPrice.price) expect(result.current.totalPrice.local.price).toEqual(expectedTotalPrice) // room 2 await act(async () => { result.current.actions.updateBedType({ roomTypeCode: bedType.king.value, description: bedType.king.description, }) result.current.actions.updateBreakfast(breakfastPackage) }) expectedTotalPrice = memberRate + publicRate + Number(breakfastPackage.localPrice.price) * 2 expect(result.current.totalPrice.local.price).toEqual(expectedTotalPrice) await act(async () => { result.current.actions.updateDetails(guestDetailsNonMember) }) expect(result.current.totalPrice.local.price).toEqual(expectedTotalPrice) }) test("room price should be set properly", async () => { const { result } = renderHook( () => useEnterDetailsStore((state) => state), { wrapper: createWrapper(), } ) const publicRate = roomRate.publicRate.localPrice.pricePerStay const memberRate = roomRate.memberRate?.localPrice.pricePerStay ?? 0 let room1 = selectRoom(result.current, 0) expect(room1.roomPrice.perStay.local.price).toEqual(publicRate) let room2 = selectRoom(result.current, 0) expect(room2.roomPrice.perStay.local.price).toEqual(publicRate) await act(async () => { result.current.actions.updateDetails(guestDetailsMember) }) room1 = selectRoom(result.current, 0) expect(room1.roomPrice.perStay.local.price).toEqual(memberRate) }) }) describe("change room", () => { test("changing to room with new bedtypes requires selecting bed again", async () => { const { result: firstRun } = renderHook( () => useEnterDetailsStore((state) => state), { wrapper: createWrapper({ bedTypes: [bedType.king, bedType.queen] }), } ) const selectedBedType = { roomTypeCode: bedType.king.value, description: bedType.king.description, } // add bedtype await act(async () => { firstRun.current.actions.updateBedType(selectedBedType) }) await act(async () => { firstRun.current.actions.updateBreakfast(false) // 'no breakfast' selected }) await act(async () => { firstRun.current.actions.updateDetails(guestDetailsNonMember) }) const updatedBooking = { ...booking, rooms: booking.rooms.map((r) => ({ ...r, roomTypeCode: "NEW", })), } // render again to change the bedtypes const { result: secondRun } = renderHook( () => useEnterDetailsStore((state) => state), { wrapper: createWrapper({ bookingParams: updatedBooking, bedTypes: [bedType.single, bedType.queen], }), } ) await waitFor(() => { const room = selectRoom(secondRun.current, 0) const roomStatus = selectRoomStatus(secondRun.current, 0) // bed type should be unset since the bed types have changed expect(room.bedType).toEqual(undefined) // bed step should be unselected expect(roomStatus.currentStep).toBe(StepEnum.selectBed) expect(roomStatus.steps[StepEnum.selectBed].isValid).toBe(false) // other steps should still be selected expect(room.breakfast).toBe(false) expect(roomStatus.steps[StepEnum.breakfast]?.isValid).toBe(true) expect(room.guest).toEqual(guestDetailsNonMember) expect(roomStatus.steps[StepEnum.details].isValid).toBe(true) }) }) test("changing to room with single bedtype option should skip step", async () => { const { result: firstRun } = renderHook( () => useEnterDetailsStore((state) => state), { wrapper: createWrapper({ bedTypes: [bedType.king, bedType.queen] }), } ) const selectedBedType = { roomTypeCode: bedType.king.value, description: bedType.king.description, } // add bedtype await act(async () => { firstRun.current.actions.updateBedType(selectedBedType) }) await act(async () => { firstRun.current.actions.updateBreakfast(breakfastPackage) }) const updatedBooking = { ...booking, rooms: booking.rooms.map((r) => ({ ...r, roomTypeCode: "NEW", })), } // render again to change the bedtypes const { result: secondRun } = renderHook( () => useEnterDetailsStore((state) => state), { wrapper: createWrapper({ bookingParams: updatedBooking, bedTypes: [bedType.queen], }), } ) await waitFor(() => { const room = selectRoom(secondRun.current, 0) const roomStatus = selectRoomStatus(secondRun.current, 0) expect(room.bedType).toEqual({ roomTypeCode: bedType.queen.value, description: bedType.queen.description, }) expect(roomStatus.steps[StepEnum.selectBed].isValid).toBe(true) expect(roomStatus.steps[StepEnum.breakfast]?.isValid).toBe(true) expect(roomStatus.steps[StepEnum.details].isValid).toBe(false) expect(roomStatus.currentStep).toBe(StepEnum.details) }) }) test("if booking has changed, stored values should be discarded", async () => { const { result: firstRun } = renderHook( () => useEnterDetailsStore((state) => state), { wrapper: createWrapper({ bedTypes: [bedType.king, bedType.queen] }), } ) const selectedBedType = { roomTypeCode: bedType.king.value, description: bedType.king.description, } // add bedtype await act(async () => { firstRun.current.actions.updateBedType(selectedBedType) }) await act(async () => { firstRun.current.actions.updateBreakfast(breakfastPackage) }) const updatedBooking = { ...booking, hotelId: "0001", fromDate: "2030-01-01", toDate: "2030-01-02", } renderHook(() => useEnterDetailsStore((state) => state), { wrapper: createWrapper({ bookingParams: updatedBooking, bedTypes: [bedType.queen], }), }) await waitFor(() => { const storageItem = window.sessionStorage.getItem(detailsStorageName) expect(storageItem).toBe(null) }) }) }) })