From 18dd08f10e9d32a73cc9f1222b4bd91fc0613f8e Mon Sep 17 00:00:00 2001 From: Tobias Johansson Date: Wed, 8 Jan 2025 12:31:30 +0000 Subject: [PATCH] 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 --- .../Confirmation/index.tsx | 19 +++ .../EnterDetails/Details/schema.ts | 15 +- .../TempDesignSystem/Form/Date/date.test.tsx | 138 ++++++++++++++++++ .../TempDesignSystem/Form/Date/index.tsx | 19 ++- i18n/dictionaries/da.json | 2 + i18n/dictionaries/de.json | 2 + i18n/dictionaries/en.json | 2 + i18n/dictionaries/fi.json | 2 + i18n/dictionaries/no.json | 2 + i18n/dictionaries/sv.json | 2 + jest.setup.ts | 12 ++ 11 files changed, 208 insertions(+), 7 deletions(-) create mode 100644 components/TempDesignSystem/Form/Date/date.test.tsx diff --git a/components/HotelReservation/BookingConfirmation/Confirmation/index.tsx b/components/HotelReservation/BookingConfirmation/Confirmation/index.tsx index 71efd978d..3512d66e1 100644 --- a/components/HotelReservation/BookingConfirmation/Confirmation/index.tsx +++ b/components/HotelReservation/BookingConfirmation/Confirmation/index.tsx @@ -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(null) + + const failedToVerifyMembership = + booking.rateDefinition.isMemberRate && !booking.guest.membershipNumber + return (
+ {failedToVerifyMembership && ( + + )} diff --git a/components/HotelReservation/EnterDetails/Details/schema.ts b/components/HotelReservation/EnterDetails/Details/schema.ts index cbe8e2eae..8371fe31f 100644 --- a/components/HotelReservation/EnterDetails/Details/schema.ts +++ b/components/HotelReservation/EnterDetails/Details/schema.ts @@ -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(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(""), }) ) diff --git a/components/TempDesignSystem/Form/Date/date.test.tsx b/components/TempDesignSystem/Form/Date/date.test.tsx new file mode 100644 index 000000000..0b2fd4183 --- /dev/null +++ b/components/TempDesignSystem/Form/Date/date.test.tsx @@ -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 + children: React.ReactNode + onSubmit: (data: unknown) => void +} + +function FormWrapper({ defaultValues, children, onSubmit }: FormWrapperProps) { + const methods = useForm({ + defaultValues, + }) + return ( + +
onSubmit(data))}> + {children} + +
+
+ ) +} + +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( + + + + ) + + 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) + ) + } + ) +}) diff --git a/components/TempDesignSystem/Form/Date/index.tsx b/components/TempDesignSystem/Form/Date/index.tsx index ae9d98818..008213b01 100644 --- a/components/TempDesignSystem/Form/Date/index.tsx +++ b/components/TempDesignSystem/Form/Date/index.tsx @@ -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} > diff --git a/i18n/dictionaries/da.json b/i18n/dictionaries/da.json index 7051fac39..2c988c4f0 100644 --- a/i18n/dictionaries/da.json +++ b/i18n/dictionaries/da.json @@ -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 kontakte os.", "booking.confirmation.title": "Booking bekræftelse", "booking.guests": "Maks {nrOfGuests, plural, one {# gæst} other {# gæster}}", diff --git a/i18n/dictionaries/de.json b/i18n/dictionaries/de.json index 7954874e0..881059090 100644 --- a/i18n/dictionaries/de.json +++ b/i18n/dictionaries/de.json @@ -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, kontaktieren Sie uns bitte..", "booking.confirmation.title": "Buchungsbestätigung", "booking.guests": "Max {nrOfGuests, plural, one {# gast} other {# gäste}}", diff --git a/i18n/dictionaries/en.json b/i18n/dictionaries/en.json index 771290e0a..d58076530 100644 --- a/i18n/dictionaries/en.json +++ b/i18n/dictionaries/en.json @@ -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 contact us.", "booking.confirmation.title": "Booking confirmation", "booking.guests": "Max {nrOfGuests, plural, one {# guest} other {# guests}}", diff --git a/i18n/dictionaries/fi.json b/i18n/dictionaries/fi.json index 4b242c84e..707506af9 100644 --- a/i18n/dictionaries/fi.json +++ b/i18n/dictionaries/fi.json @@ -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, ota meihin yhteyttä.", "booking.confirmation.title": "Varausvahvistus", "booking.guests": "Max {nrOfGuests, plural, one {# vieras} other {# vieraita}}", diff --git a/i18n/dictionaries/no.json b/i18n/dictionaries/no.json index 3fa2f494f..5e4948dc6 100644 --- a/i18n/dictionaries/no.json +++ b/i18n/dictionaries/no.json @@ -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 kontakt oss.", "booking.confirmation.title": "Bestillingsbekreftelse", "booking.guests": "Maks {nrOfGuests, plural, one {# gjest} other {# gjester}}", diff --git a/i18n/dictionaries/sv.json b/i18n/dictionaries/sv.json index 1fae7caf6..ecdd2e364 100644 --- a/i18n/dictionaries/sv.json +++ b/i18n/dictionaries/sv.json @@ -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 kontakta oss.", "booking.confirmation.title": "Bokningsbekräftelse", "booking.guests": "Max {nrOfGuests, plural, one {# gäst} other {# gäster}}", diff --git a/jest.setup.ts b/jest.setup.ts index df6631eeb..6d866f13c 100644 --- a/jest.setup.ts +++ b/jest.setup.ts @@ -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" }), +}))