CASH-2978 added policy number validation
CASH-2978 added policy number validation
This commit is contained in:
parent
a5d6308050
commit
b437ca183e
7 changed files with 560 additions and 22 deletions
|
|
@ -41,6 +41,7 @@ const errorMessages = {
|
|||
INSURANCE_COMPANY_REQUIRED: "Please select an insurance company",
|
||||
INSURANCE_COMPANY_NAME_REQUIRED: "Please enter your insurance company name",
|
||||
POLICY_NUMBER_REQUIRED: "Please enter your policy number",
|
||||
POLICY_NUMBER_FORMAT: "Please enter a valid policy number",
|
||||
CLAIM_NUMBER_VALID: "Please enter a valid claim number",
|
||||
CLAIM_NUMBER_FORMAT: "Claim number must be 6 to 8 digits",
|
||||
DATE_OF_LOSS_REQUIRED: "Please enter your date of damage",
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import { shallowMount } from "@vue/test-utils";
|
||||
import { defineRule } from "vee-validate";
|
||||
import textboxQuestion from "./textbox-question";
|
||||
|
||||
// Mock CMS content
|
||||
|
|
@ -12,6 +13,11 @@ const mockMixin = {
|
|||
};
|
||||
const maska = jest.fn();
|
||||
|
||||
// Real rules registered with vee-validate so the watcher's pre-check (which calls the
|
||||
// standalone `validate` export, not `this.validate`) exercises genuine valid/invalid paths.
|
||||
defineRule("spec-always-valid", () => true);
|
||||
defineRule("spec-always-invalid", () => "invalid");
|
||||
|
||||
describe("textboxQuestion.vue", () => {
|
||||
it("Should coerce non-string modelValue to empty string for v-model.trim", async () => {
|
||||
const wrapper = shallowMount(textboxQuestion, {
|
||||
|
|
@ -148,7 +154,7 @@ describe("textboxQuestion.vue", () => {
|
|||
expect(wrapper.emitted()).toHaveProperty("change");
|
||||
});
|
||||
|
||||
it("Should call this.handleChange with new value when the value is changed and the new value is valid", async () => {
|
||||
it("Should call this.handleChange with new value and shouldValidate=true when the value is changed and the new value is valid", async () => {
|
||||
// Arrange
|
||||
const wrapper = shallowMount(textboxQuestion, {
|
||||
global: {
|
||||
|
|
@ -157,22 +163,43 @@ describe("textboxQuestion.vue", () => {
|
|||
},
|
||||
},
|
||||
propsData: {
|
||||
options: {},
|
||||
modelValue: "foo",
|
||||
validationRules: "spec-always-valid",
|
||||
},
|
||||
mixins: [mockMixin],
|
||||
});
|
||||
|
||||
wrapper.vm.handleChange = jest.fn().mockImplementation(() => {});
|
||||
wrapper.vm.validate = jest.fn().mockImplementation(() => {
|
||||
return true;
|
||||
});
|
||||
|
||||
// Act
|
||||
wrapper.vm.$options.watch.value.call(wrapper.vm, "bar");
|
||||
await wrapper.vm.$options.watch.value.call(wrapper.vm, "bar");
|
||||
|
||||
// Assert
|
||||
expect(wrapper.vm.handleChange).toHaveBeenCalled;
|
||||
expect(wrapper.vm.handleChange).toHaveBeenCalledWith("bar", true);
|
||||
});
|
||||
|
||||
it("Should still call this.handleChange with shouldValidate=false when the new value is invalid, so the field stays in sync for later validation", async () => {
|
||||
// Arrange
|
||||
const wrapper = shallowMount(textboxQuestion, {
|
||||
global: {
|
||||
directives: {
|
||||
maska: maska,
|
||||
},
|
||||
},
|
||||
propsData: {
|
||||
modelValue: "foo",
|
||||
validationRules: "spec-always-invalid",
|
||||
},
|
||||
mixins: [mockMixin],
|
||||
});
|
||||
|
||||
wrapper.vm.handleChange = jest.fn().mockImplementation(() => {});
|
||||
|
||||
// Act
|
||||
await wrapper.vm.$options.watch.value.call(wrapper.vm, "bar");
|
||||
|
||||
// Assert
|
||||
expect(wrapper.vm.handleChange).toHaveBeenCalledWith("bar", false);
|
||||
});
|
||||
|
||||
it("Should not display camera or spinner icon when disabled", () => {
|
||||
|
|
|
|||
|
|
@ -252,9 +252,15 @@ export default {
|
|||
}
|
||||
|
||||
const result = await validate(newValue, this.validationRules); // do a test validation check, without triggering full validation
|
||||
if (result.valid) {
|
||||
this.handleChange(newValue); // trigger full validation on this field only
|
||||
}
|
||||
// Always sync vee-validate's internal field value with what the user actually
|
||||
// typed. Previously this only happened when the pre-check passed, so a field
|
||||
// that never reached a "valid" intermediate state while typing (e.g. an
|
||||
// 8-character alpha-only policy number) would never sync/validate at all,
|
||||
// silently swallowing its error message on blur/submit. Passing result.valid
|
||||
// as the shouldValidate flag keeps the no-flicker-while-typing behavior (errors
|
||||
// aren't displayed until the value is actually valid or a full validation runs),
|
||||
// while still keeping meta/value accurate for that later validation.
|
||||
this.handleChange(newValue, result.valid);
|
||||
},
|
||||
},
|
||||
components: {
|
||||
|
|
|
|||
|
|
@ -207,8 +207,8 @@ export default {
|
|||
arePagePrerequisitesValid() {
|
||||
const order = store.getters.order;
|
||||
if (order.payment.isClaimAndCoverage) {
|
||||
return true; //TODO: Future card when C&C flow is implemented
|
||||
}
|
||||
return true; //TODO: Future card when C&C flow is implemented
|
||||
}
|
||||
|
||||
const logQueue = [];
|
||||
|
||||
|
|
|
|||
|
|
@ -172,14 +172,8 @@ defineRule("city-required", required(errorMessages.CITY_REQUIRED));
|
|||
defineRule("state-required", required(errorMessages.STATE_REQUIRED));
|
||||
defineRule("zip-code-required", required(errorMessages.ZIP_CODE_REQUIRED));
|
||||
defineRule("zip-code-format", regex(/(^\d{5}$)|(^\d{5}-\d{4}$)/, errorMessages.ZIP_CODE_FORMAT));
|
||||
defineRule(
|
||||
"claim-number-numeric",
|
||||
regex(/^\d+$/, errorMessages.CLAIM_NUMBER_VALID)
|
||||
);
|
||||
defineRule(
|
||||
"claim-number-format",
|
||||
regex(/^\d{6,8}$/, errorMessages.CLAIM_NUMBER_FORMAT)
|
||||
);
|
||||
defineRule("claim-number-numeric", regex(/^\d+$/, errorMessages.CLAIM_NUMBER_VALID));
|
||||
defineRule("claim-number-format", regex(/^\d{6,8}$/, errorMessages.CLAIM_NUMBER_FORMAT));
|
||||
|
||||
export default {
|
||||
name: "insurance-details",
|
||||
|
|
|
|||
|
|
@ -1,8 +1,11 @@
|
|||
import policyInfo from "@/layouts/policy-info/policy-info.vue";
|
||||
import { shallowMount } from "@vue/test-utils";
|
||||
import { shallowMount, mount, flushPromises } from "@vue/test-utils";
|
||||
import { getMountOptions } from "@/helpers/unit-test-helper.js";
|
||||
import { fetchCmsContentForPage } from "@/helpers/cms-content-helper";
|
||||
import { settleAllPromises } from "@/helpers/layout-helper";
|
||||
import { validate, Form } from "vee-validate";
|
||||
import { errorMessages } from "@/constants/error-messages";
|
||||
import textboxQuestion from "@/digital-components/textbox-question/textbox-question";
|
||||
|
||||
// Mock our module for promises.
|
||||
jest.mock("@/helpers/layout-helper.js", () => ({
|
||||
|
|
@ -43,6 +46,391 @@ describe("policy-info.vue", () => {
|
|||
expect(wrapper.vm.arePagePrerequisitesValid()).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe("forwardButtonAction", () => {
|
||||
// Regression coverage: clicking Continue must explicitly (re)validate the form
|
||||
// rather than assuming meta.valid/isForwardActionDisabled already caught everything,
|
||||
// since that was the actual gap behind "I typed an invalid policy number, clicked
|
||||
// Continue, and the message never appeared".
|
||||
test("does not save or navigate when the form is invalid", async () => {
|
||||
const { wrapper } = setupMocks();
|
||||
wrapper.vm.isFormValid = jest.fn().mockResolvedValue(false);
|
||||
|
||||
await wrapper.vm.forwardButtonAction();
|
||||
|
||||
expect(wrapper.vm.isFormValid).toHaveBeenCalledWith(wrapper.vm.$refs.theForm);
|
||||
expect(wrapper.vm.dispatchStoreAction).not.toHaveBeenCalled();
|
||||
expect(wrapper.vm.$router.navigateWithSaving).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test("saves and navigates when the form is valid", async () => {
|
||||
const { wrapper } = setupMocks();
|
||||
wrapper.vm.isFormValid = jest.fn().mockResolvedValue(true);
|
||||
|
||||
await wrapper.vm.forwardButtonAction();
|
||||
|
||||
expect(wrapper.vm.dispatchStoreAction).toHaveBeenCalled();
|
||||
expect(wrapper.vm.$router.navigateWithSaving).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe("policy-number-format validation rule", () => {
|
||||
test("passes when no PolicyNumberValidationWidget config is returned", async () => {
|
||||
const { wrapper } = await setupMocksWithPolicyNumberValidationWidget(undefined);
|
||||
|
||||
const result = await validate("AB1", "policy-number-format");
|
||||
|
||||
expect(result.valid).toBe(true);
|
||||
expect(wrapper.vm).toBeTruthy();
|
||||
});
|
||||
|
||||
test("passes when field is empty (required rule handles that case)", async () => {
|
||||
await setupMocksWithPolicyNumberValidationWidget({
|
||||
minimumLength: 8,
|
||||
maximumLength: 9,
|
||||
validationRuleType: "Alpha",
|
||||
});
|
||||
|
||||
const result = await validate("", "policy-number-format");
|
||||
|
||||
expect(result.valid).toBe(true);
|
||||
});
|
||||
|
||||
test("fails when value is shorter than minimumLength", async () => {
|
||||
await setupMocksWithPolicyNumberValidationWidget({
|
||||
minimumLength: 8,
|
||||
maximumLength: 9,
|
||||
validationRuleType: "Alpha",
|
||||
});
|
||||
|
||||
const result = await validate("ABCDEF", "policy-number-format");
|
||||
|
||||
expect(result.valid).toBe(false);
|
||||
});
|
||||
|
||||
test("fails when value is longer than maximumLength", async () => {
|
||||
await setupMocksWithPolicyNumberValidationWidget({
|
||||
minimumLength: 8,
|
||||
maximumLength: 9,
|
||||
validationRuleType: "Alpha",
|
||||
});
|
||||
|
||||
const result = await validate("ABCDEFGHIJ", "policy-number-format");
|
||||
|
||||
expect(result.valid).toBe(false);
|
||||
});
|
||||
|
||||
test("passes for a valid Alpha value within length bounds", async () => {
|
||||
await setupMocksWithPolicyNumberValidationWidget({
|
||||
minimumLength: 8,
|
||||
maximumLength: 9,
|
||||
validationRuleType: "Alpha",
|
||||
});
|
||||
|
||||
const result = await validate("ABCDEFGH", "policy-number-format");
|
||||
|
||||
expect(result.valid).toBe(true);
|
||||
});
|
||||
|
||||
test("fails for an Alpha rule when value contains digits", async () => {
|
||||
await setupMocksWithPolicyNumberValidationWidget({
|
||||
minimumLength: 8,
|
||||
maximumLength: 9,
|
||||
validationRuleType: "Alpha",
|
||||
});
|
||||
|
||||
const result = await validate("ABCDEFG1", "policy-number-format");
|
||||
|
||||
expect(result.valid).toBe(false);
|
||||
});
|
||||
|
||||
test("passes for a valid numeric value", async () => {
|
||||
await setupMocksWithPolicyNumberValidationWidget({
|
||||
minimumLength: 6,
|
||||
maximumLength: 10,
|
||||
validationRuleType: "numeric",
|
||||
});
|
||||
|
||||
const result = await validate("123456", "policy-number-format");
|
||||
|
||||
expect(result.valid).toBe(true);
|
||||
});
|
||||
|
||||
test("fails for a numeric rule when value contains letters", async () => {
|
||||
await setupMocksWithPolicyNumberValidationWidget({
|
||||
minimumLength: 6,
|
||||
maximumLength: 10,
|
||||
validationRuleType: "numeric",
|
||||
});
|
||||
|
||||
const result = await validate("12345A", "policy-number-format");
|
||||
|
||||
expect(result.valid).toBe(false);
|
||||
});
|
||||
|
||||
test("passes for a valid AlphaNumeric value", async () => {
|
||||
await setupMocksWithPolicyNumberValidationWidget({
|
||||
minimumLength: 9,
|
||||
maximumLength: 12,
|
||||
validationRuleType: "AlphaNumeric",
|
||||
});
|
||||
|
||||
const result = await validate("123456789a", "policy-number-format");
|
||||
|
||||
expect(result.valid).toBe(true);
|
||||
});
|
||||
|
||||
test("fails for an AlphaNumeric rule when value contains a space", async () => {
|
||||
await setupMocksWithPolicyNumberValidationWidget({
|
||||
minimumLength: 9,
|
||||
maximumLength: 12,
|
||||
validationRuleType: "AlphaNumeric",
|
||||
});
|
||||
|
||||
const result = await validate("12345678 a", "policy-number-format");
|
||||
|
||||
expect(result.valid).toBe(false);
|
||||
});
|
||||
|
||||
test("fails for an AlphaNumeric rule when value contains a dash", async () => {
|
||||
await setupMocksWithPolicyNumberValidationWidget({
|
||||
minimumLength: 9,
|
||||
maximumLength: 12,
|
||||
validationRuleType: "AlphaNumeric",
|
||||
});
|
||||
|
||||
const result = await validate("12345678-a", "policy-number-format");
|
||||
|
||||
expect(result.valid).toBe(false);
|
||||
});
|
||||
|
||||
test("passes for a valid AlphaNumericWithSpaces value", async () => {
|
||||
await setupMocksWithPolicyNumberValidationWidget({
|
||||
minimumLength: 6,
|
||||
maximumLength: 12,
|
||||
validationRuleType: "AlphaNumericWithSpaces",
|
||||
});
|
||||
|
||||
const result = await validate("ABC 123", "policy-number-format");
|
||||
|
||||
expect(result.valid).toBe(true);
|
||||
});
|
||||
|
||||
test("fails for an AlphaNumericWithSpaces rule when value contains other symbols", async () => {
|
||||
await setupMocksWithPolicyNumberValidationWidget({
|
||||
minimumLength: 6,
|
||||
maximumLength: 12,
|
||||
validationRuleType: "AlphaNumericWithSpaces",
|
||||
});
|
||||
|
||||
const result = await validate("ABC-123", "policy-number-format");
|
||||
|
||||
expect(result.valid).toBe(false);
|
||||
});
|
||||
|
||||
test("uses the CMS-provided minValidationMessage when value is shorter than minimumLength", async () => {
|
||||
const minValidationMessage = "Policy number should be at least 8 characters.";
|
||||
await setupMocksWithPolicyNumberValidationWidget({
|
||||
minimumLength: 8,
|
||||
maximumLength: 9,
|
||||
validationRuleType: "Alpha",
|
||||
minValidationMessage,
|
||||
maxValidationMessage: "Policy number must be 9 characters or less.",
|
||||
});
|
||||
|
||||
const result = await validate("ABCDEF", "policy-number-format");
|
||||
|
||||
expect(result.valid).toBe(false);
|
||||
expect(result.errors).toEqual([minValidationMessage]);
|
||||
});
|
||||
|
||||
test("uses the CMS-provided maxValidationMessage when value is longer than maximumLength", async () => {
|
||||
const maxValidationMessage = "Policy number must be 9 characters or less.";
|
||||
await setupMocksWithPolicyNumberValidationWidget({
|
||||
minimumLength: 8,
|
||||
maximumLength: 9,
|
||||
validationRuleType: "Alpha",
|
||||
minValidationMessage: "Policy number should be at least 8 characters.",
|
||||
maxValidationMessage,
|
||||
});
|
||||
|
||||
const result = await validate("ABCDEFGHIJ", "policy-number-format");
|
||||
|
||||
expect(result.valid).toBe(false);
|
||||
expect(result.errors).toEqual([maxValidationMessage]);
|
||||
});
|
||||
|
||||
test("falls back to the default error message when minValidationMessage is not provided", async () => {
|
||||
await setupMocksWithPolicyNumberValidationWidget({
|
||||
minimumLength: 8,
|
||||
maximumLength: 9,
|
||||
validationRuleType: "Alpha",
|
||||
});
|
||||
|
||||
const result = await validate("ABCDEF", "policy-number-format");
|
||||
|
||||
expect(result.valid).toBe(false);
|
||||
expect(result.errors).toEqual([errorMessages.POLICY_NUMBER_FORMAT]);
|
||||
});
|
||||
|
||||
test("falls back to the default error message when maxValidationMessage is not provided", async () => {
|
||||
await setupMocksWithPolicyNumberValidationWidget({
|
||||
minimumLength: 8,
|
||||
maximumLength: 9,
|
||||
validationRuleType: "Alpha",
|
||||
});
|
||||
|
||||
const result = await validate("ABCDEFGHIJ", "policy-number-format");
|
||||
|
||||
expect(result.valid).toBe(false);
|
||||
expect(result.errors).toEqual([errorMessages.POLICY_NUMBER_FORMAT]);
|
||||
});
|
||||
|
||||
test("uses the CMS-provided invalidCharMessage for a pattern-only failure (value within length bounds)", async () => {
|
||||
const invalidCharMessage = "Policy number may only contain letters.";
|
||||
await setupMocksWithPolicyNumberValidationWidget({
|
||||
minimumLength: 8,
|
||||
maximumLength: 9,
|
||||
validationRuleType: "Alpha",
|
||||
minValidationMessage: "Policy number should be at least 8 characters.",
|
||||
maxValidationMessage: "Policy number must be 9 characters or less.",
|
||||
invalidCharMessage,
|
||||
});
|
||||
|
||||
const result = await validate("ABCDEFG1", "policy-number-format");
|
||||
|
||||
expect(result.valid).toBe(false);
|
||||
expect(result.errors).toEqual([invalidCharMessage]);
|
||||
});
|
||||
|
||||
test("falls back to the default error message for a pattern-only failure when invalidCharMessage is not provided", async () => {
|
||||
await setupMocksWithPolicyNumberValidationWidget({
|
||||
minimumLength: 8,
|
||||
maximumLength: 9,
|
||||
validationRuleType: "Alpha",
|
||||
minValidationMessage: "Policy number should be at least 8 characters.",
|
||||
maxValidationMessage: "Policy number must be 9 characters or less.",
|
||||
});
|
||||
|
||||
const result = await validate("ABCDEFG1", "policy-number-format");
|
||||
|
||||
expect(result.valid).toBe(false);
|
||||
expect(result.errors).toEqual([errorMessages.POLICY_NUMBER_FORMAT]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("policyNumber field end-to-end (via textbox-question)", () => {
|
||||
// Regression test for a reported bug: typing a value that never passes through a
|
||||
// "valid" intermediate state (e.g. "a@2" against an 8-9 char Alpha-only rule) left
|
||||
// vee-validate's field `meta.valid` stuck at `true`, since textbox-question's value
|
||||
// watcher only synced/validated the field when a pre-check said the value was valid.
|
||||
// In the app this drives the Forward button's disabled state
|
||||
// (:isForwardActionDisabled="!meta.valid"), so the bug meant a user could type an
|
||||
// invalid policy number and the form would not flag it as invalid at all - it only
|
||||
// "self-corrected" via the input's native `change` event on blur, which doesn't fire
|
||||
// from keystrokes alone. This test dispatches only "input" events (like real typing)
|
||||
// and intentionally does NOT fire a native "change"/blur event, to isolate the
|
||||
// watcher's own behavior from that unrelated safety net.
|
||||
test("marks the field invalid as soon as an unrecoverable value is typed, without waiting for blur", async () => {
|
||||
await setupMocksWithPolicyNumberValidationWidget({
|
||||
minimumLength: 8,
|
||||
maximumLength: 9,
|
||||
validationRuleType: "Alpha",
|
||||
minValidationMessage: "Policy number should be at least 8 characters.",
|
||||
maxValidationMessage: "Policy number must be 9 characters or less.",
|
||||
});
|
||||
|
||||
const wrapper = mount(textboxQuestion, {
|
||||
global: {
|
||||
directives: { maska: jest.fn() },
|
||||
mixins: [
|
||||
{
|
||||
methods: {
|
||||
getCmsContent: jest.fn(() => ""),
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
props: {
|
||||
// Start from a value that already satisfies the rule, so meta.valid
|
||||
// begins as true (isolating the watcher's behavior from the
|
||||
// required-on-empty check, which is independently already true on
|
||||
// mount regardless of this bug).
|
||||
modelValue: "ABCDEFGH",
|
||||
inputId: "policyNumber",
|
||||
validationRules: "policy-number-required|policy-number-format",
|
||||
"onUpdate:modelValue": (newValue) => wrapper.setProps({ modelValue: newValue }),
|
||||
},
|
||||
});
|
||||
await flushPromises();
|
||||
expect(wrapper.vm.meta.valid).toBe(true);
|
||||
|
||||
const input = wrapper.find("input");
|
||||
input.element.value = "a@2";
|
||||
await input.trigger("input"); // simulate typing only, no blur/change
|
||||
await flushPromises();
|
||||
|
||||
expect(wrapper.vm.meta.valid).toBe(false);
|
||||
});
|
||||
|
||||
// Reproduces "I typed a@2 and clicked Continue, and the message never appeared".
|
||||
// Clicking Continue submits the real <Form>, which vee-validate intercepts and
|
||||
// fully (re)validates every field using that field's own internally tracked value -
|
||||
// not necessarily whatever is visible in the DOM input. If that internal value is
|
||||
// stale (the underlying bug), the message can still fail to appear even though
|
||||
// meta.valid correctly flips to false while typing.
|
||||
test("shows the CMS validation message in the DOM after typing an invalid value and submitting the form", async () => {
|
||||
const minValidationMessage = "Policy number should be at least 8 characters.";
|
||||
await setupMocksWithPolicyNumberValidationWidget({
|
||||
minimumLength: 8,
|
||||
maximumLength: 9,
|
||||
validationRuleType: "Alpha",
|
||||
minValidationMessage,
|
||||
maxValidationMessage: "Policy number must be 9 characters or less.",
|
||||
});
|
||||
|
||||
const harness = {
|
||||
components: { Form, textboxQuestion },
|
||||
data() {
|
||||
return { policyNumber: "ABCDEFGH" };
|
||||
},
|
||||
template: `
|
||||
<Form v-slot="{ meta }" @submit="() => {}">
|
||||
<textboxQuestion
|
||||
v-model="policyNumber"
|
||||
inputId="policyNumber"
|
||||
validationRules="policy-number-required|policy-number-format" />
|
||||
<button type="submit" :disabled="!meta.valid">Continue</button>
|
||||
</Form>
|
||||
`,
|
||||
};
|
||||
|
||||
const wrapper = mount(harness, {
|
||||
global: {
|
||||
directives: { maska: jest.fn() },
|
||||
mixins: [
|
||||
{
|
||||
methods: {
|
||||
getCmsContent: jest.fn(() => ""),
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
await flushPromises();
|
||||
|
||||
const input = wrapper.find("input");
|
||||
input.element.value = "a@2";
|
||||
await input.trigger("input"); // simulate typing only, no blur/change
|
||||
await flushPromises();
|
||||
|
||||
// Mirrors clicking the real "Continue" button, which submits the <form>.
|
||||
await wrapper.find("form").trigger("submit");
|
||||
await flushPromises();
|
||||
|
||||
expect(wrapper.text()).toContain(minValidationMessage);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
function setupMocks() {
|
||||
|
|
@ -78,3 +466,30 @@ function setupMocks() {
|
|||
|
||||
return { wrapper };
|
||||
}
|
||||
|
||||
// Runs beforeRouteEnter with a clientConfig that includes the given
|
||||
// PolicyNumberValidationWidget config so the module-scoped rule config gets populated,
|
||||
// mirroring how the real CMS response would flow through the route guard. Like
|
||||
// ConfigWidget, the real CMS wraps the widget's config in a JSON-encoded `Text`
|
||||
// string rather than returning a plain object, so we replicate that shape here too.
|
||||
async function setupMocksWithPolicyNumberValidationWidget(policyNumberValidationConfig) {
|
||||
const { wrapper } = setupMocks();
|
||||
|
||||
settleAllPromises.mockResolvedValue({
|
||||
cmsContent: {},
|
||||
clientConfig: {
|
||||
PolicyNumberValidationWidget: policyNumberValidationConfig
|
||||
? { Text: JSON.stringify(policyNumberValidationConfig) }
|
||||
: undefined,
|
||||
},
|
||||
});
|
||||
|
||||
await policyInfo.beforeRouteEnter.call(
|
||||
wrapper.vm,
|
||||
{ name: "policy-info" },
|
||||
undefined,
|
||||
(callback) => callback(wrapper.vm)
|
||||
);
|
||||
|
||||
return { wrapper };
|
||||
}
|
||||
|
|
|
|||
|
|
@ -21,7 +21,7 @@
|
|||
:overrideQuestionText="policyNumberLabelOverride"
|
||||
v-model="policyNumber"
|
||||
inputId="policyNumber"
|
||||
validationRules="policy-number-required" />
|
||||
validationRules="policy-number-required|policy-number-format" />
|
||||
<span
|
||||
v-if="displayPolicyHelperText"
|
||||
class="d-block mb-5 helper-text"
|
||||
|
|
@ -222,7 +222,88 @@ import { settleAllPromises } from "@/helpers/layout-helper";
|
|||
import store from "@/store";
|
||||
import { debugLog } from "@/helpers/debug-log-helper";
|
||||
|
||||
// The PolicyNumberValidationWidget is fetched asynchronously from the CMS (see
|
||||
// beforeRouteEnter below), so the format rule reads it from this module-scoped
|
||||
// reference rather than capturing a value at defineRule time.
|
||||
let policyNumberValidationConfig = null;
|
||||
|
||||
function getPolicyNumberFormatPattern(validationRuleType) {
|
||||
switch ((validationRuleType || "").toLowerCase()) {
|
||||
case "alpha":
|
||||
return /^[A-Za-z]+$/;
|
||||
case "numeric":
|
||||
return /^[0-9]+$/;
|
||||
case "alphanumeric":
|
||||
return /^[A-Za-z0-9]+$/;
|
||||
case "alphanumericwithspaces":
|
||||
return /^[A-Za-z0-9 ]+$/;
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// Like ConfigWidget, PolicyNumberValidationWidget's actual config comes back from the CMS
|
||||
// as a JSON-encoded string in its `Text` property, not as a plain object.
|
||||
// examples:
|
||||
/* {
|
||||
"minimumLength": 1,
|
||||
"maximumLength": 16,
|
||||
"validationRuleType": "AlphaNumeric",
|
||||
"minValidationMessage": "",
|
||||
"maxValidationMessage": "That looks like a VIN. Your policy number should be 16 characters maximum.",
|
||||
"invalidCharMessage": "Policy number must be numbers and letters only."
|
||||
}
|
||||
{
|
||||
"minimumLength": 8,
|
||||
"maximumLength": 9,
|
||||
"validationRuleType": "AlphaNumeric",
|
||||
"minValidationMessage": "Policy number should be at least 8 characters.",
|
||||
"maxValidationMessage": "Policy number must be 9 characters or less."
|
||||
}
|
||||
*/
|
||||
function parsePolicyNumberValidationWidget(widget) {
|
||||
const raw = widget?.Text;
|
||||
if (!raw) {
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
return JSON.parse(raw);
|
||||
} catch (err) {
|
||||
console.warn("Failed to parse PolicyNumberValidationWidget.Text", err);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
defineRule("policy-number-required", required(errorMessages.POLICY_NUMBER_REQUIRED));
|
||||
defineRule("policy-number-format", (value) => {
|
||||
if (!value || !value.length || !policyNumberValidationConfig) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const {
|
||||
minimumLength,
|
||||
maximumLength,
|
||||
validationRuleType,
|
||||
minValidationMessage,
|
||||
maxValidationMessage,
|
||||
invalidCharMessage,
|
||||
} = policyNumberValidationConfig;
|
||||
|
||||
if (typeof minimumLength === "number" && value.length < minimumLength) {
|
||||
return minValidationMessage || errorMessages.POLICY_NUMBER_FORMAT;
|
||||
}
|
||||
|
||||
if (typeof maximumLength === "number" && value.length > maximumLength) {
|
||||
return maxValidationMessage || errorMessages.POLICY_NUMBER_FORMAT;
|
||||
}
|
||||
|
||||
const pattern = getPolicyNumberFormatPattern(validationRuleType);
|
||||
if (pattern && !pattern.test(value)) {
|
||||
return invalidCharMessage || errorMessages.POLICY_NUMBER_FORMAT;
|
||||
}
|
||||
|
||||
return true;
|
||||
});
|
||||
defineRule("zip-code-required", required(errorMessages.ZIP_REQUIRED));
|
||||
defineRule("zip-code-format", regex(/(^\d{5}$)|(^\d{5}-\d{4}$)/, errorMessages.ZIP_FORMAT));
|
||||
defineRule("phone-number-required", required(errorMessages.PHONE_REQUIRED));
|
||||
|
|
@ -412,6 +493,9 @@ export default {
|
|||
next((vm) => {
|
||||
vm.setCmsContent(resultMap.cmsContent);
|
||||
vm.clientConfig = resultMap.clientConfig ?? {};
|
||||
policyNumberValidationConfig = parsePolicyNumberValidationWidget(
|
||||
vm.clientConfig?.PolicyNumberValidationWidget
|
||||
);
|
||||
// The load-session API can return damageCause in a different case than
|
||||
// the CMS answer values. Reconcile it so the dropdown pre-selects.
|
||||
vm.damageCause = vm.matchDamageCauseToOption(vm.damageCause);
|
||||
|
|
@ -488,6 +572,17 @@ export default {
|
|||
);
|
||||
},
|
||||
async forwardButtonAction() {
|
||||
// Explicitly (re)validate every field before navigating, rather than relying
|
||||
// solely on isForwardActionDisabled/meta.valid already being accurate and on the
|
||||
// Continue button's incidental native form-submission side effect to surface
|
||||
// error messages. This guarantees the CMS-driven policy number format error (and
|
||||
// any other field's error) is actually rendered when Continue is clicked while
|
||||
// the form is invalid.
|
||||
const isValid = await this.isFormValid(this.$refs.theForm);
|
||||
if (!isValid) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.dispatchStoreAction(
|
||||
this.storeActions.SAVE_INSURANCE_DETAILS,
|
||||
{
|
||||
|
|
|
|||
Loading…
Reference in a new issue