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
634 lines
19 KiB
TypeScript
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)
|
|
})
|
|
})
|
|
})
|
|
})
|