Merged in feat/SW-619-signup-non-happy (pull request #1083)
Feat/SW-619 signup non happy * feat(SW-619): Added tests for Date input * feat(SW-619): Updated date input to not allow date below 18 years old, also added form validation and tests to cover this change * fix * feat(SW-619): add info banner if membership verification fails * fix(SW-619): update test description Approved-by: Christel Westerberg Approved-by: Arvid Norlin
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
"use client"
|
||||
import { useRef } from "react"
|
||||
import { useIntl } from "react-intl"
|
||||
|
||||
import Header from "@/components/HotelReservation/BookingConfirmation/Header"
|
||||
import HotelDetails from "@/components/HotelReservation/BookingConfirmation/HotelDetails"
|
||||
@@ -8,22 +9,40 @@ import Promos from "@/components/HotelReservation/BookingConfirmation/Promos"
|
||||
import Receipt from "@/components/HotelReservation/BookingConfirmation/Receipt"
|
||||
import Rooms from "@/components/HotelReservation/BookingConfirmation/Rooms"
|
||||
import SidePanel from "@/components/HotelReservation/SidePanel"
|
||||
import Alert from "@/components/TempDesignSystem/Alert"
|
||||
import Divider from "@/components/TempDesignSystem/Divider"
|
||||
|
||||
import styles from "./confirmation.module.css"
|
||||
|
||||
import type { ConfirmationProps } from "@/types/components/hotelReservation/bookingConfirmation/bookingConfirmation"
|
||||
import { AlertTypeEnum } from "@/types/enums/alert"
|
||||
|
||||
export default function Confirmation({
|
||||
booking,
|
||||
hotel,
|
||||
room,
|
||||
}: ConfirmationProps) {
|
||||
const intl = useIntl()
|
||||
const mainRef = useRef<HTMLElement | null>(null)
|
||||
|
||||
const failedToVerifyMembership =
|
||||
booking.rateDefinition.isMemberRate && !booking.guest.membershipNumber
|
||||
|
||||
return (
|
||||
<main className={styles.main} ref={mainRef}>
|
||||
<Header booking={booking} hotel={hotel} mainRef={mainRef} />
|
||||
<div className={styles.booking}>
|
||||
{failedToVerifyMembership && (
|
||||
<Alert
|
||||
type={AlertTypeEnum.Info}
|
||||
heading={intl.formatMessage({
|
||||
id: "booking.confirmation.membershipInfo.heading",
|
||||
})}
|
||||
text={intl.formatMessage({
|
||||
id: "booking.confirmation.membershipInfo.text",
|
||||
})}
|
||||
/>
|
||||
)}
|
||||
<Rooms booking={booking} room={room} />
|
||||
<PaymentDetails booking={booking} />
|
||||
<Divider color="primaryLightSubtle" />
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import { z } from "zod"
|
||||
|
||||
import { dt } from "@/lib/dt"
|
||||
|
||||
import { phoneValidator } from "@/utils/phoneValidator"
|
||||
|
||||
// stringMatcher regex is copied from current web as specified by requirements.
|
||||
@@ -78,7 +80,18 @@ export const joinDetailsSchema = baseDetailsSchema.merge(
|
||||
z.object({
|
||||
join: z.literal<boolean>(true),
|
||||
zipCode: z.string().min(1, { message: "Zip code is required" }),
|
||||
dateOfBirth: z.string().min(1, { message: "Date of birth is required" }),
|
||||
dateOfBirth: z
|
||||
.string()
|
||||
.min(1, { message: "Date of birth is required" })
|
||||
.refine(
|
||||
(date) => {
|
||||
const today = dt()
|
||||
const dob = dt(date)
|
||||
const age = today.diff(dob, "year")
|
||||
return age >= 18
|
||||
},
|
||||
{ message: "Must be at least 18 years of age to continue" }
|
||||
),
|
||||
membershipNo: z.string().default(""),
|
||||
})
|
||||
)
|
||||
|
||||
138
components/TempDesignSystem/Form/Date/date.test.tsx
Normal file
138
components/TempDesignSystem/Form/Date/date.test.tsx
Normal file
@@ -0,0 +1,138 @@
|
||||
import { describe, expect, test } from "@jest/globals" // importing because of type conflict with globals from Cypress
|
||||
import { render, screen } from "@testing-library/react"
|
||||
import { type UserEvent, userEvent } from "@testing-library/user-event"
|
||||
import { FormProvider, useForm } from "react-hook-form"
|
||||
|
||||
import { Lang } from "@/constants/languages"
|
||||
import { dt } from "@/lib/dt"
|
||||
|
||||
import { getLocalizedMonthName } from "@/utils/dateFormatting"
|
||||
|
||||
import Date from "./index"
|
||||
|
||||
interface FormWrapperProps {
|
||||
defaultValues: Record<string, unknown>
|
||||
children: React.ReactNode
|
||||
onSubmit: (data: unknown) => void
|
||||
}
|
||||
|
||||
function FormWrapper({ defaultValues, children, onSubmit }: FormWrapperProps) {
|
||||
const methods = useForm({
|
||||
defaultValues,
|
||||
})
|
||||
return (
|
||||
<FormProvider {...methods}>
|
||||
<form onSubmit={methods.handleSubmit((data) => onSubmit(data))}>
|
||||
{children}
|
||||
<button type="submit">Submit</button>
|
||||
</form>
|
||||
</FormProvider>
|
||||
)
|
||||
}
|
||||
|
||||
async function selectOption(user: UserEvent, name: RegExp, value: string) {
|
||||
// since its not a proper Select element selectOptions from userEvent doesn't work
|
||||
const select = screen.queryByRole("button", { name })
|
||||
if (select) {
|
||||
await user.click(select)
|
||||
|
||||
const option = screen.queryByRole("option", { name: value })
|
||||
if (option) {
|
||||
await user.click(option)
|
||||
} else {
|
||||
await user.click(select) // click select again to close it
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const testCases = [
|
||||
{
|
||||
description: "date is set and submitted successfully",
|
||||
defaultValue: "",
|
||||
dateOfBirth: "1987-12-05",
|
||||
expectedOutput: {
|
||||
dateOfBirth: "1987-12-05",
|
||||
year: 1987,
|
||||
month: 12,
|
||||
day: 5,
|
||||
},
|
||||
},
|
||||
{
|
||||
description: "sets default value and submits successfully",
|
||||
defaultValue: "2000-01-01",
|
||||
dateOfBirth: "",
|
||||
expectedOutput: {
|
||||
dateOfBirth: "2000-01-01",
|
||||
year: 2000,
|
||||
month: 1,
|
||||
day: 1,
|
||||
},
|
||||
},
|
||||
{
|
||||
description: "accepts date exactly 18 years old",
|
||||
defaultValue: "",
|
||||
dateOfBirth: dt().subtract(18, "year").format("YYYY-MM-DD"),
|
||||
expectedOutput: {
|
||||
dateOfBirth: dt().subtract(18, "year").format("YYYY-MM-DD"),
|
||||
},
|
||||
},
|
||||
{
|
||||
description: "rejects date below 18 years old - by year",
|
||||
defaultValue: "",
|
||||
dateOfBirth: dt().subtract(17, "year").format("YYYY-MM-DD"),
|
||||
expectedOutput: {
|
||||
dateOfBirth: "",
|
||||
},
|
||||
},
|
||||
{
|
||||
description: "rejects date below 18 years old - by month",
|
||||
defaultValue: "",
|
||||
dateOfBirth: dt().subtract(18, "year").add(1, "month").format("YYYY-MM-DD"),
|
||||
expectedOutput: {
|
||||
dateOfBirth: "",
|
||||
},
|
||||
},
|
||||
{
|
||||
description: "rejects date below 18 years old - by day",
|
||||
defaultValue: "",
|
||||
dateOfBirth: dt().subtract(18, "year").add(1, "day").format("YYYY-MM-DD"),
|
||||
expectedOutput: {
|
||||
dateOfBirth: "",
|
||||
},
|
||||
},
|
||||
]
|
||||
|
||||
describe("Date input", () => {
|
||||
test.each(testCases)(
|
||||
"$description",
|
||||
async ({ defaultValue, dateOfBirth, expectedOutput }) => {
|
||||
const user = userEvent.setup()
|
||||
const handleSubmit = jest.fn()
|
||||
|
||||
render(
|
||||
<FormWrapper
|
||||
defaultValues={{ dateOfBirth: defaultValue }}
|
||||
onSubmit={handleSubmit}
|
||||
>
|
||||
<Date name="dateOfBirth" />
|
||||
</FormWrapper>
|
||||
)
|
||||
|
||||
const date = dt(dateOfBirth).toDate()
|
||||
const year = date.getFullYear()
|
||||
const month = date.getMonth() + 1
|
||||
const day = date.getDate()
|
||||
|
||||
await selectOption(user, /year/i, year.toString())
|
||||
await selectOption(user, /month/i, getLocalizedMonthName(month, Lang.en))
|
||||
await selectOption(user, /day/i, day.toString())
|
||||
|
||||
const submitButton = screen.getByRole("button", { name: /submit/i })
|
||||
await user.click(submitButton)
|
||||
|
||||
expect(handleSubmit).toHaveBeenCalledWith(
|
||||
expect.objectContaining(expectedOutput)
|
||||
)
|
||||
}
|
||||
)
|
||||
})
|
||||
@@ -19,6 +19,8 @@ import styles from "./date.module.css"
|
||||
|
||||
export default function DateSelect({ name, registerOptions = {} }: DateProps) {
|
||||
const intl = useIntl()
|
||||
const lang = useLang()
|
||||
|
||||
const { control, setValue, formState, watch } = useFormContext()
|
||||
const { field, fieldState } = useController({
|
||||
control,
|
||||
@@ -31,14 +33,20 @@ export default function DateSelect({ name, registerOptions = {} }: DateProps) {
|
||||
const month = watch(DateName.month)
|
||||
const day = watch(DateName.day)
|
||||
|
||||
const lang = useLang()
|
||||
const months = rangeArray(1, 12).map((month) => ({
|
||||
const minAgeDate = dt().subtract(18, "year").toDate() // age 18
|
||||
const minAgeYear = minAgeDate.getFullYear()
|
||||
const minAgeMonth = year === minAgeYear ? minAgeDate.getMonth() + 1 : null
|
||||
const minAgeDay =
|
||||
Number(year) === minAgeYear && Number(month) === minAgeMonth
|
||||
? minAgeDate.getDate()
|
||||
: null
|
||||
|
||||
const months = rangeArray(1, minAgeMonth ?? 12).map((month) => ({
|
||||
value: month,
|
||||
label: getLocalizedMonthName(month, lang),
|
||||
}))
|
||||
|
||||
const currentYear = new Date().getFullYear()
|
||||
const years = rangeArray(1900, currentYear - 18)
|
||||
const years = rangeArray(1900, minAgeYear)
|
||||
.reverse()
|
||||
.map((year) => ({ value: year, label: year.toString() }))
|
||||
|
||||
@@ -48,7 +56,7 @@ export default function DateSelect({ name, registerOptions = {} }: DateProps) {
|
||||
month ? Number(month) - 1 : null
|
||||
)
|
||||
|
||||
const days = rangeArray(1, daysInMonth).map((day) => ({
|
||||
const days = rangeArray(1, minAgeDay ?? daysInMonth).map((day) => ({
|
||||
value: day,
|
||||
label: `${day}`,
|
||||
}))
|
||||
@@ -119,7 +127,6 @@ export default function DateSelect({ name, registerOptions = {} }: DateProps) {
|
||||
ref={field.ref}
|
||||
value={dateValue}
|
||||
data-testid={name}
|
||||
className={styles.datePicker}
|
||||
>
|
||||
<Group>
|
||||
<DateInput className={styles.container}>
|
||||
|
||||
@@ -501,6 +501,8 @@
|
||||
"booking.bedOptions": "Sengemuligheder",
|
||||
"booking.children": "{totalChildren, plural, one {# barn} other {# børn}}",
|
||||
"booking.children.breakfasts": "{totalChildren, plural, one {# barn} other {# børn}}, {totalBreakfasts, plural, one {# morgenmad} other {# morgenmad}}",
|
||||
"booking.confirmation.membershipInfo.heading": "Medlemskab ikke verificeret",
|
||||
"booking.confirmation.membershipInfo.text": "Din booking er bekræftet, men vi kunne ikke verificere dit medlemskab. Hvis du har booket med et medlemstilbud, skal du enten vise dit eksisterende medlemskab ved check-in, blive medlem eller betale prisdifferencen ved check-in. Tilmelding er foretrukket online før opholdet.",
|
||||
"booking.confirmation.text": "Tak fordi du bookede hos os! Vi glæder os til at byde dig velkommen og håber du får et behageligt ophold. Hvis du har spørgsmål eller har brug for at foretage ændringer i din reservation, bedes du <emailLink>kontakte os.</emailLink>",
|
||||
"booking.confirmation.title": "Booking bekræftelse",
|
||||
"booking.guests": "Maks {nrOfGuests, plural, one {# gæst} other {# gæster}}",
|
||||
|
||||
@@ -500,6 +500,8 @@
|
||||
"booking.bedOptions": "Bettoptionen",
|
||||
"booking.children": "{totalChildren, plural, one {# kind} other {# kinder}}",
|
||||
"booking.children.breakfasts": "{totalChildren, plural, one {# kind} other {# kinder}}, {totalBreakfasts, plural, one {# frühstück} other {# frühstücke}}",
|
||||
"booking.confirmation.membershipInfo.heading": "Medlemskab nicht verifiziert",
|
||||
"booking.confirmation.membershipInfo.text": "Ihre Buchung ist bestätigt, aber wir konnten Ihr Mitglied nicht verifizieren. Wenn Sie mit einem Mitgliederrabatt gebucht haben, müssen Sie entweder Ihr vorhandenes Mitgliedschaftsnummer bei der Anreise präsentieren, ein Mitglied werden oder die Preisdifferenz bei der Anreise bezahlen. Die Anmeldung ist vorzugsweise online vor der Aufenthaltsdauer erfolgreich.",
|
||||
"booking.confirmation.text": "Vielen Dank, dass Sie bei uns gebucht haben! Wir freuen uns, Sie bei uns begrüßen zu dürfen und wünschen Ihnen einen angenehmen Aufenthalt. Wenn Sie Fragen haben oder Änderungen an Ihrer Buchung vornehmen müssen, <emailLink>kontaktieren Sie uns bitte.</emailLink>.",
|
||||
"booking.confirmation.title": "Buchungsbestätigung",
|
||||
"booking.guests": "Max {nrOfGuests, plural, one {# gast} other {# gäste}}",
|
||||
|
||||
@@ -544,6 +544,8 @@
|
||||
"booking.bedOptions": "Bed options",
|
||||
"booking.children": "{totalChildren, plural, one {# child} other {# children}}",
|
||||
"booking.children.breakfasts": "{totalChildren, plural, one {# child} other {# children}}, {totalBreakfasts, plural, one {# breakfast} other {# breakfasts}}",
|
||||
"booking.confirmation.membershipInfo.heading": "Failed to verify membership",
|
||||
"booking.confirmation.membershipInfo.text": "Your booking(s) is confirmed but we could not verify your membership. If you have booked with a member discount, you'll either need to present your existing membership number upon check-in, become a member or pay the price difference at the hotel. Signing up is preferably done online before the stay.",
|
||||
"booking.confirmation.text": "Thank you for booking with us! We look forward to welcoming you and hope you have a pleasant stay. If you have any questions or need to make changes to your reservation, please <emailLink>contact us.</emailLink>",
|
||||
"booking.confirmation.title": "Booking confirmation",
|
||||
"booking.guests": "Max {nrOfGuests, plural, one {# guest} other {# guests}}",
|
||||
|
||||
@@ -499,6 +499,8 @@
|
||||
"booking.bedOptions": "Vuodevaihtoehdot",
|
||||
"booking.children": "{totalChildren, plural, one {# lapsi} other {# lasta}}",
|
||||
"booking.children.breakfasts": "{totalChildren, plural, one {# lapsi} other {# lasta}}, {totalBreakfasts, plural, one {# aamiainen} other {# aamiaista}}",
|
||||
"booking.confirmation.membershipInfo.heading": "Jäsenyys ei verifioitu",
|
||||
"booking.confirmation.membershipInfo.text": "Varauksesi on vahvistettu, mutta jäsenyytesi ei voitu vahvistaa. Jos olet bookeutunut jäsenyysalennoilla, sinun on joko esitettävä olemassa olevan jäsenyysnumero tarkistukseen, tulla jäseneksi tai maksamaan hinnan eron hotellissa. Jäsenyyden tilittäminen on suositeltavampaa tehdä verkkoon ennen majoittumista.",
|
||||
"booking.confirmation.text": "Kiitos, että teit varauksen meiltä! Toivotamme sinut tervetulleeksi ja toivomme sinulle miellyttävää oleskelua. Jos sinulla on kysyttävää tai haluat tehdä muutoksia varaukseesi, <emailLink>ota meihin yhteyttä.</emailLink>",
|
||||
"booking.confirmation.title": "Varausvahvistus",
|
||||
"booking.guests": "Max {nrOfGuests, plural, one {# vieras} other {# vieraita}}",
|
||||
|
||||
@@ -499,6 +499,8 @@
|
||||
"booking.bedOptions": "Sengemuligheter",
|
||||
"booking.children": "{totalChildren, plural, one {# barn} other {# barn}}",
|
||||
"booking.children.breakfasts": "{totalChildren, plural, one {# barn} other {# barn}}, {totalBreakfasts, plural, one {# frokost} other {# frokoster}}",
|
||||
"booking.confirmation.membershipInfo.heading": "Medlemskap ikke verifisert",
|
||||
"booking.confirmation.membershipInfo.text": "Din bestilling er bekreftet, men vi kunne ikke verifisere medlemskapet ditt. Hvis du har booke ut med et medlemsrabatt, må du enten presentere eksisterende medlemsnummer ved check-in, bli medlem eller betale prisdifferansen ved hotellet. Registrering er foretrukket gjort online før oppholdet.",
|
||||
"booking.confirmation.text": "Takk for at du booket hos oss! Vi ser frem til å ønske deg velkommen og håper du får et hyggelig opphold. Hvis du har spørsmål eller trenger å gjøre endringer i bestillingen din, vennligst <emailLink>kontakt oss.</emailLink>",
|
||||
"booking.confirmation.title": "Bestillingsbekreftelse",
|
||||
"booking.guests": "Maks {nrOfGuests, plural, one {# gjest} other {# gjester}}",
|
||||
|
||||
@@ -499,6 +499,8 @@
|
||||
"booking.bedOptions": "Sängalternativ",
|
||||
"booking.children": "{totalChildren, plural, one {# barn} other {# barn}}",
|
||||
"booking.children.breakfasts": "{totalChildren, plural, one {# barn} other {# barn}}, {totalBreakfasts, plural, one {# frukost} other {# frukostar}}",
|
||||
"booking.confirmation.membershipInfo.heading": "Medlemskap inte verifierat",
|
||||
"booking.confirmation.membershipInfo.text": "Din bokning är bekräftad, men vi kunde inte verifiera ditt medlemskap. Om du har bokat med ett medlemsrabatt måste du antingen presentera ditt befintliga medlemsnummer vid check-in, bli medlem eller betala prisdifferensen vid hotell. Registrering är föredragen gjord online före vistelsen.",
|
||||
"booking.confirmation.text": "Tack för att du bokar hos oss! Vi ser fram emot att välkomna dig och hoppas att du får en trevlig vistelse. Om du har några frågor eller behöver göra ändringar i din bokning, vänligen <emailLink>kontakta oss.</emailLink>",
|
||||
"booking.confirmation.title": "Bokningsbekräftelse",
|
||||
"booking.guests": "Max {nrOfGuests, plural, one {# gäst} other {# gäster}}",
|
||||
|
||||
@@ -1 +1,13 @@
|
||||
import "@testing-library/jest-dom"
|
||||
|
||||
jest.mock("react-intl", () => ({
|
||||
useIntl: () => ({
|
||||
formatMessage: (message: { id: string }) => message.id,
|
||||
}),
|
||||
}))
|
||||
|
||||
jest.mock("next/navigation", () => ({
|
||||
useRouter: jest.fn(),
|
||||
usePathname: jest.fn().mockReturnValue("/"),
|
||||
useParams: jest.fn().mockReturnValue({ lang: "en" }),
|
||||
}))
|
||||
|
||||
Reference in New Issue
Block a user