Merge pull request #263 from Safelite/feature/CSR-319

Feature/CSR-319
This commit is contained in:
AdamCaouetteSafelite 2022-03-07 16:17:13 -05:00 committed by GitHub
commit 8850c11503
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
17 changed files with 473 additions and 174 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 { 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

@ -15,27 +15,38 @@
groupName="DamageLocationQuestion"
/>
<windshieldOptions
ref="windshieldOptions"
:showWindshieldDamageTypeQuestion="values.DamageLocationQuestion && values.DamageLocationQuestion.toString().includes('-Windshield')"
:suppressError="errors.windshieldDamageTypeQuestion && errors.windshieldDamageTypeQuestion.startsWith('checkForRepairAndReplace')"
ref="windshieldOptions"
v-model="selectedWindshieldOptions"
:hasRepairReplaceConflict="hasRepairReplaceConflict"
:selectedDamageLocations="selectedDamageLocations"
/>
<alert
class="my-3"
v-if="errors.windshieldDamageTypeQuestion && errors.windshieldDamageTypeQuestion.startsWith('checkForRepairAndReplace')"
v-show="hasRepairReplaceConflict"
alertClass="alert-danger"
alertHeadline="You'll need to schedule separate appointments"
alertCopy="Vehicle service requiring both glass repair and replacement must be scheduled separately, as they're performed by different technicians."
alertCopy="Vehicle service requiring both glass repair and replacement must be scheduled separately, as they're performed by different technicians. Continue scheduling your first service now, and then come back to schedule the second service."
:isDismissible="false"
/>
<sideDoorOptions
ref="sideDoorOptions"
groupName="SideDoorSideQuestion"
v-model="sideDoorOptionsData"
v-show="!hasRepairReplaceConflict"
:selectedDamageLocations="selectedDamageLocations"
/>
<replaceOptionsQuestion
ref="driverSideOptions"
isAvailable
v-model="driverSideOptionsData"
groupName="DriverSideReplaceOptionsQuestion"
ref="backGlassOptions"
:isAvailable="isRearWindowDamageLocation && !hasRepairReplaceConflict"
v-model="selectedRearReplaceOptions"
groupName="BackGlassReplaceOptionsQuestion"
validationRules="replace-options-required"
/>
<funnel-footer
ref="funnelFooter"
:isDisabled="!meta.valid"
@back-clicked="backButtonAction"
@ForwardClicked="forwardButtonAction"
/>
<!-- KEEP TEMPORARILY FOR TROUBLESHOOTING VALIDATION -->
@ -60,19 +71,23 @@ import vehicleBanner from "@/common-components/vehicle-banner/vehicle-banner";
import funnelSubHeader from "@/common-components/funnel-sub-header/funnel-sub-header";
import replaceOptionsQuestion from "@/layouts/vehicle-damage/replace-options-question/replace-options-question";
import damageLocationQuestion from "@/layouts/vehicle-damage/damage-location-question/damage-location-question";
import sideDoorOptions from "@/layouts/vehicle-damage/side-door-options/side-door-options";
import windshieldOptions from "@/layouts/vehicle-damage/windshield-options/windshield-options";
import alert from "@/ux-components/alert/alert";
import { Form } from 'vee-validate';
// Supporting files
import { fetchCmsContentForPage } from "@/helpers/cms-content-helper";
import { settleAllPromises } from "@/helpers/layout-helper";
import { storeActions } from "@/constants/store-actions";
import store from "@/store";
import baseMixin from "@/mixins/base-mixin";
import { Form, defineRule } from "vee-validate";
import { required } from "@/helpers/validation-rules";
import { errorMessages } from "@/constants/error-messages";
import { damageLocationsCms } from "@/constants/damage-locations-cms.js";
// DEFINE VALIDATION RULES
defineRule("replace-options-required", required(errorMessages.REPLACE_OPTIONS_REQUIRED));
export default {
name: "form-test",
@ -110,12 +125,19 @@ export default {
vm.$refs.funnelSubHeader.initializeComponent(
resultMap.cmsContent.FunnelSubHeaderWidget
);
vm.$refs.driverSideOptions.initializeComponent(
resultMap.cmsContent.DriverSideReplaceOptionsQuestion, resultMap.damageOptions.driverSideOptions.availableReplacementOptions
);
vm.$refs.damageLocation.initializeComponent(
resultMap.cmsContent.DamageLocationQuestion, resultMap.damageOptions
);
vm.$refs.sideDoorOptions.initializeComponent(
resultMap.cmsContent.SideDoorSideQuestion, resultMap.cmsContent.DriverSideReplaceOptionsQuestion, resultMap.cmsContent.PassengerSideReplaceOptionsQuestion, resultMap.damageOptions.driverSideOptions.availableReplacementOptions, resultMap.damageOptions.passengerSideOptions.availableReplacementOptions
);
vm.$refs.windshieldOptions.initializeComponent(
resultMap.cmsContent.WindshieldDamageTypeQuestion, resultMap.cmsContent.WindshieldChipCountQuestion,
resultMap.cmsContent.WindshieldReplaceOptionsQuestion, resultMap.damageOptions.windshieldOptions.availableReplacementOptions
);
vm.$refs.backGlassOptions.initializeComponent(
resultMap.cmsContent.RearReplaceOptionsQuestion, resultMap.damageOptions.backGlassOptions.availableReplacementOptions
);
vm.$refs.funnelFooter.initializeComponent(
resultMap.cmsContent.FunnelFooterWidget
);
@ -124,26 +146,20 @@ export default {
data(){
return {
selectedDamageLocations: [],
driverSideOptionsData: [],
sideDoorOptionsData: {
selectedDoorSides: [],
selectedDriverSideReplaceOptions: [],
selectedPassengerSideReplaceOptions: []
},
selectedWindshieldOptions: [],
selectedRearReplaceOptions: [],
}
},
components: {
Form,
funnelHeader,
vehicleBanner,
funnelSubHeader,
damageLocationQuestion,
replaceOptionsQuestion,
funnelFooter,
windshieldOptions,
alert,
},
methods: {
arePagePrerequisitesValid() {
return store.getters.vehicle.carId !== null;
},
resetDependentState() {
// Invokes
store.dispatch(storeActions.RESET_PARTS_STATE_AND_DEPENDENCIES);
},
onSubmit(values) {
@ -165,5 +181,64 @@ export default {
}
},
},
computed: {
isWindshieldDamageLocation() {
return this.selectedDamageLocations.some(selectedDamages =>
{
return selectedDamages.toUpperCase() === damageLocationsCms.WINDSHIELD;
});
},
isSideDoorDamageLocation() {
return this.selectedDamageLocations.some(selectedDamages =>
{
return selectedDamages.toUpperCase() === damageLocationsCms.SIDEDOOR;
});
},
isRearWindowDamageLocation() {
return this.selectedDamageLocations.some(selectedDamages =>
{
return selectedDamages.toUpperCase() === damageLocationsCms.REARWINDOW;
});
},
isWindshieldRepair() {
if (!this.isWindshieldDamageLocation) return false;
return this.selectedWindshieldOptions.selectedWindshieldDamageType && this.selectedWindshieldOptions.selectedWindshieldDamageType.some(selectedDamageType =>
{
return selectedDamageType.toUpperCase() === "REPAIR";
});
},
isDriverSideReplace() {
if (!this.isSideDoorDamageLocation) return false;
return this.sideDoorOptionsData.selectedDoorSides.some(selectedDriverSide =>
{
return selectedDriverSide.toUpperCase() === damageLocationsCms.DRIVERSIDE;
});
},
isPassengerSideReplace() {
if (!this.isSideDoorDamageLocation) return false;
return this.sideDoorOptionsData.selectedDoorSides.some(selectedPassengerSide =>
{
return selectedPassengerSide.toUpperCase() === damageLocationsCms.PASSENGERSIDE;
});
},
hasRepairReplaceConflict() {
return this.isWindshieldDamageLocation && this.selectedDamageLocations.length > 1 && this.isWindshieldRepair;
},
},
components: {
funnelHeader,
funnelFooter,
vehicleBanner,
funnelSubHeader,
sideDoorOptions,
damageLocationQuestion,
windshieldOptions,
replaceOptionsQuestion,
Form,
alert,
},
};
</script>

View file

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

View file

@ -9,7 +9,7 @@
:groupName="groupName"
buttonType="listCard"
v-model="selectedValues"
validationRules="replace-options-required"
:validationRules="validationRules"
/>
</div>
</transition>
@ -18,15 +18,6 @@
<script>
import buttonQuestion from "@/common-components/button-question/button-question";
import store from "@/store";
import { defineRule } from "vee-validate";
// DEFINE VALIDATION RULES
defineRule("replace-options-required", (value) => {
if (!value || value.length < 1) {
return "Please select window";
}
return true;
});
export default ({
name: "replaceOptionsQuestion",
@ -43,6 +34,7 @@ export default ({
groupName: String,
modelValue: Array,
isMultiSelect: Boolean,
validationRules: String,
},
methods: {
initializeComponent(cmsContent, replaceOptions){

View file

@ -9,13 +9,28 @@
:groupName="groupName"
buttonType="listCard"
v-model="selectedDoorSidesValues"
validationRules="damage-side-required"
/>
</div>
</transition>
<replaceOptionsQuestion ref="driverSideOptions" :isAvailable="isDriverSideReplaceOptionsQuestionAvailable" groupName="driverSideOptions" filterByVehicleCategory isMultiSelect v-model="selectedDriverSideReplaceOptionsValues" />
<replaceOptionsQuestion ref="passengerSideOptions" :isAvailable="isPassengerSideReplaceOptionsQuestionAvailable" groupName="passengerSideOptions" filterByVehicleCategory isMultiSelect v-model="selectedPassengerSideReplaceOptionsValues" />
<replaceOptionsQuestion
ref="driverSideOptions"
:isAvailable="isDriverSideReplaceOptionsQuestionAvailable"
groupName="driverSideOptions"
isMultiSelect
filterByVehicleCategory
v-model="selectedDriverSideReplaceOptionsValues"
validationRules="driver-side-options-required"
/>
<replaceOptionsQuestion
ref="passengerSideOptions"
:isAvailable="isPassengerSideReplaceOptionsQuestionAvailable"
groupName="passengerSideOptions"
isMultiSelect
filterByVehicleCategory
v-model="selectedPassengerSideReplaceOptionsValues"
validationRules="passenger-side-options-required"
/>
</div>
</template>
@ -23,6 +38,14 @@
import buttonQuestion from "@/common-components/button-question/button-question";
import replaceOptionsQuestion from "@/layouts/vehicle-damage/replace-options-question/replace-options-question";
import store from "@/store";
import { defineRule } from "vee-validate";
import { required } from "@/helpers/validation-rules";
import { errorMessages } from "@/constants/error-messages";
// DEFINE VALIDATION RULES
defineRule("damage-side-required", required(errorMessages.DAMAGE_SIDE_REQUIRED));
defineRule("driver-side-options-required", required(errorMessages.DRIVER_SIDE_OPTIONS_REQUIRED));
defineRule("passenger-side-options-required", required(errorMessages.PASSENGER_SIDE_OPTIONS_REQUIRED));
export default ({
name: "sideDoorOptions",

View file

@ -13,12 +13,13 @@ import replaceOptionsQuestion from "@/layouts/vehicle-damage/replace-options-que
import { settleAllPromises } from "@/helpers/layout-helper.js";
import baseMixin from "@/mixins/base-mixin";
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 { nextTick } from "vue";
import { storeActions } from "@/constants/store-actions";
import { storeMutations } from "@/constants/store-mutations";
import store from "@/store";
import { validate } from "vee-validate";
import { damageLocationsSelected } from "@/constants/damage-locations-selected.js";
// Mock our module for promises.
@ -38,7 +39,9 @@ jest.mock("@/store", () => ({
getters: {
vehicle: {
carId: "C00000000",
image: "test.jpg",
},
eventBusItem: jest.fn(),
damage: {
glassToReplace: []
},
@ -216,6 +219,11 @@ describe("vehicle-damage.vue", () => {
mountOptionsMockData: {
router: { navigateAfterSave: jest.fn(), },
actionList: [{ actionName: storeActions.GET_PARTS_OR_QUESTIONS, data: partsData, },],
store: {
getters: {
vehicle: {},
},
},
},
});
@ -277,7 +285,13 @@ describe("vehicle-damage.vue", () => {
pageHeaderWidgetHeaderText: "",
mountOptionsMockData: {
router: { navigate: jest.fn(), },
actionList: [{ actionName: storeActions.GET_PARTS_OR_QUESTIONS, data: partsData, },],},
actionList: [{ actionName: storeActions.GET_PARTS_OR_QUESTIONS, data: partsData, },],
store: {
getters: {
vehicle: {},
},
},
},
});
wrapper.vm.selectedDamageLocations = ["Windshield"];
@ -361,7 +375,13 @@ describe("vehicle-damage.vue", () => {
pageHeaderWidgetHeaderText: "",
mountOptionsMockData: {
router: { navigateAfterSave: jest.fn(), },
actionList: [{ actionName: storeActions.GET_PARTS_OR_QUESTIONS, data: partsData, },],},
actionList: [{ actionName: storeActions.GET_PARTS_OR_QUESTIONS, data: partsData, },],
store: {
getters: {
vehicle: {},
},
},
},
});
wrapper.vm.selectedDamageLocations = ["Windshield"];
@ -400,7 +420,13 @@ describe("vehicle-damage.vue", () => {
pageHeaderWidgetHeaderText: "",
mountOptionsMockData: {
router: { navigate: jest.fn(), },
actionList: [{ actionName: storeActions.GET_PARTS_OR_QUESTIONS, data: partsData, },],},
actionList: [{ actionName: storeActions.GET_PARTS_OR_QUESTIONS, data: partsData, },],
store: {
getters: {
vehicle: {},
},
},
},
});
wrapper.vm.selectedDamageLocations = ["Windshield"];
@ -618,7 +644,7 @@ describe("vehicle-damage.vue", () => {
(c) => c(wrapper.vm)
);
store.getters = { vehicle: { carId: "C0000000" }, damage: { glassToReplace: [{location: damageLocation}] }, isRepair: true};
store.getters = { vehicle: { carId: "C0000000" }, eventBusItem: jest.fn(), damage: { glassToReplace: [{location: damageLocation}] }, isRepair: true};
var glassSelections = wrapper.vm.getDamageLocationsFromStore();
@ -654,6 +680,7 @@ describe("vehicle-damage.vue", () => {
store.getters = {
vehicle:
{ carId: "C0000000" },
eventBusItem: jest.fn(),
damage:
{
glassToReplace: [{location: damageLocation, name: damageName}],
@ -689,7 +716,7 @@ describe("vehicle-damage.vue", () => {
(c) => c(wrapper.vm)
);
store.getters = { vehicle: { carId: "C0000000" }, damage: { glassToReplace: [{location: damageLocation, name: damageName}] }, isRepair: true};
store.getters = { vehicle: { carId: "C0000000" }, eventBusItem: jest.fn(), damage: { glassToReplace: [{location: damageLocation, name: damageName}] }, isRepair: true};
var glassSelections = wrapper.vm.getDriverSideReplaceOptionsFromStore();
@ -718,7 +745,7 @@ describe("vehicle-damage.vue", () => {
(c) => c(wrapper.vm)
);
store.getters = { vehicle: { carId: "C0000000" }, damage: { glassToReplace: [{location: damageLocation, name: damageName}] }, isRepair: true};
store.getters = { vehicle: { carId: "C0000000" }, eventBusItem: jest.fn(), damage: { glassToReplace: [{location: damageLocation, name: damageName}] }, isRepair: true};
var glassSelections = wrapper.vm.getPassengerSideReplaceOptionsFromStore();
@ -745,7 +772,7 @@ describe("vehicle-damage.vue", () => {
(c) => c(wrapper.vm)
);
store.getters = { vehicle: { carId: "C0000000" }, damage: { glassToReplace: [{location: damageLocation, name: damageName}] }, isRepair: true};
store.getters = { vehicle: { carId: "C0000000" }, eventBusItem: jest.fn(), damage: { glassToReplace: [{location: damageLocation, name: damageName}] }, isRepair: true};
var glassSelections = wrapper.vm.getRearReplaceOptionsFromStore();
@ -755,6 +782,72 @@ 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({
pageHeaderWidgetHeaderText = {},
@ -762,6 +855,11 @@ function setupMocks({
router: {
navigate: jest.fn(),
},
store: {
getters: {
vehicle: {},
},
},
},
}) {
//Mock api responses
@ -826,6 +924,7 @@ function setupMocks({
replaceOptionsQuestion.methods = {
initializeComponent: jest.fn(),
updateSelectedValues: jest.fn(),
};
funnelFooter.methods = {
@ -833,7 +932,9 @@ function setupMocks({
}
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" });
funnelHeaderWrapper.vm.initializeComponent =
@ -847,36 +948,25 @@ function setupMocks({
sideDoorOptionsWrapper.vm.initializeComponent =
sideDoorOptions.methods.initializeComponent;
const windshieldOptionsWrapper = wrapper.findComponent({
name: "windshieldOptions",
});
const windshieldOptionsWrapper = wrapper.findComponent({ name: "windshieldOptions" });
windshieldOptionsWrapper.vm.initializeComponent =
windshieldOptions.methods.initializeComponent;
const funnelSubHeaderWrapper = wrapper.findComponent({
name: "funnelSubHeader",
});
const funnelSubHeaderWrapper = wrapper.findComponent({ name: "funnelSubHeader" });
funnelSubHeaderWrapper.vm.initializeComponent =
funnelSubHeader.methods.initializeComponent;
const backGlassOptionsWrapper = wrapper.findComponent({
name: "replaceOptionsQuestion",
});
const backGlassOptionsWrapper = wrapper.findComponent({ name: "replaceOptionsQuestion" });
backGlassOptionsWrapper.vm.initializeComponent =
replaceOptionsQuestion.methods.initializeComponent;
const damageLocationQuestionWrapper = wrapper.findComponent({
name: "damageLocationQuestion",
});
const damageLocationQuestionWrapper = wrapper.findComponent({ name: "damageLocationQuestion" });
damageLocationQuestionWrapper.vm.initializeComponent =
damageLocationQuestion.methods.initializeComponent;
const funnelFooterWrapper = wrapper.findComponent({
name: "funnelFooter",
});
funnelFooterWrapper.vm.initializeComponent = funnelFooter.methods.initializeComponent;
const funnelFooterWrapper = wrapper.findComponent({ name: "funnelFooter" });
funnelFooterWrapper.vm.initializeComponent =
funnelFooter.methods.initializeComponent;
return { wrapper, apiPromise };
}

View file

@ -3,29 +3,52 @@
<funnelHeader ref="funnelHeader" />
<vehicleBanner ref="vehicleBanner" :displayGenericVehicleImage=false />
<funnelSubHeader ref="funnelSubHeader" />
<damageLocationQuestion
ref="damageLocation"
v-model="selectedDamageLocations"
groupName="DamageLocationQuestion"
/>
<windshieldOptions
ref="windshieldOptions"
v-model="selectedWindshieldOptions"
:selectedDamageLocations="selectedDamageLocations"
/>
<sideDoorOptions
ref="sideDoorOptions"
groupName="SideDoorSideQuestion"
v-model="sideDoorOptionsData"
:selectedDamageLocations="selectedDamageLocations"
/>
<replaceOptionsQuestion
ref="backGlassOptions"
:isAvailable="isRearWindowDamageLocation"
v-model="selectedRearReplaceOptions"
groupName="BackGlassReplaceOptionsQuestion"
/>
<funnelFooter ref="funnelFooter" @back-clicked="backButtonAction" @ForwardClicked="forwardButtonAction" />
<Form
@submit="onSubmit"
@invalid-submit="onInvalidSubmit"
ref="theForm"
v-slot="{ meta }"
>
<damageLocationQuestion
ref="damageLocation"
v-model="selectedDamageLocations"
groupName="DamageLocationQuestion"
/>
<windshieldOptions
ref="windshieldOptions"
v-model="selectedWindshieldOptions"
:hasRepairReplaceConflict="hasRepairReplaceConflict"
:selectedDamageLocations="selectedDamageLocations"
/>
<alert
class="my-3"
v-show="hasRepairReplaceConflict"
alertClass="alert-danger"
alertHeadline="You'll need to schedule separate appointments"
alertCopy="Vehicle service requiring both glass repair and replacement must be scheduled separately, as they're performed by different technicians. Continue scheduling your first service now, and then come back to schedule the second service."
:isDismissible="false"
/>
<sideDoorOptions
ref="sideDoorOptions"
groupName="SideDoorSideQuestion"
v-model="sideDoorOptionsData"
v-show="!hasRepairReplaceConflict"
:selectedDamageLocations="selectedDamageLocations"
/>
<replaceOptionsQuestion
ref="backGlassOptions"
:isAvailable="isRearWindowDamageLocation && !hasRepairReplaceConflict"
v-model="selectedRearReplaceOptions"
groupName="BackGlassReplaceOptionsQuestion"
validationRules="replace-options-required"
/>
<funnel-footer
ref="funnelFooter"
:isDisabled="!meta.valid"
@back-clicked="backButtonAction"
@ForwardClicked="forwardButtonAction"
/>
</Form>
</div>
</template>
@ -39,6 +62,7 @@ import sideDoorOptions from "@/layouts/vehicle-damage/side-door-options/side-doo
import damageLocationQuestion from "@/layouts/vehicle-damage/damage-location-question/damage-location-question";
import windshieldOptions from "@/layouts/vehicle-damage/windshield-options/windshield-options";
import replaceOptionsQuestion from "@/layouts/vehicle-damage/replace-options-question/replace-options-question";
import alert from "@/ux-components/alert/alert";
// Supporting files
import { fetchCmsContentForPage } from "@/helpers/cms-content-helper";
@ -46,9 +70,14 @@ import { settleAllPromises } from "@/helpers/layout-helper";
import { storeActions } from "@/constants/store-actions";
import store from "@/store";
import baseMixin from "@/mixins/base-mixin";
import { Form, defineRule } from "vee-validate";
import { required } from "@/helpers/validation-rules";
import { errorMessages } from "@/constants/error-messages";
import { damageLocationsCms } from "@/constants/damage-locations-cms.js";
import { damageLocationsSelected } from "@/constants/damage-locations-selected.js";
// DEFINE VALIDATION RULES
defineRule("replace-options-required", required(errorMessages.REPLACE_OPTIONS_REQUIRED));
export default {
name: "vehicle-damage",
@ -119,7 +148,7 @@ export default {
methods: {
arePagePrerequisitesValid() {
return store.getters.vehicle.carId !== null;
},
},
resetDependentState() {
store.dispatch(storeActions.RESET_PARTS_STATE_AND_DEPENDENCIES);
},
@ -130,6 +159,18 @@ export default {
this.$route
);
},
onSubmit() {}, // DO NOT REMOVE; needed to prevent default form submit behavior
onInvalidSubmit({ values, errors, results }) {
// identify the first error field and put focus on it
// get error names array
const errorNames = errors ? Object.keys(errors) : [];
const firstErrorEl = errorNames[0];
if (firstErrorEl) {
const qsString = "[data-focus-target='" + firstErrorEl + "']";
const el = document.querySelector(qsString);
el && el.focus();
}
},
getDamageLocationsFromStore() {
var glassSelections = [];
@ -304,7 +345,7 @@ export default {
return selectedGlassToReplace;
},
},
computed:{
computed: {
isWindshieldDamageLocation() {
return this.selectedDamageLocations.some(selectedDamages =>
{
@ -326,7 +367,7 @@ export default {
isWindshieldRepair() {
if (!this.isWindshieldDamageLocation) return false;
return this.selectedWindshieldOptions.selectedWindshieldDamageType.some(selectedDamageType =>
return this.selectedWindshieldOptions.selectedWindshieldDamageType && this.selectedWindshieldOptions.selectedWindshieldDamageType.some(selectedDamageType =>
{
return selectedDamageType.toUpperCase() === "REPAIR";
});
@ -346,7 +387,10 @@ export default {
{
return selectedPassengerSide.toUpperCase() === damageLocationsCms.PASSENGERSIDE;
});
}
},
hasRepairReplaceConflict() {
return this.isWindshieldDamageLocation && this.selectedDamageLocations.length > 1 && this.isWindshieldRepair;
},
},
components: {
@ -358,6 +402,8 @@ export default {
damageLocationQuestion,
windshieldOptions,
replaceOptionsQuestion,
Form,
alert,
},
};
</script>

View file

@ -8,6 +8,7 @@
buttonType="listButtonHorizontal"
useTextForValue
v-model="selectedChipCountValues"
:validationRules="validationRules"
/>
</div>
</transition>
@ -28,12 +29,19 @@ export default ({
modelValue: Array,
groupName: String,
isAvailable: Boolean,
validationRules: String,
},
methods: {
initializeComponent(cmsContent){
this.questionText = cmsContent.QuestionText;
this.answersFromCms = cmsContent.Answers;
}
},
updateSelectedValues() {
// UPDATE SELECTEDVALUES IF ONLY ONE ANSWER
if(Array.isArray(this.answersToDisplay) && this.answersToDisplay.length === 1 && this.selectedValues) {
this.selectedValues = [this.answersToDisplay[0].Name];
}
},
},
computed: {
selectedChipCountValues: {
@ -45,6 +53,12 @@ export default ({
}
},
},
watch: {
isAvailable(val) {
// CHECK TO UPDATE SELECTED VALUES WHEN ISAVAILABLE IS TRUE
val && this.updateSelectedValues();
}
},
components: {
buttonQuestion,
}

View file

@ -7,6 +7,8 @@
:groupName="groupName"
buttonType="listCard"
v-model="selectedValues"
:suppressError="suppressError"
:validationRules="validationRules"
/>
</div>
</transition>
@ -14,24 +16,7 @@
<script>
import buttonQuestion from "@/common-components/button-question/button-question";
//import { defineRule } from "vee-validate";
//DEFINE VALIDATION RULES
// add these to the button component above:
// validationRules="windshield-damage-required|checkForRepairAndReplace:@DamageLocationQuestion"
// :suppressError="suppressError"
// defineRule("windshield-damage-required", (value) => {
// if (!value || value.length < 1) {
// return "Please select windshield damage";
// }
// return true;
// });
// defineRule("checkForRepairAndReplace", (value, [other]) => {
// if (value === "Windshield-Chip" && other.toString().includes("Windshield") && other.length > 1) {
// return "checkForRepairAndReplace error"
// }
// return true;
// });
import store from "@/store";
export default ({
name: "windshieldDamageTypeQuestion",
@ -46,12 +31,13 @@ export default ({
groupName: String,
isAvailable: Boolean,
suppressError: Boolean,
validationRules: String,
},
methods: {
initializeComponent(cmsContent){
this.questionText = cmsContent.QuestionText;
this.answersFromCms = cmsContent.Answers;
}
},
},
computed: {
selectedValues: {

View file

@ -2,20 +2,23 @@
<div class="windshield-options">
<windshieldDamageTypeQuestion ref="windshieldDamageTypeQuestion"
:isAvailable=isWindshieldDamageLocation
:suppressError="suppressError"
:suppressError="hasRepairReplaceConflict"
groupName="WindshieldDamageTypeQuestion"
v-model="selectedWindshieldDamageTypeValues"
v-model="selectedWindshieldDamageTypeValues"
validationRules="windshield-damage-type-required|checkForRepairAndReplace:@DamageLocationQuestion"
/>
<windshieldChipCountQuestion ref="windshieldChipCountQuestion"
:isAvailable=isRepairOptionSelected
:isAvailable="isRepairOptionSelected && !hasRepairReplaceConflict"
groupName="WindshieldChipCountQuestion"
v-model="selectedWindshieldChipCountValues"
v-model="selectedWindshieldChipCountValues"
validationRules="windshield-chip-count-required"
/>
<replaceOptionsQuestion ref="replaceOptionsQuestion"
:isAvailable=isReplaceOptionSelected
isMultiSelect
groupName="WindshieldReplaceOptions"
v-model="selectedWindshieldReplaceOptionsValues"
validationRules="windshield-replace-options-required"
/>
</div>
</template>
@ -24,15 +27,29 @@
import windshieldDamageTypeQuestion from"@/layouts/vehicle-damage/windshield-options/windshield-damage-type-question/windshield-damage-type-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 { defineRule } from "vee-validate";
import { required } from "@/helpers/validation-rules";
import { errorMessages } from "@/constants/error-messages";
// DEFINE VALIDATION RULES
defineRule("windshield-damage-type-required", required(errorMessages.WINDSHIELD_DAMAGE_TYPE_REQUIRED));
defineRule("windshield-chip-count-required", required(errorMessages.WINDSHIELD_CHIP_COUNT_REQUIRED));
defineRule("windshield-replace-options-required", required(errorMessages.WINSHIELD_REPLACE_OPTIONS_REQUIRED));
defineRule("checkForRepairAndReplace", (value, [other]) => {
if (value.toString().toUpperCase().includes("REPAIR") && other.toString().toUpperCase().includes("WINDSHIELD") && other.length > 1) {
return "checkForRepairAndReplace error"
}
return true;
});
export default ({
name: "windshieldOptions",
props: {
modelValue: Array,
selectedDamageLocations: Array,
selectedChipCount: Array,
selectedReplaceOptions: Array,
suppressError: Boolean
hasRepairReplaceConflict: Boolean
},
methods: {

View file

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

View file

@ -1,9 +1,7 @@
<template>
<div
class="list-group list-button-horizontal d-flex flex-column w-100 mb-2"
:class="hasError ? 'has-error' : ''"
@mouseup="handleClick(value)"
@keyup.space="handleClick(value)"
:class="[errors.length > 0 ? 'has-error' : '', hasError ? 'has-error' : '']"
>
<input
:type="isMultiSelect ? 'checkbox' : 'radio'"
@ -12,6 +10,7 @@
:value="value"
:aria-required="isRequired"
:data-focus-target="groupName"
@click="handleClick(value)"
v-model="checkValue"
@change="!selectingInitiatesLoad ? handleCheckChange() : ''"
/>
@ -46,7 +45,6 @@
</template>
<script>
import { toRefs } from "vue";
import { useField } from "vee-validate";
import loader from "@/ux-components/loader/loader";
@ -69,6 +67,7 @@ export default {
type: String,
default: "",
},
validationRules: String,
selectedValues: [Array, String],
hasError: Boolean,
},
@ -105,20 +104,25 @@ export default {
loader,
},
setup(props) {
const { groupName, value } = toRefs(props);
const inputType = props.isMultiSelect ? "checkbox" : "radio";
const { checked, handleChange, errorMessage } = useField(
groupName,
undefined,
{
type: inputType,
checkedValue: value,
}
);
const fieldOptions = {
type: inputType,
checkedValue: props.value,
};
if (Array.isArray(props.selectedValues) && props.selectedValues.length == 1) {
fieldOptions['initialValue'] = fieldOptions.checkedValue;
}
const {
checked,
handleChange,
errors,
} = useField(props.groupName, props.validationRules, fieldOptions);
return {
checked,
handleChange,
errorMessage,
errors,
};
},
};

View file

@ -1,9 +1,7 @@
<template>
<div
class="list-group list-button d-flex flex-column w-100 mb-2"
:class="hasError ? 'has-error' : ''"
@mouseup="handleClick(value)"
@keyup.space="handleClick(value)"
:class="[errors.length > 0 ? 'has-error' : '', hasError ? 'has-error' : '']"
>
<input
:type="isMultiSelect ? 'checkbox' : 'radio'"
@ -12,6 +10,7 @@
:value="value"
:aria-required="isRequired"
:data-focus-target="groupName"
@click="handleClick(value)"
v-model="checkValue"
@change="!selectingInitiatesLoad ? handleCheckChange() : ''"
>
@ -49,9 +48,9 @@
</template>
<script>
import { toRefs } from "vue";
import { useField } from "vee-validate";
import loader from "@/ux-components/loader/loader";
export default {
name: "listButton",
props: {
@ -71,6 +70,7 @@ export default {
type: [String, Number],
default: "",
},
validationRules: String,
selectedValues: [Array, String],
hasError: Boolean,
},
@ -107,20 +107,25 @@ export default {
loader,
},
setup(props) {
const { groupName, value } = toRefs(props);
const inputType = props.isMultiSelect ? "checkbox" : "radio";
const { checked, handleChange, errorMessage } = useField(
groupName,
undefined,
{
type: inputType,
checkedValue: value,
}
);
const fieldOptions = {
type: inputType,
checkedValue: props.value,
};
if (Array.isArray(props.selectedValues) && props.selectedValues.length == 1) {
fieldOptions['initialValue'] = fieldOptions.checkedValue;
}
const {
checked,
handleChange,
errors,
} = useField(props.groupName, props.validationRules, fieldOptions);
return {
checked,
handleChange,
errorMessage,
errors,
};
},
};

View file

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

View file

@ -17,7 +17,7 @@
:data-focus-target="groupName"
@click="handleChange(value)"
v-model="checkValue"
@change="handleCheckChange()"
@change="handleCheckChange(value)"
/>
<label
:for="buttonID"
@ -83,8 +83,8 @@ export default {
},
data() {
return {
checkValue: Boolean,
};
checkValue: [Boolean, String],
}
},
created() {
if (Array.isArray(this.selectedValues)) {
@ -131,14 +131,21 @@ export default {
},
setup(props) {
const inputType = props.isMultiSelect ? "checkbox" : "radio";
const {
value: inputValue,
handleChange,
errors,
} = useField(props.groupName, props.validationRules, {
const fieldOptions = {
type: inputType,
checkedValue: props.value,
});
};
if (Array.isArray(props.selectedValues) && props.selectedValues.length == 1) {
fieldOptions['initialValue'] = fieldOptions.checkedValue;
}
const {
handleChange,
errors,
} = useField(props.groupName, props.validationRules, fieldOptions);
return {
handleChange,
errors,