diff --git a/frontend/cypress/e2e/FormStaffPage.cy.ts b/frontend/cypress/e2e/FormStaffPage.cy.ts index c526338c39..5ba2a0ada4 100644 --- a/frontend/cypress/e2e/FormStaffPage.cy.ts +++ b/frontend/cypress/e2e/FormStaffPage.cy.ts @@ -99,4 +99,172 @@ describe("Staff Form", () => { }); + describe("when the user clicks the Create client button", () => { + beforeEach(() => { + cy.login("uattest@gov.bc.ca", "Uat Test", "idir", { + given_name: "James", + family_name: "Baxter", + "cognito:groups": ["CLIENT_ADMIN"], + }); + + // Check if the Create client button is visible + cy.get('#menu-list-staff-form') + .should('be.visible') + .click(); + }); + + it("should display the Client type input field", () => { + cy.get("#clientType").should("be.visible").and("have.value", ""); + }); + + it("should not display any Client type specific input fields", () => { + cy.get("#firstName").should("not.exist"); + }); + + describe("when option Individual gets selected", () => { + beforeEach(() => { + cy.get("#clientType") + .should("be.visible") + .and("have.value", "") + .find("[part='trigger-button']") + .click(); + + cy.get("#clientType") + .find('cds-combo-box-item[data-id="I"]') + .should("be.visible") + .click() + .and("have.value", "Individual"); + }); + it("should display the Individual information input fields", () => { + cy.contains("h2", "Client information"); + cy.get("#firstName").should("be.visible"); + cy.get("#middleName").should("be.visible"); + cy.get("#lastName").should("be.visible"); + cy.get("#birthdate").should("be.visible"); + cy.get("#identificationType").should("be.visible"); + cy.get("#clientIdentification").should("be.visible"); + }); + + describe("when all the required information is filled in", () => { + const baseData = { + firstName: "John", + middleName: "Michael", + lastName: "Silver", + birthdateYear: "2001", + birthdateMonth: "05", + birthdateDay: "30", + identificationTypeValue: "Canadian passport", + identificationProvinceValue: undefined, + clientIdentification: "AB345678", + }; + const scenarios = [ + { + name: "and the selected ID type doesn't require Issuing province", + data: { + ...baseData, + }, + }, + { + name: "and the selected ID type requires Issuing province", + data: { + ...baseData, + identificationTypeValue: "Canadian driver's licence", + identificationProvinceValue: "Nova Scotia", + }, + }, + ]; + scenarios.forEach(({ name, data }) => { + describe(name, () => { + beforeEach(() => { + cy.get("#firstName").shadow().find("input").type(data.firstName); + + cy.get("#middleName").shadow().find("input").type(data.middleName); + + cy.get("#lastName").shadow().find("input").type(data.lastName); + + cy.get("#birthdateYear").shadow().find("input").type(data.birthdateYear); + cy.get("#birthdateMonth").shadow().find("input").type(data.birthdateMonth); + cy.get("#birthdateDay").shadow().find("input").type(data.birthdateDay); + + cy.get("#identificationType").find("[part='trigger-button']").click(); + cy.get("#identificationType") + .find(`cds-combo-box-item[data-value="${data.identificationTypeValue}"]`) + .click(); + + if (data.identificationProvinceValue) { + cy.get("#identificationProvince").find("[part='trigger-button']").click(); + cy.get("#identificationProvince") + .find(`cds-combo-box-item[data-value="${data.identificationProvinceValue}"]`) + .click(); + } + + cy.get("#clientIdentification") + .shadow() + .find("input") + .type(data.clientIdentification); + + cy.get("#clientIdentification").shadow().find("input").blur(); + }); + it("enables the button Next", () => { + cy.get("[data-test='wizard-next-button']") + .shadow() + .find("button") + .should("be.enabled"); + }); + + describe("and the button Next is clicked", () => { + beforeEach(() => { + cy.get("[data-test='wizard-next-button']").click(); + }); + it("hides the Client information section", () => { + cy.contains("h2", "Client information").should("not.exist"); + }); + describe("and the button Back is clicked", () => { + beforeEach(() => { + cy.get("[data-test='wizard-back-button']").click(); + }); + it("renders the Individual input fields with the same data", () => { + cy.get("#firstName").shadow().find("input").should("have.value", data.firstName); + + cy.get("#middleName") + .shadow() + .find("input") + .should("have.value", data.middleName); + + cy.get("#lastName").shadow().find("input").should("have.value", data.lastName); + + cy.get("#birthdateYear") + .shadow() + .find("input") + .should("have.value", data.birthdateYear); + cy.get("#birthdateMonth") + .shadow() + .find("input") + .should("have.value", data.birthdateMonth); + cy.get("#birthdateDay") + .shadow() + .find("input") + .should("have.value", data.birthdateDay); + + cy.get("#identificationType").should("have.value", data.identificationTypeValue); + + if (data.identificationProvinceValue) { + cy.get("#identificationProvince").should( + "have.value", + data.identificationProvinceValue, + ); + } + + cy.get("#clientIdentification") + .shadow() + .find("input") + .should("have.value", data.clientIdentification); + }); + }); + }); + }); + }); + }); + }); + }); }); diff --git a/frontend/src/assets/styles/global.scss b/frontend/src/assets/styles/global.scss index 9ca4536c0e..89e14f09c6 100644 --- a/frontend/src/assets/styles/global.scss +++ b/frontend/src/assets/styles/global.scss @@ -98,6 +98,7 @@ The value is in pixels instead of rem because it shouldn't vary with text resize. */ --scroll-bar-width-px: 15; + --scroll-bar-width: calc(1rem * var(--scroll-bar-width-px) / 16); } @media screen and (hover: none) { @@ -275,6 +276,12 @@ cds-combo-box-item[data-loading="true"]::part( border-top: 0; } +cds-combo-box#identificationProvince::part(menu-body) { + width: min-content; + min-width: 100%; + max-width: 200%; +} + .top-notification { margin-top: 3rem; display: flex; @@ -648,7 +655,7 @@ cds-actionable-notification * { align-items: flex-start; } -.form-steps { +.form-steps, .form-steps-staff { flex-grow: 1; align-self: stretch; display: flex; @@ -657,6 +664,10 @@ cds-actionable-notification * { max-width: 50.4375rem; } +.form-steps-staff { + max-width: 42.5rem; +} + .form-steps-01 { display: flex; flex-direction: column; @@ -807,6 +818,22 @@ cds-actionable-notification * { background-blend-mode: multiply; } +.form-steps-staff :is( + .grouping-02, + .grouping-03 +) { + width: 42.5rem; +} + +.grouping-03:has(#identificationType) { + width: 20.75rem; +} + +.grouping-03:has(#identificationProvince), +.grouping-02:has(#clientIdentification) { + width: 9.875rem; +} + .grouping-04 { align-self: stretch; display: flex; @@ -1005,6 +1032,12 @@ cds-actionable-notification * { margin: 0.5rem 0; } +.horizontal-input-grouping { + display: flex; + flex-direction: row; + gap: 1rem; +} + .label-01 { align-self: stretch; padding-bottom: 0.25rem; @@ -1069,6 +1102,16 @@ cds-text-input::part(label)::before, .cds-text-input-required-label { color: var(--light-theme-text-text-error, #b32001); } +.label-with-icon .cds-text-input-label { + padding-bottom: 0; +} + +.label-with-icon { + padding-bottom: 1rem; + display: flex; + gap: 0.5rem; +} + cds-text-input[data-required-label="true"]::part(label)::before { content: '* '; } @@ -1774,8 +1817,10 @@ Useful for scrolling to the *start* of an HTML element without having it covered margin-bottom: 2rem; } .submission-content { - padding: 2.5rem 2rem 2.5rem 18.5rem; - max-width: calc(100vw - 21rem); + --padding-right: 2rem; + --padding-left: 18.5rem; + padding: 2.5rem var(--padding-right) 2.5rem var(--padding-left); + max-width: calc(100vw - (var(--padding-right) + var(--padding-left) + var(--scroll-bar-width))); } #datatable { @@ -1843,8 +1888,10 @@ Useful for scrolling to the *start* of an HTML element without having it covered } .submission-content { - padding: 2.5rem 2rem 2.5rem 18.5rem; - max-width: calc(100vw - 21rem); + --padding-right: 2rem; + --padding-left: 18.5rem; + padding: 2.5rem var(--padding-right) 2.5rem var(--padding-left); + max-width: calc(100vw - (var(--padding-right) + var(--padding-left) + var(--scroll-bar-width))); } } @@ -1892,8 +1939,10 @@ Useful for scrolling to the *start* of an HTML element without having it covered } .submission-content { - padding: 2.5rem 2rem 2.5rem 18.5rem; - max-width: calc(100vw - 21rem); + --padding-right: 2rem; + --padding-left: 18.5rem; + padding: 2.5rem var(--padding-right) 2.5rem var(--padding-left); + max-width: calc(100vw - (var(--padding-right) + var(--padding-left) + var(--scroll-bar-width))); } } @@ -1927,7 +1976,9 @@ Useful for scrolling to the *start* of an HTML element without having it covered } .submission-content { - padding: 2.5rem 2.5rem 2.5rem 18.5rem; - max-width: calc(100vw - 21rem); + --padding-right: 2.5rem; + --padding-left: 18.5rem; + padding: 2.5rem var(--padding-right) 2.5rem var(--padding-left); + max-width: calc(100vw - (var(--padding-right) + var(--padding-left) + var(--scroll-bar-width))); } } diff --git a/frontend/src/components/forms/DropdownInputComponent.vue b/frontend/src/components/forms/DropdownInputComponent.vue index 0cbfec2cb0..727e50fa81 100644 --- a/frontend/src/components/forms/DropdownInputComponent.vue +++ b/frontend/src/components/forms/DropdownInputComponent.vue @@ -105,7 +105,7 @@ const comboBoxMountTime = ref<[number]>([Date.now()]); //Watch for changes on the input watch([selectedValue], () => { - if (selectedValue.value === "") { + if (!selectedValue.value) { comboBoxMountTime.value = [Date.now()]; } const reference = selectedValue.value diff --git a/frontend/src/dto/ApplyClientNumberDto.ts b/frontend/src/dto/ApplyClientNumberDto.ts index 571cf9e580..65a5857eeb 100644 --- a/frontend/src/dto/ApplyClientNumberDto.ts +++ b/frontend/src/dto/ApplyClientNumberDto.ts @@ -29,6 +29,13 @@ export interface FormDataDto { goodStandingInd: string; birthdate: string; address: Address; + firstName?: string; + middleName?: string; + lastName?: string; + identificationType?: string; + clientIdentification?: string; + identificationCountry?: string; + identificationProvince?: string; }; location: { addresses: Address[]; diff --git a/frontend/src/dto/CommonTypesDto.ts b/frontend/src/dto/CommonTypesDto.ts index d3ff59e5ae..b519ad2e26 100644 --- a/frontend/src/dto/CommonTypesDto.ts +++ b/frontend/src/dto/CommonTypesDto.ts @@ -67,6 +67,20 @@ export enum ClientTypeEnum { USP, } +export enum IdentificationTypeEnum { + BRTH, + CDDL, + PASS, + CITZ, + FNID, + USDL, + OTHR, +} + +export interface IdentificationType extends CodeNameType { + code: keyof typeof IdentificationTypeEnum; +} + export interface ProgressData { kind: string; title: string; diff --git a/frontend/src/helpers/CustomDirectives.ts b/frontend/src/helpers/CustomDirectives.ts index bf70d7b2da..24a5ffb4f2 100644 --- a/frontend/src/helpers/CustomDirectives.ts +++ b/frontend/src/helpers/CustomDirectives.ts @@ -1,5 +1,8 @@ /* eslint-disable @typescript-eslint/explicit-module-boundary-types */ -import { mask } from 'vue-the-mask' +import { mask, tokens } from "vue-the-mask"; + +// add custom token +tokens.N = { pattern: /[0-9a-zA-Z]/, transform: (v) => v.toLocaleUpperCase() }; export const masking = (shadowSelector: string) => (el: any, binding: any) => { if (el.shadowRoot && binding.value) { diff --git a/frontend/src/helpers/validators/GlobalValidators.ts b/frontend/src/helpers/validators/GlobalValidators.ts index 9f48851a99..a51280c95b 100644 --- a/frontend/src/helpers/validators/GlobalValidators.ts +++ b/frontend/src/helpers/validators/GlobalValidators.ts @@ -9,12 +9,13 @@ import type { ValidationMessageType } from "@/dto/CommonTypesDto"; // Defines the used regular expressions // @sonar-ignore-next-line const emailRegex: RegExp = /^[a-zA-Z0-9._-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,4}$/; -const specialCharacters: RegExp = /^[a-zA-Z0-9\s]+$/; +const specialCharacters: RegExp = /^[a-zA-Z0-9\s]*$/; +const idCharacters: RegExp = /^[A-Z0-9]*$/; const e164Regex: RegExp = /^((\+?[1-9]\d{1,14})|(\(\d{3}\) \d{3}-\d{4}))$/; const canadianPostalCodeRegex: RegExp = /^(([A-Z]\d){3})$/i; const usZipCodeRegex: RegExp = /^\d{5}(?:[-\s]\d{4})?$/; -const nameRegex: RegExp = /^[a-zA-Z0-9\s'-]+$/; -const ascii: RegExp = /^[\x20-\x7e]+$/; +const nameRegex: RegExp = /^[a-zA-Z0-9\s'-]*$/; +const ascii: RegExp = /^[\x20-\x7e]*$/; const notificationBus = useEventBus( "error-notification" @@ -158,8 +159,7 @@ export const isMaxSize = (message: string = "This field must be smaller") => (maxSize: number) => { return (value: string): string => { - if (isNotEmpty(message)(value) === "" && value.length <= maxSize) - return ""; + if (!value || value.length <= maxSize) return ""; return message; }; }; @@ -266,6 +266,13 @@ export const isNoSpecialCharacters = return message; }; +export const isIdCharacters = + (message: string = "This field can only contain: A-Z or 0-9") => + (value: string): string => { + if (idCharacters.test(value)) return ""; + return message; + }; + export const hasOnlyNamingCharacters = ( field: string = "field", @@ -402,6 +409,34 @@ export const isDateInThePast = (message: string) => (value: string) => { return ""; }; +export const isRegex = + ( + regex: RegExp, + message: string = `This field must conform to the following regular expression: ${regex}`, + ) => + (value: string): string => { + if (regex.test(value)) return ""; + return message; + }; + +/** + * Allows to extract a portion of the value to be validated and apply the validation only to this portion. + * @param selector - a function that returns the portion of the string you want to validate + * @param selectorErrorMessage - message returned only in case of an error thrown by the selector + * @returns A function that accepts a validator and applies it to the portion of the string obtained by applying the selector. + */ +export const validateSelection = + (selector: (value: string) => string, selectorErrorMessage = "Value could not be validated") => + (validator: (value: string) => string) => + (value: string): string => { + try { + const selected = selector(value); + return validator(selected); + } catch (error) { + return selectorErrorMessage; + } + }; + /** * Retrieves the value of a field in an object or array based on a given path. * If the field is an array, it returns an array of values. @@ -568,6 +603,8 @@ export const formFieldValidations: Record< export const getValidations = (key: string): ((value: string) => string)[] => formFieldValidations[key] || []; +const defaultGetValidations = getValidations; + const arrayIndexGlobalRegex = /\.\*\./g; const targetGlobalRegex = /\$\./g; @@ -575,8 +612,9 @@ const targetGlobalRegex = /\$\./g; export const validate = ( keys: string[], target: any, - notify: boolean = false -): boolean => { + notify: boolean = false, + getValidations = defaultGetValidations, +): boolean => { // For every received key we get the validations and run them return keys.every((key) => { // First we get all validators for that field diff --git a/frontend/src/helpers/validators/StaffFormValidations.ts b/frontend/src/helpers/validators/StaffFormValidations.ts new file mode 100644 index 0000000000..47d3d41ff0 --- /dev/null +++ b/frontend/src/helpers/validators/StaffFormValidations.ts @@ -0,0 +1,199 @@ +import { + isNotEmpty, + isMaxSize, + isMinSize, + isOnlyNumbers, + isMinimumYearsAgo, + isDateInThePast, + hasOnlyNamingCharacters, + isIdCharacters, + isRegex, + validateSelection, + formFieldValidations as externalFormFieldValidations, + validate as globalValidate, + runValidation as globalRunValidation, +} from "@/helpers/validators/GlobalValidators"; + +// Allow externalFormFieldValidations to get populated +import "@/helpers/validators/BCeIDFormValidations"; + +/* +Start by grabbing the same validations we use on the external form. +And just change / add what's different. +*/ +const fieldValidations: Record string)[]> = { + ...externalFormFieldValidations, +}; + +// This function will return all validators for the field +export const getValidations = (key: string): ((value: any) => string)[] => + fieldValidations[key] || []; + +const isMinSizeMsg = (fieldName: string, minSize: number) => + isMinSize(`The ${fieldName} must contain at least ${minSize} characters`)(minSize); + +const isMaxSizeMsg = (fieldName: string, maxSize: number) => + isMaxSize(`The ${fieldName} has a ${maxSize} character limit`)(maxSize); + +const isExactSizMsg = (fieldName: string, size: number) => { + const msg = `The ${fieldName} must contain ${size} characters`; + return [isMinSize(msg)(size), isMaxSize(msg)(size)]; +}; + +// Step 1: Business Information +fieldValidations["businessInformation.birthdate"] = [ + isDateInThePast("Date of birth must be in the past"), + isMinimumYearsAgo(19, "The applicant must be at least 19 years old to apply"), +]; + +// use the same validations as firstName in contacts +fieldValidations["businessInformation.firstName"] = [ + ...fieldValidations["location.contacts.*.firstName"], +]; + +fieldValidations["businessInformation.middleName"] = [ + isMaxSizeMsg("middle name", 30), + hasOnlyNamingCharacters("middle name"), +]; + +// use the same validations as lastName in contacts +fieldValidations["businessInformation.lastName"] = [ + ...fieldValidations["location.contacts.*.lastName"], +]; + +// For the input field. +fieldValidations["identificationType.text"] = [isNotEmpty("You must select an ID type.")]; + +// For the input field. +fieldValidations["identificationProvince.text"] = [isNotEmpty("You must select a value.")]; + +// For the form data. +fieldValidations["businessInformation.identificationType"] = [ + isNotEmpty("You must select an ID type (and related additional fields if any)."), +]; + +interface ClientIdentificationValidation { + maxSize: number; + onlyNumbers?: boolean; +} + +/* +Variable defined using an IIFE to allow an easy definition of type-checkable keys. +*/ +export const clientIdentificationMaskParams = (() => { + const init = { + BCDL: { + maxSize: 8, + onlyNumbers: true, + }, + nonBCDL: { + maxSize: 20, + }, + BRTH: { + maxSize: 13, + onlyNumbers: true, + }, + PASS: { + maxSize: 8, + }, + CITZ: { + maxSize: 8, + }, + FNID: { + maxSize: 10, + onlyNumbers: true, + }, + OTHR: undefined, + }; + return init as Record; +})(); + +type ClientIdentificationFormFieldValidationKey = + `businessInformation.clientIdentification-${keyof typeof clientIdentificationMaskParams}`; + +const createClientIdentificationFieldValidations = ( + validations: Record string)[]>, +) => validations; + +// clientIdentification base validations - applied regardless of the ID type / province. +fieldValidations["businessInformation.clientIdentification"] = [ + isNotEmpty("You must provide an ID number"), +]; + +const extractOtherId = (value: string) => value.split(":")[1]?.trim(); + +Object.assign( + fieldValidations, + createClientIdentificationFieldValidations({ + "businessInformation.clientIdentification-BCDL": [ + isOnlyNumbers("BC driver's licence should contain only numbers"), + isMinSizeMsg("BC driver's licence", 7), + isMaxSizeMsg("BC driver's licence", clientIdentificationMaskParams.BCDL.maxSize), + ], + + "businessInformation.clientIdentification-nonBCDL": [ + isIdCharacters(), + isMinSizeMsg("driver's licence", 7), + isMaxSizeMsg("driver's licence", clientIdentificationMaskParams.nonBCDL.maxSize), + ], + + "businessInformation.clientIdentification-BRTH": [ + isOnlyNumbers("Canadian birth certificate should contain only numbers"), + isMinSizeMsg("Canadian birth certificate", 12), + isMaxSizeMsg("Canadian birth certificate", clientIdentificationMaskParams.BRTH.maxSize), + ], + + "businessInformation.clientIdentification-PASS": [ + isIdCharacters(), + ...isExactSizMsg("Canadian passport", clientIdentificationMaskParams.PASS.maxSize), + ], + + "businessInformation.clientIdentification-CITZ": [ + isIdCharacters(), + ...isExactSizMsg("Canadian citizenship card", clientIdentificationMaskParams.CITZ.maxSize), + ], + + "businessInformation.clientIdentification-FNID": [ + isOnlyNumbers("First Nation status ID should contain only numbers"), + ...isExactSizMsg("First Nation status ID", clientIdentificationMaskParams.FNID.maxSize), + ], + + "businessInformation.clientIdentification-OTHR": [ + isMinSizeMsg("ID number", 3), + isMaxSizeMsg("ID number", 40), + isRegex( + /^[^:]+:\s?[^\s:]+$/, + 'Other identification must follow the pattern: [ID Type] : [ID Value] such as "USA Passport : 12345"', + ), + validateSelection(extractOtherId)( + isIdCharacters("The value to right of the colon can only contain: A-Z or 0-9"), + ), + validateSelection(extractOtherId)(isMinSizeMsg("value to right of the colon", 3)), + ], + }), +); + +export const getClientIdentificationValidations = ( + key: ClientIdentificationFormFieldValidationKey, +): ((value: any) => string)[] => getValidations(key); + +// Step 2: Addresses + +// Step 3: Contacts + +export const addValidation = (key: string, validation: (value: string) => string): void => { + if (!fieldValidations[key]) fieldValidations[key] = []; + fieldValidations[key].push(validation); +}; + +const defaultGetValidations = getValidations; + +export const validate = ( + ...args: Parameters +): ReturnType => { + const getValidations = args[3] || defaultGetValidations; + args[3] = getValidations; + return globalValidate.apply(this, args); +}; + +export const runValidation = globalRunValidation; diff --git a/frontend/src/pages/FormStaffPage.vue b/frontend/src/pages/FormStaffPage.vue index 3d828227bd..7de7536fca 100644 --- a/frontend/src/pages/FormStaffPage.vue +++ b/frontend/src/pages/FormStaffPage.vue @@ -1,5 +1,5 @@ diff --git a/frontend/src/pages/staffform/IndividualClientInformationWizardStep.vue b/frontend/src/pages/staffform/IndividualClientInformationWizardStep.vue new file mode 100644 index 0000000000..407b704edf --- /dev/null +++ b/frontend/src/pages/staffform/IndividualClientInformationWizardStep.vue @@ -0,0 +1,390 @@ + + + diff --git a/frontend/tests/pages/staff/IndividualClientInformationWizardStep.cy.ts b/frontend/tests/pages/staff/IndividualClientInformationWizardStep.cy.ts new file mode 100644 index 0000000000..b7b2ab18c2 --- /dev/null +++ b/frontend/tests/pages/staff/IndividualClientInformationWizardStep.cy.ts @@ -0,0 +1,469 @@ +import IndividualClientInformationWizardStep from "@/pages/staffform/IndividualClientInformationWizardStep.vue"; +import type { FormDataDto } from "@/dto/ApplyClientNumberDto"; + +describe("", () => { + beforeEach(() => { + cy.intercept("GET", "/api/countries/CA/provinces?page=0&size=250", { + fixture: "provinces.json", + }).as("getProvinces"); + + cy.intercept("GET", "/api/countries/US/provinces?page=0&size=250", { + fixture: "states.json", + }).as("getStates"); + }); + + const getDefaultProps = () => ({ + data: { + businessInformation: { + businessType: "", + legalType: "", + clientType: "", + registrationNumber: "", + businessName: "", + goodStandingInd: "", + birthdate: "", + address: "", + }, + location: { + contacts: [ + { + firstName: "", + lastName: "", + }, + ], + }, + } as unknown as FormDataDto, + }); + + let currentProps = null; + const mount = (props = getDefaultProps()) => { + currentProps = props; + return cy + .mount(IndividualClientInformationWizardStep, { + props, + }) + .its("wrapper") + .as("vueWrapper"); + }; + + it("renders the IndividualClientInformationWizardStep component", () => { + mount(); + + cy.get("#firstName").should("be.visible"); + cy.get("#middleName").should("be.visible"); + cy.get("#lastName").should("be.visible"); + cy.get("#birthdate").should("be.visible"); + cy.get("#identificationType").should("be.visible"); + cy.get("#clientIdentification").should("be.visible"); + }); + + describe('when ID type is "Canadian driver\'s licence"', () => { + beforeEach(() => { + mount(); + + cy.get("#identificationType") + .should("be.visible") + .and("have.value", "") + .find("[part='trigger-button']") + .click(); + + cy.get("#identificationType") + .find('cds-combo-box-item[data-id="CDDL"]') + .should("be.visible") + .click(); + }); + it("displays the Issuing province field with label 'Issuing province'", () => { + cy.get("#identificationProvince").contains("Issuing province"); + }); + it("displays the Issuing province field with option 'British Columbia' selected by default", () => { + cy.get("#identificationProvince").should("be.visible").and("have.value", "British Columbia"); + }); + + describe('and ID type is changed to "US driver\'s licence"', () => { + beforeEach(() => { + cy.get("#identificationType") + .should("be.visible") + .and("have.value", "Canadian driver's licence") // initial value + .find("[part='trigger-button']") + .click(); + + cy.get("#identificationType") + .find('cds-combo-box-item[data-id="USDL"]') + .should("be.visible") + .and("have.value", "US driver's licence") // new value + .click(); + }); + it("should clear the value on identificationProvince", () => { + cy.get("#identificationProvince").should("be.visible").and("have.value", ""); + }); + }); + }); + + describe('when ID type is "US driver\'s licence"', () => { + beforeEach(() => { + mount(); + + cy.get("#identificationType") + .should("be.visible") + .and("have.value", "") + .find("[part='trigger-button']") + .click(); + + cy.get("#identificationType") + .find('cds-combo-box-item[data-id="USDL"]') + .should("be.visible") + .click(); + }); + + it("displays the Issuing province field with label 'Issuing state'", () => { + cy.get("#identificationProvince").contains("Issuing state"); + }); + it("displays the Issuing province field with option 'British Columbia' selected by default", () => { + cy.get("#identificationProvince").should("be.visible").and("have.value", ""); + }); + }); + + describe("validation", () => { + describe("ID number according to the current ID type (and identificationProvince)", () => { + beforeEach(() => { + mount(); + }); + const valueNumeric13 = "1234567890123"; + const valueAlphanumeric13 = "1234567890ABC"; + const valueNumeric8 = "12345678"; + const valueAlphanumeric8 = "12345ABC"; + + const isValid = () => { + cy.get("#clientIdentification").shadow().find("[name='invalid-text']").should("not.exist"); + }; + + const isInvalid = () => { + cy.get("#clientIdentification") + .shadow() + .find("[name='invalid-text']") + .invoke("text") + .should("not.be.empty"); + }; + + describe("When ID type is 'BRTH'", () => { + beforeEach(() => { + cy.get("#identificationType") + .should("be.visible") + .and("have.value", "") + .find("[part='trigger-button']") + .click(); + + cy.get("#identificationType") + .find('cds-combo-box-item[data-id="BRTH"]') + .should("be.visible") + .click(); + }); + it("allows up to 13 digits", () => { + cy.get("#clientIdentification").shadow().find("input").type(valueNumeric13); + + cy.get("#clientIdentification").shadow().find("input").blur(); + cy.wait(1); + + isValid(); + }); + it("displays error message if the value contains letters", () => { + cy.get("#clientIdentification").shadow().find("input").type(valueAlphanumeric13); + + cy.get("#clientIdentification").shadow().find("input").blur(); + cy.wait(1); + + isInvalid(); + }); + }); + describe("when ID type is 'PASS'", () => { + beforeEach(() => { + cy.get("#identificationType") + .should("be.visible") + .and("have.value", "") + .find("[part='trigger-button']") + .click(); + + cy.get("#identificationType") + .find('cds-combo-box-item[data-id="PASS"]') + .should("be.visible") + .click(); + }); + it("displays error message if the value has more than 8 digits", () => { + cy.get("#clientIdentification").shadow().find("input").type(valueNumeric13); + + cy.get("#clientIdentification").shadow().find("input").blur(); + cy.wait(1); + + isInvalid(); + }); + it("allows numbers and letters", () => { + cy.get("#clientIdentification").shadow().find("input").type(valueAlphanumeric8); + + cy.get("#clientIdentification").shadow().find("input").blur(); + cy.wait(1); + + isValid(); + }); + }); + describe("when ID type is 'CDDL'", () => { + beforeEach(() => { + cy.get("#identificationType") + .should("be.visible") + .and("have.value", "") + .find("[part='trigger-button']") + .click(); + + cy.get("#identificationType") + .find('cds-combo-box-item[data-id="CDDL"]') + .should("be.visible") + .click(); + }); + describe("and issuing province is 'BC' (default value)", () => { + it("displays error message if the value has more than 8 digits", () => { + cy.get("#clientIdentification").shadow().find("input").type(valueNumeric13); + + cy.get("#clientIdentification").shadow().find("input").blur(); + cy.wait(1); + + isInvalid(); + }); + it("displays error message if the value contains letters", () => { + cy.get("#clientIdentification").shadow().find("input").type(valueAlphanumeric8); + + cy.get("#clientIdentification").shadow().find("input").blur(); + cy.wait(1); + + isInvalid(); + }); + it("allows 8-digit numeric value", () => { + cy.get("#clientIdentification").shadow().find("input").type(valueNumeric8); + + cy.get("#clientIdentification").shadow().find("input").blur(); + cy.wait(1); + + isValid(); + }); + }); + describe("and issuing province is something other than 'BC' (for example, 'AB')", () => { + beforeEach(() => { + cy.get("#identificationProvince") + .should("be.visible") + .and("have.value", "British Columbia") + .find("[part='trigger-button']") + .click(); + + cy.get("#identificationProvince") + .find('cds-combo-box-item[data-id="AB"]') + .should("be.visible") + .click(); + }); + it("allows up to 20 digits", () => { + cy.get("#clientIdentification").shadow().find("input").type("12345678901234567890"); + + cy.get("#clientIdentification").shadow().find("input").blur(); + cy.wait(1); + + isValid(); + }); + it("allows numbers and letters", () => { + cy.get("#clientIdentification").shadow().find("input").type(valueAlphanumeric8); + + cy.get("#clientIdentification").shadow().find("input").blur(); + cy.wait(1); + + isValid(); + }); + it("displays error message if the value has more than 20 digits", () => { + cy.get("#clientIdentification").shadow().find("input").type("123456789012345678901"); + + cy.get("#clientIdentification").shadow().find("input").blur(); + cy.wait(1); + + isInvalid(); + }); + }); + }); + }); + }); + + describe("businessInformation.businessName", () => { + beforeEach(() => { + mount(); + + cy.get("#firstName").shadow().find("input").type("John"); + cy.get("#lastName").shadow().find("input").type("Silver"); + }); + describe("when middleName is not provided", () => { + it("sets the businessName in the businessInformation to ' '", () => { + cy.wrap(currentProps.data.businessInformation) + .its("businessName") + .should("equal", "John Silver"); + }); + }); + describe("when middleName is provided", () => { + beforeEach(() => { + cy.get("#middleName").shadow().find("input").type("Michael"); + }); + it("sets the businessName in the businessInformation to ' '", () => { + cy.wrap(currentProps.data.businessInformation) + .its("businessName") + .should("equal", "John Michael Silver"); + }); + }); + }); + + describe("when all required fields are properly filled in", () => { + beforeEach(() => { + mount(); + + cy.get("#firstName").shadow().find("input").type("John"); + + cy.get("#lastName").shadow().find("input").type("Silver"); + + cy.get("#birthdateYear").shadow().find("input").type("2001"); + cy.get("#birthdateMonth").shadow().find("input").type("05"); + cy.get("#birthdateDay").shadow().find("input").type("30"); + + cy.get("#identificationType").find("[part='trigger-button']").click(); + + cy.get("#identificationType").find('cds-combo-box-item[data-id="BRTH"]').click(); + + cy.get("#clientIdentification").shadow().find("input").type("1234567890123"); + + cy.get("#clientIdentification").shadow().find("input").blur(); + cy.wait(1); + }); + it("emits valid true", () => { + cy.get("@vueWrapper").should((vueWrapper) => { + const lastValid = vueWrapper.emitted("valid").slice(-1)[0]; + + // Last event (in)"valid" emitted + expect(lastValid[0]).to.equal(true); + }); + }); + describe("when the middle name is also filled in", () => { + beforeEach(() => { + cy.get("#middleName").shadow().find("input").type("Michael"); + + cy.get("#middleName").shadow().find("input").blur(); + cy.wait(1); + }); + it("should not emit valid false even if the middle name gets cleared", () => { + cy.get("@vueWrapper").should((vueWrapper) => { + const lastValid = vueWrapper.emitted("valid").slice(-1)[0]; + + // Last event (in)"valid" emitted + expect(lastValid[0]).to.equal(true); + }); + + cy.get("#middleName").shadow().find("input").clear(); + + cy.get("#middleName").shadow().find("input").blur(); + cy.wait(1); + + cy.get("@vueWrapper").should((vueWrapper) => { + const lastValid = vueWrapper.emitted("valid").slice(-1)[0]; + + // Last event (in)"valid" emitted + expect(lastValid[0]).to.equal(true); + }); + }); + }); + describe("when the step is invalid due to an invalid middle name", () => { + beforeEach(() => { + cy.get("#middleName").shadow().find("input").type("é"); + + cy.get("#middleName").shadow().find("input").blur(); + cy.wait(1); + + // Asserting the step is currently invalid + cy.get("@vueWrapper").should((vueWrapper) => { + const lastValid = vueWrapper.emitted("valid").slice(-1)[0]; + + // Last event (in)"valid" emitted + expect(lastValid[0]).to.equal(false); + }); + }); + describe("and the middle name gets cleared", () => { + beforeEach(() => { + cy.get("#middleName").shadow().find("input").clear(); + }); + it("should emit valid true even without blurring the input", () => { + cy.get("@vueWrapper").should((vueWrapper) => { + const lastValid = vueWrapper.emitted("valid").slice(-1)[0]; + + // Last event (in)"valid" emitted + expect(lastValid[0]).to.equal(true); + }); + }); + }); + }); + + describe('when ID type is "Canadian driver\'s licence"', () => { + beforeEach(() => { + cy.get("#identificationType").should("be.visible").find("[part='trigger-button']").click(); + + cy.get("#identificationType") + .find('cds-combo-box-item[data-id="CDDL"]') + .should("be.visible") + .click(); + }); + + describe("and the Issuing province gets cleared", () => { + beforeEach(() => { + cy.get("#identificationProvince") + .should("be.visible") + .and("have.value", "British Columbia") + .find("#selection-button") // The X clear button + .click(); + }); + + it("should emit valid false", () => { + cy.get("@vueWrapper").should((vueWrapper) => { + const lastValid = vueWrapper.emitted("valid").slice(-1)[0]; + + // Last event (in)"valid" emitted + expect(lastValid[0]).to.equal(false); + }); + }); + + describe("and the ID type gets changed to one that does not display the Issuing province/state", () => { + beforeEach(() => { + cy.get("#identificationType") + .should("be.visible") + .and("have.value", "Canadian driver's licence") // initial value + .find("[part='trigger-button']") + .click(); + + cy.get("#identificationType") + .find('cds-combo-box-item[data-id="PASS"]') + .and("have.value", "Canadian passport") // new value + .should("be.visible") + .click(); + }); + describe("and the ID number is properly filled in", () => { + beforeEach(() => { + cy.get("#clientIdentification").shadow().find("input").type("12345678"); + + cy.get("#clientIdentification").shadow().find("input").blur(); + cy.wait(1); + }); + it("should emit valid true", () => { + /* + This test makes sure the impact of clearing the province (and thus making the + province field invalid) does not matter anymore after we get to a situation where the + province field is not used. + */ + cy.get("@vueWrapper").should((vueWrapper) => { + const lastValid = vueWrapper.emitted("valid").slice(-1)[0]; + + // Last event (in)"valid" emitted + expect(lastValid[0]).to.equal(true); + }); + }); + }); + }); + }); + }); + }); +}); diff --git a/frontend/tests/unittests/components/forms/DropdownInputComponent.spec.ts b/frontend/tests/unittests/components/forms/DropdownInputComponent.spec.ts index 4000b0486a..4fc1f58078 100644 --- a/frontend/tests/unittests/components/forms/DropdownInputComponent.spec.ts +++ b/frontend/tests/unittests/components/forms/DropdownInputComponent.spec.ts @@ -192,6 +192,29 @@ describe("DropdownInputComponent", () => { expect(wrapper.emitted("error")![0][0]).toBe(undefined); }); + it.each([[""], [undefined], [null]])( + "should clear the selected value when initialValue changes to a falsy value (%s)", + async (value) => { + const wrapper = mount(DropdownInputComponent, { + props: { + id: "test", + label: "test", + tip: "", + modelValue: [ + { code: "A", name: "Value A" }, + { code: "B", name: "Value B" }, + ], + initialValue: "Value A", + validations, + }, + }); + + await wrapper.setProps({ initialValue: value }); + + expect(wrapper.get("#test").element.value).toEqual(""); + }, + ); + it("should validate and emit no error if required", async () => { const wrapper = mount(DropdownInputComponent, { props: { diff --git a/frontend/tests/unittests/helpers/validators/GlobalValidators.spec.ts b/frontend/tests/unittests/helpers/validators/GlobalValidators.spec.ts index 2e5bfcd87d..b8406dbae8 100644 --- a/frontend/tests/unittests/helpers/validators/GlobalValidators.spec.ts +++ b/frontend/tests/unittests/helpers/validators/GlobalValidators.spec.ts @@ -15,6 +15,9 @@ import { isNot, hasOnlyNamingCharacters, isAscii, + isIdCharacters, + isRegex, + validateSelection, } from '@/helpers/validators/GlobalValidators' describe('GlobalValidators', () => { @@ -192,6 +195,12 @@ describe('GlobalValidators', () => { 'This field must be at most 5 characters long' ) }) + it('should return empty when isMaxSize is called on an empty string', () => { + expect(isMaxSize('This field must be at most 5 characters long')(5)('')).toBe('') + }) + it.each([[null], [undefined]])('should return empty when isMaxSize is called on %s', (value) => { + expect(isMaxSize('This field must be at most 5 characters long')(5)(value)).toBe('') + }) it('should return empty when isMinSize is called on a string with a size greater than the min size', () => { expect(isMinSize()(5)('123456')).toBe('') }) @@ -241,8 +250,8 @@ describe('GlobalValidators', () => { 'No special characters allowed' ) }) - it('should return an error message when isNoSpecialCharacters is called on an empty string', () => { - expect(isNoSpecialCharacters()('')).toBe('No special characters allowed') + it('should return empty when isNoSpecialCharacters is called on an empty string', () => { + expect(isNoSpecialCharacters()('')).toBe('') }) it('should return empty when content is contained in the list', () => { expect(isContainedIn(ref(['a', 'b']))('a')).toBe('') @@ -279,4 +288,117 @@ describe('GlobalValidators', () => { const result = isAscii()("AZaz09 '!@#$%_-+()/\\"); expect(result).toBe(""); }); + it("should return empty when value is an empty string", () => { + const result = isAscii()(""); + expect(result).toBe(""); + }); + describe("isIdCharacters", () => { + it.each([ + ["is an empty string", ""], + ["contains only A-Z and 0-9", "A1R4"], + ])("should return empty string when value %s", (_, value) => { + const result = isIdCharacters()(value); + expect(result).toBe(""); + }); + + it.each([ + ["contains a lower-case letter", "A1t2"], + ["contains a space", "A 12"], + ["contains a punctuation symbol", "A!12"], + ["contains an accented letter", "Á1F2"], + ["contains a hyphen", "A1-R4"], + ])("should return an error when value %s", (_, value) => { + const result = isIdCharacters()(value); + expect(result).not.toBe(""); + }); + }); + describe("isRegex", () => { + const sampleRegex = /\d-\d/; + it.each([ + ["when the regex is matched with the whole value", "1-2"], + ["when the regex is matched with part of the string", "s1-2e"], + ])("should return empty string %s", (_, value) => { + const result = isRegex(sampleRegex)(value); + expect(result).toBe(""); + }); + + const nullableRegex = /.*/; + it.each([ + ["even when the value is an empty string if the regex allows it", ""], + ])("should return empty string %s", (_, value) => { + const result = isRegex(nullableRegex)(value); + expect(result).toBe(""); + }); + + it.each([ + ["when the value is empty and doesn't match", ""], + ["when the value is not empty but doesn't match anyway", "12"], + ])("should return an error %s", (_, value) => { + const result = isRegex(sampleRegex)(value); + expect(result).not.toBe(""); + }); + }); + describe("validateSelection", () => { + describe("when the provided selector is a function that extracts the string portion after the colon", () => { + const extract = (value: string) => { + const index = value.indexOf(":") + 1; + return value.substring(index); + }; + describe("and there is a validation on max length as 5", () => { + const validator = isMaxSize()(5); + + it.each([ + ["when the extracted portion passes the validation", "prefix:1234"], + ])("should return an empty string %s (%s)", (_, value) => { + const result = validateSelection(extract)(validator)(value); + expect(result).toBe(""); + }); + + it.each([ + ["when the extracted portion doesn't pass the validation", "prefix:123456"], + ])("should return an error %s (%s)", (_, value) => { + const result = validateSelection(extract)(validator)(value); + expect(result).not.toBe(""); + }); + }); + describe("and there is a validation on min length as 3", () => { + const validator = isMinSize()(3); + + it.each([ + ["when the extracted portion passes the validation", "prefix:1234"], + ])("should return an empty string %s (%s)", (_, value) => { + const result = validateSelection(extract)(validator)(value); + expect(result).toBe(""); + }); + + it.each([ + ["when the extracted portion doesn't pass the validation", "prefix:12"], + ])("should return an error %s (%s)", (_, value) => { + const result = validateSelection(extract)(validator)(value); + expect(result).not.toBe(""); + }); + }); + }); + }); + describe("when the provided selector throws an error", () => { + const selector = (value: string) => { + throw new Error; + }; + + const validator = isMaxSize()(20); + + describe.each([ + ["and a onSelectorErrorMessage was not provided", undefined, "the default selector error message"], + ["and a onSelectorErrorMessage was provided", "Sorry, the extraction failed!", "the provided message"] + ])("%s", (_, selectorMessage, explanation) => { + it.each([ + ["regardless of the value", "anything"], + ])(`should return ${explanation}`, (_, value) => { + const result = validateSelection(selector, selectorMessage)(validator)(value); + const defaultSelectorMessage = "Value could not be validated"; + const message = selectorMessage !== undefined ? selectorMessage : defaultSelectorMessage; + expect(result).toBe(message); + }); + }); + }); }) diff --git a/frontend/tests/unittests/helpers/validators/StaffFormValidations.spec.ts b/frontend/tests/unittests/helpers/validators/StaffFormValidations.spec.ts new file mode 100644 index 0000000000..edb2a3c4c3 --- /dev/null +++ b/frontend/tests/unittests/helpers/validators/StaffFormValidations.spec.ts @@ -0,0 +1,268 @@ +import { describe, it, expect } from "vitest"; +import { newFormDataDto } from "@/dto/ApplyClientNumberDto"; +import { validate } from "@/helpers/validators/StaffFormValidations"; + +type Scenario = [any, boolean, string?]; + +const test = (target: any, key: string, setter: (value: any) => void, scenario: Scenario) => { + const [value, expected, reason] = scenario; + const name = + `should return ${expected} when value is: ${JSON.stringify(value)}` + + (reason ? ` (${reason})` : ""); + it(name, () => { + setter(value); + expect(validate([key], target)).toBe(expected); + }); +}; + +describe("validations", () => { + describe.each(["businessInformation.birthdate"])("%s", (key) => { + const formDataDto = newFormDataDto(); + const setter = (value: string) => { + formDataDto.businessInformation.birthdate = value; + }; + const today = new Date(); + const currentYear = today.getFullYear(); + const twentyYearsAgo = currentYear - 20; + const tenYearsAgo = currentYear - 10; + ([ + [`${twentyYearsAgo}-02-16`, true], + [`${tenYearsAgo}-02-16`, false], + ]).forEach((scenario) => { + test(formDataDto, key, setter, scenario); + }); + }); + + describe.each(["businessInformation.firstName"])("%s", (key) => { + const formDataDto = newFormDataDto(); + const setter = (value: string) => { + formDataDto.businessInformation.firstName = value; + }; + ([ + ["", false], + ["John", true], + ["Name with more than 30 characters", false], + ["Unallowed!", false], + ["Unallowéd", false], + ]).forEach((scenario) => { + test(formDataDto, key, setter, scenario); + }); + }); + + describe.each(["businessInformation.middleName"])("%s", (key) => { + const formDataDto = newFormDataDto(); + const setter = (value: string) => { + formDataDto.businessInformation.middleName = value; + }; + ([ + ["", true, "not required"], + ["Elizabeth", true], + ["Name with more than 30 characters", false], + ["Unallowed!", false], + ["Unallowéd", false], + ]).forEach((scenario) => { + test(formDataDto, key, setter, scenario); + }); + }); + + describe.each(["businessInformation.lastName"])("%s", (key) => { + const formDataDto = newFormDataDto(); + const setter = (value: string) => { + formDataDto.businessInformation.lastName = value; + }; + ([ + ["", false], + ["Silver", true], + ["Name with more than 30 characters", false], + ["Unallowed!", false], + ["Unallowéd", false], + ]).forEach((scenario) => { + test(formDataDto, key, setter, scenario); + }); + }); + + describe.each(["identificationType.text"])("%s", (key) => { + const data: any = {}; + const setter = (value: any) => { + data.identificationType = value; + }; + ([ + [{ text: "" }, false], + [{ text: "" }, false], + [{ text: "ABCD" }, true], + ]).forEach((scenario) => { + test(data, key, setter, scenario); + }); + }); + + describe.each(["identificationProvince.text"])("%s", (key) => { + const data: any = {}; + const setter = (value: any) => { + data.identificationProvince = value; + }; + ([ + [{ text: "" }, false], + [{ text: "" }, false], + [{ text: "AB" }, true], + ]).forEach((scenario) => { + test(data, key, setter, scenario); + }); + }); + + describe.each(["businessInformation.identificationType"])("%s", (key) => { + const formDataDto = newFormDataDto(); + const setter = (value: string) => { + formDataDto.businessInformation.identificationType = value; + }; + ([ + ["", false], + [null, false], + ["A", true], + ]).forEach((scenario) => { + test(formDataDto, key, setter, scenario); + }); + }); + + describe.each(["businessInformation.clientIdentification"])("%s", (key) => { + const formDataDto = newFormDataDto(); + const setter = (value: string) => { + formDataDto.businessInformation.clientIdentification = value; + }; + ([ + ["", false], + [null, false], + ["1", true], + ]).forEach((scenario) => { + test(formDataDto, key, setter, scenario); + }); + }); + + describe("clientIdentification variations", () => { + const data: any = { + businessInformation: {}, + }; + + describe.each(["businessInformation.clientIdentification-BCDL"])("%s", (key) => { + const setter = (value: string) => { + data.businessInformation["clientIdentification-BCDL"] = value; + }; + ([ + ["1234ABCD", false, "contains letters"], + ["123456", false, "less than 7 digits"], + ["1234567", true], + ["12345678", true], + ["123456789", false, "more than 8 digits"], + ]).forEach((scenario) => { + test(data, key, setter, scenario); + }); + }); + + describe.each(["businessInformation.clientIdentification-nonBCDL"])("%s", (key) => { + const setter = (value: string) => { + data.businessInformation["clientIdentification-nonBCDL"] = value; + }; + ([ + ["1234ABC!", false, "contains special character"], + ["1234ABCa", false, "contains lower-case letter"], + ["1234 ABC", false, "contains space"], + ["123456", false, "less than 7 digits"], + ["1234ABC", true], + ["12345678901234567890", true], + ["12345678901234567890A", false, "more than 20 digits"], + ]).forEach((scenario) => { + test(data, key, setter, scenario); + }); + }); + + describe.each(["businessInformation.clientIdentification-BRTH"])("%s", (key) => { + const setter = (value: string) => { + data.businessInformation["clientIdentification-BRTH"] = value; + }; + ([ + ["12345678901", false, "less than 12 digits"], + ["12345678901A", false, "contains letters"], + ["123456789012", true], + ["1234567890123", true], + ["12345678901234", false, "more than 13 digits"], + ]).forEach((scenario) => { + test(data, key, setter, scenario); + }); + }); + + describe.each(["businessInformation.clientIdentification-PASS"])("%s", (key) => { + const setter = (value: string) => { + data.businessInformation["clientIdentification-PASS"] = value; + }; + ([ + ["1234ABC!", false, "contains special character"], + ["1234ABCa", false, "contains lower-case letter"], + ["1234 ABC", false, "contains space"], + ["1234567", false, "less than 8 digits"], + ["1234ABCD", true], + ["1234ABCD9", false, "more than 8 digits"], + ]).forEach((scenario) => { + test(data, key, setter, scenario); + }); + }); + + describe.each(["businessInformation.clientIdentification-CITZ"])("%s", (key) => { + const setter = (value: string) => { + data.businessInformation["clientIdentification-CITZ"] = value; + }; + ([ + ["1234ABC!", false, "contains special character"], + ["1234ABCa", false, "contains lower-case letter"], + ["1234 ABC", false, "contains space"], + ["1234567", false, "less than 8 digits"], + ["1234ABCD", true], + ["1234ABCD9", false, "more than 8 digits"], + ]).forEach((scenario) => { + test(data, key, setter, scenario); + }); + }); + + describe.each(["businessInformation.clientIdentification-FNID"])("%s", (key) => { + const setter = (value: string) => { + data.businessInformation["clientIdentification-FNID"] = value; + }; + ([ + ["123456789", false, "less than 10 digits"], + ["123456789A", false, "contains letters"], + ["1234567890", true], + ["12345678901", false, "more than 10 digits"], + ]).forEach((scenario) => { + test(data, key, setter, scenario); + }); + }); + + describe.each(["businessInformation.clientIdentification-OTHR"])("%s", (key) => { + const setter = (value: string) => { + data.businessInformation["clientIdentification-OTHR"] = value; + }; + ([ + ["12345", false, "No colon"], + ["Name:", false, "missing Id number value"], + [":1234", false, "missing Id type value"], + ["A:123", true], + ["A: 123", true], + ["A :123", true], + ["A : 123", true], + ["A: 123", false, "more than 1 space after colon"], + ["Name With Spaces: 123", true], + ["Name: 1234ABC!", false, "Id number part contains special character"], + ["Name: 1234ABCa", false, "Id number part contains lower-case letter"], + ["Name: 1234 ABC", false, "Id number part contains space"], + ["Name: 12", false, "Id number part is less than 3 digits"], + ["Name: 123", true], + ["Name: 7890123456789012345678901234567890", true], + [ + "Name: 78901234567890123456789012345678901", + false, + "the whole value has more than 40 digits", + ], + ]).forEach((scenario) => { + test(data, key, setter, scenario); + }); + }); + }); +});