CSR-319: move validation rules and errors into centralized files; fix and set up validation unit tests

This commit is contained in:
Adam Caouette 2022-03-04 13:03:08 -05:00
parent ca0b266efa
commit 312ba851bf
12 changed files with 167 additions and 96 deletions

View file

@ -0,0 +1,12 @@
const errorMessages = {
DAMAGE_LOCATION_REQUIRED: "Please select damage location",
DAMAGE_SIDE_REQUIRED: "Please select vehicle side",
DRIVER_SIDE_OPTIONS_REQUIRED: "Please select window",
PASSENGER_SIDE_OPTIONS_REQUIRED: "Please select window",
WINDSHIELD_DAMAGE_TYPE_REQUIRED: "Please select windshield damage",
WINDSHIELD_CHIP_COUNT_REQUIRED: "Please select chip(s)",
WINSHIELD_REPLACE_OPTIONS_REQUIRED: "Please select windshield part",
REPLACE_OPTIONS_REQUIRED: "Please select rear window type",
};
export default { errorMessages };

View file

@ -0,0 +1,8 @@
export function required(errorMessage) {
return (value) => {
if (!value || value.length < 1) {
return errorMessage;
}
return true;
};
}

View file

@ -0,0 +1,29 @@
import { required } from "@/helpers/validation-rules";
describe("validation-rules.vue", () => {
test("required rules should return error if value missing", () => {
//Arrange
const testFn = required("an error");
//Act
const testResponse = testFn();
//Assert
expect(testResponse).toBe("an error");
});
});
describe("validation-rules.vue", () => {
test("required rules should return true if value present", () => {
//Arrange
const testFn = required("an error");
//Act
const testResponse = testFn('some value');
//Assert
expect(testResponse).toBe(true);
});
});

View file

