Files
web/apps/scandic-web/stores/enter-details/useEnterDetailsStore.test.tsx
Anton Gunnarsson 80100e7631 Merged in monorepo-step-1 (pull request #1080)
Migrate to a monorepo setup - step 1

* Move web to subfolder /apps/scandic-web

* Yarn + transitive deps

- Move to yarn
- design-system package removed for now since yarn doesn't
support the parameter for token (ie project currently broken)
- Add missing transitive dependencies as Yarn otherwise
prevents these imports
- VS Code doesn't pick up TS path aliases unless you open
/apps/scandic-web instead of root (will be fixed with monorepo)

* Pin framer-motion to temporarily fix typing issue

https://github.com/adobe/react-spectrum/issues/7494

* Pin zod to avoid typ error

There seems to have been a breaking change in the types
returned by zod where error is now returned as undefined
instead of missing in the type. We should just handle this
but to avoid merge conflicts just pin the dependency for
now.

* Pin react-intl version

Pin version of react-intl to avoid tiny type issue where formatMessage
does not accept a generic any more. This will be fixed in a future
commit, but to avoid merge conflicts just pin for now.

* Pin typescript version

Temporarily pin version as newer versions as stricter and results in
a type error. Will be fixed in future commit after merge.

* Setup workspaces

* Add design-system as a monorepo package

* Remove unused env var DESIGN_SYSTEM_ACCESS_TOKEN

* Fix husky for monorepo setup

* Update netlify.toml

* Add lint script to root package.json

* Add stub readme

* Fix react-intl formatMessage types

* Test netlify.toml in root

* Remove root toml

* Update netlify.toml publish path

* Remove package-lock.json

* Update build for branch/preview builds


Approved-by: Linus Flood
2025-02-26 10:36:17 +00:00

634 lines
19 KiB
TypeScript

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<CreateWrapperParams> = {}) {
const {
showBreakfastStep = true,
breakfastIncluded = false,
mustBeGuaranteed = false,
bookingParams = booking,
bedTypes = [bedType.king, bedType.queen],
} = params
return function Wrapper({ children }: PropsWithChildren) {
return (
<EnterDetailsProvider
booking={bookingParams}
showBreakfastStep={showBreakfastStep}
roomsData={[
{
bedTypes,
packages: null,
mustBeGuaranteed,
breakfastIncluded,
cancellationText: "",
rateDetails: [],
roomType: "Standard",
roomRate: roomRate,
},
{
bedTypes,
packages: null,
mustBeGuaranteed,
breakfastIncluded,
cancellationText: "",
rateDetails: [],
roomType: "Standard",
roomRate: roomRate,
},
]}
searchParamsStr=""
user={null}
vat={0}
>
{children}
</EnterDetailsProvider>
)
}
}
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)
})
})
})
})