@ -16,14 +16,11 @@
import buttonQuestion from "@/common-components/button-question/button-question"; import buttonQuestion from "@/common-components/button-question/button-question";
import store from "@/store"; import store from "@/store";
import { defineRule } from "vee-validate"; import { defineRule } from "vee-validate";
import { required } from "@/helpers/validation-rules";
import errorMessages from "@/constants/error-messages";
// DEFINE VALIDATION RULES // DEFINE VALIDATION RULES
defineRule("damage-location-required", (value) => { defineRule("damage-location-required", required(errorMessages.DAMAGE_LOCATION_REQUIRED));
if (!value || value.length < 1) {
return "Please select damage location";
}
return true;
});
export default ({ export default ({
name: "damageLocationQuestion", name: "damageLocationQuestion",

View file

@ -1,7 +1,6 @@
<template> <template>
<transition name="fade" mode="out-in"> <transition name="fade" mode="out-in">
<div v-if="isAvailable && this.answersToDisplay.length > 0" class="replace-options-question" :class="this.answersToDisplay.length < 2 ? 'd-noneXXXXconsole.logXXXXXXXXXXXXX' : ''" aria-live="polite"> <div v-if="isAvailable && this.answersToDisplay.length > 0" class="replace-options-question" :class="this.answersToDisplay.length < 2 ? 'd-noneXXXXconsole.logXXXXXXXXXXXXX' : ''" aria-live="polite">
answersToDisplay.length: {{answersToDisplay.length}}
<buttonQuestion <buttonQuestion
isWide isWide
:questionText="questionText" :questionText="questionText"

View file

@ -39,28 +39,13 @@ import buttonQuestion from "@/common-components/button-question/button-question"
import replaceOptionsQuestion from "@/layouts/vehicle-damage/replace-options-question/replace-options-question"; import replaceOptionsQuestion from "@/layouts/vehicle-damage/replace-options-question/replace-options-question";
import store from "@/store"; import store from "@/store";
import { defineRule } from "vee-validate"; import { defineRule } from "vee-validate";
import { required } from "@/helpers/validation-rules";
import errorMessages from "@/constants/error-messages";
// DEFINE VALIDATION RULES // DEFINE VALIDATION RULES
defineRule("damage-side-required", (value) => { defineRule("damage-side-required", required(errorMessages.DAMAGE_SIDE_REQUIRED));
if (!value || value.length < 1) { defineRule("driver-side-options-required", required(errorMessages.DRIVER_SIDE_OPTIONS_REQUIRED));
return "Please select vehicle side"; defineRule("passenger-side-options-required", required(errorMessages.PASSENGER_SIDE_OPTIONS_REQUIRED));
}
return true;
});
defineRule("driver-side-options-required", (value) => {
if (!value || value.length < 1) {
return "Please select window";
}
return true;
});
defineRule("passenger-side-options-required", (value) => {
if (!value || value.length < 1) {
return "Please select window";
}
return true;
});
export default ({ export default ({
name: "sideDoorOptions", name: "sideDoorOptions",

View file

@ -13,11 +13,12 @@ import replaceOptionsQuestion from "@/layouts/vehicle-damage/replace-options-que
import { settleAllPromises } from "@/helpers/layout-helper.js"; import { settleAllPromises } from "@/helpers/layout-helper.js";
import baseMixin from "@/mixins/base-mixin"; import baseMixin from "@/mixins/base-mixin";
import { fetchCmsContentForPage } from "@/helpers/cms-content-helper"; import { fetchCmsContentForPage } from "@/helpers/cms-content-helper";
import { shallowMount } from "@vue/test-utils"; import { mount, flushPromises } from "@vue/test-utils";
import { getMountOptions } from "@/helpers/unit-test-helper.js"; import { getMountOptions } from "@/helpers/unit-test-helper.js";
import { nextTick } from "vue"; import { nextTick } from "vue";
import { storeActions } from "@/constants/store-actions"; import { storeActions } from "@/constants/store-actions";
import store from "@/store"; import store from "@/store";
import { validate } from "vee-validate";
// Mock our module for promises. // Mock our module for promises.
jest.mock("@/helpers/layout-helper.js", () => ({ jest.mock("@/helpers/layout-helper.js", () => ({
@ -36,7 +37,9 @@ jest.mock("@/store", () => ({
getters: { getters: {
vehicle: { vehicle: {
carId: "C00000000", carId: "C00000000",
image: "test.jpg",
}, },
eventBusItem: jest.fn(),
}, },
})); }));
@ -211,12 +214,84 @@ describe("vehicle-damage.vue", () => {
}); });
}); });
describe("vehicle-damage.vue", () => {
test("when onInvalidSubmit is triggered with errors focus will be put on the first element with an error", async () => {
//Arrange
const { wrapper } = setupMocks({});
const mockedValidationPayload = {
values: {},
errors: {
driverSideOptions: 'Please select window',
passengerSideOptions: 'Please select window'
},
results: {},
}
const newObj = document.createElement('input');
newObj.setAttribute("id", "testInput");
newObj.setAttribute("data-focus-target", "driverSideOptions");
document.body.appendChild(newObj);
const testInputElement = document.getElementById("testInput");
//Act
vehicleDamage.beforeRouteEnter.call(
wrapper.vm,
{ query: { fmgPage: "vehicle-damage" } },
undefined,
(c) => c(wrapper.vm)
);
wrapper.vm.onInvalidSubmit(mockedValidationPayload);
await nextTick();
const focusedEl = document.activeElement;
//Assert
expect(testInputElement).toBe(focusedEl);
});
});
// THE FOLLOWING TEST IS NOT NECESSARILY REQUIRED FOR COVERAGE
// BUT KEEP FOR AN EXAMPLE OF A VALIDATION TEST
//
describe("vehicle-damage.vue", () => {
test("when validation rules are set they should validate correctly", async () => {
//Arrange
const { wrapper } = setupMocks({});
//Act
vehicleDamage.beforeRouteEnter.call(
wrapper.vm,
{ query: { fmgPage: "vehicle-damage" } },
undefined,
(c) => c(wrapper.vm)
);
const testNull = validate( "", "replace-options-required");
const testString = validate( "sldfj", "replace-options-required");
await flushPromises();
//Assert
testNull.then(function(data) {
expect(data.valid).toEqual(false);
});
testString.then(function(data) {
expect(data.valid).toEqual(true);
});
});
});
function setupMocks({ function setupMocks({
pageHeaderWidgetHeaderText = {}, pageHeaderWidgetHeaderText = {},
mountOptionsMockData = { mountOptionsMockData = {
router: { router: {
navigate: jest.fn(), navigate: jest.fn(),
}, },
store: {
getters: {
vehicle: {},
},
},
}, },
}) { }) {
//Mock api responses //Mock api responses
@ -281,6 +356,7 @@ function setupMocks({
replaceOptionsQuestion.methods = { replaceOptionsQuestion.methods = {
initializeComponent: jest.fn(), initializeComponent: jest.fn(),
updateSelectedValues: jest.fn(),
}; };
funnelFooter.methods = { funnelFooter.methods = {
@ -288,7 +364,9 @@ function setupMocks({
} }
const mountOptions = getMountOptions(mountOptionsMockData); const mountOptions = getMountOptions(mountOptionsMockData);
const wrapper = shallowMount(vehicleDamage, mountOptions); mountOptions['attachTo'] = document.body; // append wrapper to document.body to test DOM methods
const wrapper = mount(vehicleDamage, mountOptions);
const funnelHeaderWrapper = wrapper.findComponent({ name: "funnelHeader" }); const funnelHeaderWrapper = wrapper.findComponent({ name: "funnelHeader" });
funnelHeaderWrapper.vm.initializeComponent = funnelHeaderWrapper.vm.initializeComponent =
@ -300,38 +378,27 @@ function setupMocks({
const sideDoorOptionsWrapper = wrapper.findComponent({ name: "sideDoorOptions" }); const sideDoorOptionsWrapper = wrapper.findComponent({ name: "sideDoorOptions" });
sideDoorOptionsWrapper.vm.initializeComponent = sideDoorOptionsWrapper.vm.initializeComponent =
sideDoorOptions.methods.initializeComponent; sideDoorOptions.methods.initializeComponent;
const windshieldOptionsWrapper = wrapper.findComponent({ const windshieldOptionsWrapper = wrapper.findComponent({ name: "windshieldOptions" });
name: "windshieldOptions",
});
windshieldOptionsWrapper.vm.initializeComponent = windshieldOptionsWrapper.vm.initializeComponent =
windshieldOptions.methods.initializeComponent; windshieldOptions.methods.initializeComponent;
const funnelSubHeaderWrapper = wrapper.findComponent({ const funnelSubHeaderWrapper = wrapper.findComponent({ name: "funnelSubHeader" });
name: "funnelSubHeader",
});
funnelSubHeaderWrapper.vm.initializeComponent = funnelSubHeaderWrapper.vm.initializeComponent =
funnelSubHeader.methods.initializeComponent; funnelSubHeader.methods.initializeComponent;
const backGlassOptionsWrapper = wrapper.findComponent({ const backGlassOptionsWrapper = wrapper.findComponent({ name: "replaceOptionsQuestion" });
name: "replaceOptionsQuestion",
});
backGlassOptionsWrapper.vm.initializeComponent = backGlassOptionsWrapper.vm.initializeComponent =
replaceOptionsQuestion.methods.initializeComponent; replaceOptionsQuestion.methods.initializeComponent;
const damageLocationQuestionWrapper = wrapper.findComponent({ const damageLocationQuestionWrapper = wrapper.findComponent({ name: "damageLocationQuestion" });
name: "damageLocationQuestion",
});
damageLocationQuestionWrapper.vm.initializeComponent = damageLocationQuestionWrapper.vm.initializeComponent =
damageLocationQuestion.methods.initializeComponent; damageLocationQuestion.methods.initializeComponent;
const funnelFooterWrapper = wrapper.findComponent({ const funnelFooterWrapper = wrapper.findComponent({ name: "funnelFooter" });
name: "funnelFooter", funnelFooterWrapper.vm.initializeComponent =
}); funnelFooter.methods.initializeComponent;
funnelFooterWrapper.vm.initializeComponent = funnelFooter.methods.initializeComponent;
return { wrapper, apiPromise }; return { wrapper, apiPromise };
} }

View file

@ -3,6 +3,8 @@
<funnelHeader ref="funnelHeader" /> <funnelHeader ref="funnelHeader" />
<vehicleBanner ref="vehicleBanner" :displayGenericVehicleImage=false /> <vehicleBanner ref="vehicleBanner" :displayGenericVehicleImage=false />
<funnelSubHeader ref="funnelSubHeader" /> <funnelSubHeader ref="funnelSubHeader" />
selectedDamageLocations {{selectedDamageLocations}}
isRearWindowDamageLocation {{ isRearWindowDamageLocation }}
<Form <Form
@submit="onSubmit" @submit="onSubmit"
@invalid-submit="onInvalidSubmit" @invalid-submit="onInvalidSubmit"
@ -80,15 +82,11 @@ import { storeActions } from "@/constants/store-actions";
import store from "@/store"; import store from "@/store";
import baseMixin from "@/mixins/base-mixin"; import baseMixin from "@/mixins/base-mixin";
import { Form, defineRule } from "vee-validate"; import { Form, defineRule } from "vee-validate";
import { required } from "@/helpers/validation-rules";
import errorMessages from "@/constants/error-messages";
// DEFINE VALIDATION RULES // DEFINE VALIDATION RULES
defineRule("replace-options-required", (value) => { defineRule("replace-options-required", required(errorMessages.REPLACE_OPTIONS_REQUIRED));
if (!value || value.length < 1) {
return "Please select rear window type";
}
return true;
});
export default { export default {
name: "vehicle-damage", name: "vehicle-damage",
@ -180,21 +178,20 @@ export default {
); );
}, },
onSubmit(values) { onSubmit(values) {
window.alert('Success! Submitted values: ' + JSON.stringify(values, null ,2)) // window.alert('Success! Submitted values: ' + JSON.stringify(values, null ,2))
console.log('submitted: ', JSON.stringify(values, null ,2)); // TODO - ADD IN SAVING OF DATA
// TODO - ADD IN ADVANCING TO NEXT PAGE
console.log('submitted: ', JSON.stringify(values, null, 2));
}, },
onInvalidSubmit({ values, errors, results }) { onInvalidSubmit({ values, errors, results }) {
// identify the first error field and put focus on it // identify the first error field and put focus on it
console.log("values: ", values);
console.log("errors: ", errors);
console.log("results: ", results);
// get error names array // get error names array
const errorNames = errors ? Object.keys(errors) : []; const errorNames = errors ? Object.keys(errors) : [];
const firstErrorEl = errorNames[0]; const firstErrorEl = errorNames[0];
if (firstErrorEl) { if (firstErrorEl) {
console.log('firstErrorEl: ', firstErrorEl);
const qsString = "[data-focus-target='" + firstErrorEl + "']"; const qsString = "[data-focus-target='" + firstErrorEl + "']";
document.querySelector(qsString).focus(); const el = document.querySelector(qsString);
el && el.focus();
} }
}, },
}, },

View file

@ -1,6 +1,5 @@
<template> <template>
<div class="windshield-options"> <div class="windshield-options">
selectedDamageLocations: {{selectedDamageLocations}}
<windshieldDamageTypeQuestion ref="windshieldDamageTypeQuestion" <windshieldDamageTypeQuestion ref="windshieldDamageTypeQuestion"
:isAvailable=isWindshieldDamageLocation :isAvailable=isWindshieldDamageLocation
:suppressError="suppressError" :suppressError="suppressError"
@ -29,15 +28,14 @@ import windshieldDamageTypeQuestion from"@/layouts/vehicle-damage/windshield-opt
import windshieldChipCountQuestion from"@/layouts/vehicle-damage/windshield-options/windshield-chip-count-question/windshield-chip-count-question"; import windshieldChipCountQuestion from"@/layouts/vehicle-damage/windshield-options/windshield-chip-count-question/windshield-chip-count-question";
import replaceOptionsQuestion from"@/layouts/vehicle-damage/replace-options-question/replace-options-question"; import replaceOptionsQuestion from"@/layouts/vehicle-damage/replace-options-question/replace-options-question";
import { defineRule } from "vee-validate"; import { defineRule } from "vee-validate";
import { required } from "@/helpers/validation-rules";
import errorMessages from "@/constants/error-messages";
// DEFINE VALIDATION RULES // DEFINE VALIDATION RULES
defineRule("windshield-damage-type-required", (value) => { defineRule("windshield-damage-type-required", required(errorMessages.WINDSHIELD_DAMAGE_TYPE_REQUIRED));
console.log('windshield-damage-type-required, value: ', value); defineRule("windshield-chip-count-required", required(errorMessages.WINDSHIELD_CHIP_COUNT_REQUIRED));
if (!value || value.length < 1) { defineRule("windshield-replace-options-required", required(errorMessages.WINSHIELD_REPLACE_OPTIONS_REQUIRED));
return "Please select windshield damage";
}
return true;
});
defineRule("checkForRepairAndReplace", (value, [other]) => { defineRule("checkForRepairAndReplace", (value, [other]) => {
console.log('checkForRepairAndReplace, value: ', value, ' / other: ', other); console.log('checkForRepairAndReplace, value: ', value, ' / other: ', other);
if (value.toString().toUpperCase().includes("REPAIR") && other.toString().toUpperCase().includes("WINDSHIELD") && other.length > 1) { if (value.toString().toUpperCase().includes("REPAIR") && other.toString().toUpperCase().includes("WINDSHIELD") && other.length > 1) {
@ -45,18 +43,6 @@ defineRule("checkForRepairAndReplace", (value, [other]) => {
} }
return true; return true;
}); });
defineRule("windshield-chip-count-required", (value) => {
if (!value || value.length < 1) {
return "Please select chip(s)";
}
return true;
});
defineRule("windshield-replace-options-required", (value) => {
if (!value || value.length < 1) {
return "Please select windshield part";
}
return true;
});
export default ({ export default ({
name: "windshieldOptions", name: "windshieldOptions",

View file

@ -186,12 +186,7 @@ describe("vehicle-style.vue", () => {
store: { store: {
commit: jest.fn(), commit: jest.fn(),
getters: { getters: {
vehicle: { vehicle: {},
year: 2020,
make: "Honda",
model: "Civic",
style: "2 Door",
},
}, },
}, },
actionList: [ actionList: [

View file

@ -208,7 +208,7 @@ describe("list-card.vue", () => {
}); });
wrapper.vm.handleCheckChange(); wrapper.vm.handleCheckChange();
// Assert // Assert
expect(wrapper.emitted()["isCheckedChanged"][0]).toEqual([{value: "List Card Checkbox", checkValue: Boolean}]); expect(wrapper.emitted()["isCheckedChanged"][0]).toEqual([{value: "List Card Checkbox", checkValue: [Boolean, String]}]);
}); });
it("Should set checkValue data if selectedButtonIDs has value(s)", async () => { it("Should set checkValue data if selectedButtonIDs has value(s)", async () => {

View file

@ -83,7 +83,6 @@ export default {
created(){ created(){
if(Array.isArray(this.selectedValues)){ if(Array.isArray(this.selectedValues)){
this.checkValue = this.isMultiSelect ? this.selectedValues.includes(this.value) : this.selectedValues[0]; this.checkValue = this.isMultiSelect ? this.selectedValues.includes(this.value) : this.selectedValues[0];
console.log('=================== this.checkValue: ', this.checkValue)
} }
}, },
computed: { computed: {
@ -101,9 +100,6 @@ export default {
}, },
methods: { methods: {
handleCheckChange(newValue, oldValue){ handleCheckChange(newValue, oldValue){
// console.log('LC handleCheckChange... oldValue: ', oldValue);
// console.log('LC handleCheckChange... newValue: ', newValue);
const isInitialization = typeof(oldValue) === 'function'; const isInitialization = typeof(oldValue) === 'function';
if (!isInitialization) { if (!isInitialization) {
this.$emit('isCheckedChanged', { checkValue: this.checkValue, value: this.value.toString() }); this.$emit('isCheckedChanged', { checkValue: this.checkValue, value: this.value.toString() });