Merging from develop

This commit is contained in:
Leah Schumann 2022-03-16 11:51:59 -04:00
commit 31450a374c
24 changed files with 685 additions and 147 deletions

View file

@ -37,8 +37,8 @@
</div>
</fieldset>
</div>
<div class="row mt-2 form-test-error" v-if="!suppressError">
<error-message :name="groupName"></error-message>
<div class="row mt-2 form-test-error">
<error-message :name="groupName" v-if="!suppressError"></error-message>
</div>
</div>
</template>
@ -133,9 +133,10 @@ export default {
if(this.isMultiSelect && this.selectedValues) {
// Add or remove item to array of data to emit
const newSelectedValues = this.selectedValues;
if(Array.isArray(this.selectedValues)) {
val.checkValue ? newSelectedValues.push(val.value) : newSelectedValues.splice(newSelectedValues.indexOf(val.value), 1);
this.selectedValues = newSelectedValues;
this.selectedValues = newSelectedValues;
}
} else {
this.selectedValues = [val.value];
@ -154,11 +155,11 @@ export default {
<style lang="scss" scoped>
.button-question-overflow {
height: calc(100vh - 280px);
height: calc(100vh - 252px);
.overflow-scroll {
// Height will be determined by overall height of content above list
height: calc(100% - 320px);
height: calc(100% - 300px);
-webkit-overflow-scrolling: touch;
}
}

View file

@ -16,7 +16,7 @@
</div>
<div class="container-fluid fixed-bottom g-4 bg-light py-4" id="infoBox">
<div class="row d-flex flex-row-reverse align-items-center">
<div class="col-12 button-col d-flex justify-content-end" id="stacked">
<div class="col button-col d-flex" id="stacked">
<buttonMain
isPrimary
:buttonText="buttonText"
@ -27,7 +27,7 @@
@click-event="buttonClick"
/>
</div>
<div class="col-12 link-col py-1">
<div class="col-auto link-col py-1 text-break">
<textLink
linkType="navigation"
:text="backLink"
@ -91,19 +91,21 @@ export default {
.footer {
display: flex;
margin-top: auto;
.button-col,
.link-col,
.btn-primary {
width: 100%;
}
a {
display: flex;
justify-content: center;
}
.col,
.col-auto,
.col button {
width: 100%;
}
@media only screen and (min-width: 340px) {
.button-col,
.link-col {
width: 50% !important;
.col,
.col-auto,
.col button {
width: auto;
justify-content: flex-end;
}
.btn-primary {
width: auto;

View file

@ -1129,6 +1129,16 @@
/>
</div>
</div>
<div class="row my-2">
<div class="col">
<alert
alertClass="alert-success"
alertHeadline="Multi-Paragraph Alert"
:alertCopy="['This is an example of a MULTI-PARAGRAPH alert, which takes an array of strings instead of a single string value for alertCopy.', 'This is the second item in the array of strings.']"
v-bind:isDismissible="false"
/>
</div>
</div>
</div>
</template>

View file

@ -10,6 +10,7 @@
buttonType="listCard"
v-model="selectedValues"
:validationRules="validationRules"
:suppressError="suppressError"
/>
</div>
</transition>
@ -35,6 +36,7 @@ export default ({
modelValue: Array,
isMultiSelect: Boolean,
validationRules: String,
suppressError: Boolean,
},
methods: {
initializeComponent(cmsContent, replaceOptions){

View file

@ -782,41 +782,6 @@ 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
//

View file

@ -1,23 +1,24 @@
<template>
<div class="container-fluid shadow rounded-3 p-2 position-relative make-tall">
<Form
@submit="onSubmit"
@invalid-submit="onInvalidSubmit"
ref="theForm"
v-slot="{ meta }"
>
<div class="container-fluid shadow rounded-3 p-2 position-relative make-tall">
<funnelHeader ref="funnelHeader" />
<vehicleBanner ref="vehicleBanner" :displayGenericVehicleImage=false />
<funnelSubHeader ref="funnelSubHeader" />
<Form
@submit="onSubmit"
@invalid-submit="onInvalidSubmit"
ref="theForm"
v-slot="{ meta }"
>
<damageLocationQuestion
ref="damageLocation"
v-model="selectedDamageLocations"
groupName="DamageLocationQuestion"
/>
<windshieldOptions
ref="windshieldOptions"
ref="windshieldOptions"
v-model="selectedWindshieldOptions"
:hasRepairReplaceConflict="hasRepairReplaceConflict"
:hasSplitSingleConflict="hasSplitSingleConflict"
:selectedDamageLocations="selectedDamageLocations"
/>
<alert
@ -25,10 +26,10 @@
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."
: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
<sideDoorOptions
ref="sideDoorOptions"
groupName="SideDoorSideQuestion"
v-model="sideDoorOptionsData"
@ -48,8 +49,8 @@
@back-clicked="backButtonAction"
@ForwardClicked="forwardButtonAction"
/>
</Form>
</div>
</div>
</Form>
</template>
<script>
@ -159,22 +160,9 @@ 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 = [];
if (store.getters.damage.glassToReplace.some(glass => { return glass.location === damageLocationsSelected.WINDSHIELD }) ||
store.getters.damage.isRepair) {
glassSelections.push(damageLocationsSelected.WINDSHIELD);
@ -193,7 +181,7 @@ export default {
getWindshieldOptionsFromStore() {
var windShieldOptions = { selectedWindshieldDamageType: [], selectedWindshieldChipCount: [], selectedWindshieldReplaceOptions: []};
if (store.getters.damage.isRepair === undefined) return windshieldOptions;
if (!store.getters.damage.isRepair) {
@ -222,7 +210,7 @@ export default {
}
return windShieldOptions;
},
getDoorSidesFromStore() {
@ -249,7 +237,7 @@ export default {
return driverSideReplaceOptions;
},
getPassengerSideReplaceOptionsFromStore() {
var passengerSideReplaceOptions = [];
@ -264,7 +252,7 @@ export default {
getRearReplaceOptionsFromStore(){
var rearReplaceOptions = [];
store.getters.damage.glassToReplace.forEach(glass => {
if (glass.location === damageLocationsSelected.REAR){
rearReplaceOptions.push(glass.name);
@ -284,10 +272,10 @@ export default {
}
store.commit(this.storeMutations.UPDATE_GLASS_TO_REPLACE, this.selectedGlassToReplace());
const partsData = await baseMixin.methods.dispatchNonBlockingStoreAction(this.storeActions.GET_PARTS_OR_QUESTIONS,
{ carId: store.getters.vehicle.carId, glassArray: this.selectedGlassToReplace()}, false);
this.navigateForward(partsData);
},
@ -297,7 +285,7 @@ export default {
this.$router.navigateAfterSave(this.navigationScenarios.SELECTED_DAMAGE_WITH_PART_QUESTIONS, this.$route, {}, {}, partsData.data);
return;
}
//found multiple parts for a single glass location (Windshield, Driver, Passenger, Rear)
if (partsData.data.partsOrQuestions){
for (var i = 0; i < partsData.data.partsOrQuestions.length; i++){
@ -312,7 +300,7 @@ export default {
if (!this.isWindshieldRepair){
store.commit(this.storeMutations.UPDATE_PARTS, partsData.data.partsOrQuestions[0].parts);
}
this.$router.navigate(this.navigationScenarios.SELECTED_DAMAGE_WITH_SINGLE_PART, this.$route);
},
@ -341,33 +329,33 @@ export default {
selectedGlassToReplace.push({ location: damageLocationsSelected.REAR, name: rearItem});
})
}
return selectedGlassToReplace;
},
},
computed: {
isWindshieldDamageLocation() {
return this.selectedDamageLocations.some(selectedDamages =>
return this.selectedDamageLocations.some(selectedDamages =>
{
return selectedDamages.toUpperCase() === damageLocationsCms.WINDSHIELD;
});
},
isSideDoorDamageLocation() {
return this.selectedDamageLocations.some(selectedDamages =>
return this.selectedDamageLocations.some(selectedDamages =>
{
return selectedDamages.toUpperCase() === damageLocationsCms.SIDEDOOR;
});
},
isRearWindowDamageLocation() {
return this.selectedDamageLocations.some(selectedDamages =>
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 this.selectedWindshieldOptions.selectedWindshieldDamageType && this.selectedWindshieldOptions.selectedWindshieldDamageType.some(selectedDamageType =>
{
return selectedDamageType.toUpperCase() === "REPAIR";
});
@ -375,7 +363,7 @@ export default {
isDriverSideReplace() {
if (!this.isSideDoorDamageLocation) return false;
return this.sideDoorOptionsData.selectedDoorSides.some(selectedDriverSide =>
return this.sideDoorOptionsData.selectedDoorSides.some(selectedDriverSide =>
{
return selectedDriverSide.toUpperCase() === damageLocationsCms.DRIVERSIDE;
});
@ -383,7 +371,7 @@ export default {
isPassengerSideReplace() {
if (!this.isSideDoorDamageLocation) return false;
return this.sideDoorOptionsData.selectedDoorSides.some(selectedPassengerSide =>
return this.sideDoorOptionsData.selectedDoorSides.some(selectedPassengerSide =>
{
return selectedPassengerSide.toUpperCase() === damageLocationsCms.PASSENGERSIDE;
});
@ -391,6 +379,23 @@ export default {
hasRepairReplaceConflict() {
return this.isWindshieldDamageLocation && this.selectedDamageLocations.length > 1 && this.isWindshieldRepair;
},
hasSplitSingleConflict() {
if (!this.selectedWindshieldOptions.selectedWindshieldReplaceOptions) return false;
return this.selectedWindshieldOptions.selectedWindshieldReplaceOptions.some(selectedSingleWindshield =>
{
return selectedSingleWindshield.toUpperCase() === damageLocationsSelected.SINGLE.toUpperCase();
}) &&
(this.selectedWindshieldOptions.selectedWindshieldReplaceOptions.some(selectedDriverWindshield =>
{
return selectedDriverWindshield.toUpperCase() === damageLocationsSelected.DRIVER.toUpperCase();
}) ||
this.selectedWindshieldOptions.selectedWindshieldReplaceOptions.some(selectedPassengerWindshield =>
{
return selectedPassengerWindshield.toUpperCase() === damageLocationsSelected.PASSENGER.toUpperCase();
})
);
},
},
components: {

View file

@ -1,12 +1,20 @@
<template>
<div class="windshield-options">
<windshieldDamageTypeQuestion ref="windshieldDamageTypeQuestion"
<windshieldDamageTypeQuestion ref="windshieldDamageTypeQuestion"
:isAvailable=isWindshieldDamageLocation
:suppressError="hasRepairReplaceConflict"
:suppressError="hasRepairReplaceConflict || showNoReplacementAvailableError"
groupName="WindshieldDamageTypeQuestion"
v-model="selectedWindshieldDamageTypeValues"
validationRules="windshield-damage-type-required|checkForRepairAndReplace:@DamageLocationQuestion"
:validationRules="windshieldDamageTypeQuestionValidationRules"
/>
<alert
class="my-3"
v-show="showNoReplacementAvailableError"
alertClass="alert-danger"
alertHeadline="Service not available"
:alertCopy="['We\'re sorry, but we currently offer only repair service for your vehicle type.', 'Need help with next steps? Call us at 800-394-0288.']"
:isDismissible="false"
/>
<windshieldChipCountQuestion ref="windshieldChipCountQuestion"
:isAvailable="isRepairOptionSelected && !hasRepairReplaceConflict"
groupName="WindshieldChipCountQuestion"
@ -18,8 +26,17 @@
isMultiSelect
groupName="WindshieldReplaceOptions"
v-model="selectedWindshieldReplaceOptionsValues"
validationRules="windshield-replace-options-required"
validationRules="windshield-replace-options-required|preventSplitAndSingleTogether"
:suppressError="hasSplitSingleConflict"
/>
<alert
class="my-3"
v-show="hasSplitSingleConflict"
alertClass="alert-danger"
alertHeadline="Single-piece or split?"
alertCopy="Please select just one windshield option: single-piece or split."
:isDismissible="false"
/>
</div>
</template>
@ -27,18 +44,40 @@
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 alert from "@/ux-components/alert/alert";
import { defineRule } from "vee-validate";
import { required } from "@/helpers/validation-rules";
import { errorMessages } from "@/constants/error-messages";
import { damageLocationsSelected } from "@/constants/damage-locations-selected.js";
// 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"
defineRule("checkForRepairAndReplace", (value, [otherFieldValue]) => {
if (value.toString().toUpperCase().includes(damageLocationsSelected.REPAIR.toUpperCase()) &&
otherFieldValue.toString().toUpperCase().includes(damageLocationsSelected.WINDSHIELD.toUpperCase()) &&
Array.isArray(otherFieldValue) &&
otherFieldValue.length > 1)
{
return false;
}
return true;
});
defineRule("repairOnly", (value) => {
if (value.toString().toUpperCase() === damageLocationsSelected.REPAIR.toUpperCase()) {
return true;
}
return false;
});
defineRule("preventSplitAndSingleTogether", (value) => {
if (value.toString().toUpperCase().includes(damageLocationsSelected.SINGLE.toUpperCase()) &&
(value.toString().toUpperCase().includes(damageLocationsSelected.DRIVER.toUpperCase()) ||
value.toString().toUpperCase().includes(damageLocationsSelected.PASSENGER.toUpperCase())))
{
return false;
}
return true;
});
@ -46,10 +85,17 @@ defineRule("checkForRepairAndReplace", (value, [other]) => {
export default ({
name: "windshieldOptions",
data(){
return {
windshieldAvailableReplacementOptions: Object,
}
},
props: {
modelValue: Array,
selectedDamageLocations: Array,
hasRepairReplaceConflict: Boolean
hasRepairReplaceConflict: Boolean,
hasSplitSingleConflict: Boolean,
},
methods: {
@ -59,12 +105,15 @@ export default ({
this.$refs.windshieldDamageTypeQuestion.initializeComponent(windshieldDamageTypeQuestionFromCms);
this.$refs.windshieldChipCountQuestion.initializeComponent(windshieldChipCountQuestionFromCms);
this.$refs.replaceOptionsQuestion.initializeComponent(windshieldReplaceOptionsQuestionFromCms, windshieldAvailableReplacementOptions);
this.windshieldAvailableReplacementOptions = windshieldAvailableReplacementOptions;
},
getWindshieldOptions(selectedWindshieldDamageType, selectedWindshieldChipCount, selectedWindshieldReplaceOptions){
// ONLY UPDATE THE NEW VALUE IF IT IS TRUTHY (NOT NULL)
return {
selectedWindshieldDamageType: selectedWindshieldDamageType,
selectedWindshieldChipCount: selectedWindshieldChipCount,
selectedWindshieldReplaceOptions: selectedWindshieldReplaceOptions
selectedWindshieldDamageType: selectedWindshieldDamageType ? selectedWindshieldDamageType : this.selectedValues.selectedWindshieldDamageType,
selectedWindshieldChipCount: selectedWindshieldChipCount ? selectedWindshieldChipCount : this.selectedValues.selectedWindshieldChipCount,
selectedWindshieldReplaceOptions: selectedWindshieldReplaceOptions ? selectedWindshieldReplaceOptions : this.selectedValues.selectedWindshieldReplaceOptions,
}
},
},
@ -102,7 +151,7 @@ export default ({
}
},
isWindshieldDamageLocation() {
return this.selectedDamageLocations.some(selectedDamages =>
return this.selectedDamageLocations.some(selectedDamages =>
{
return Boolean(selectedDamages.toUpperCase() === "WINDSHIELD");
});
@ -117,11 +166,37 @@ export default ({
return this.selectedWindshieldDamageTypeValues.some(val => val.toUpperCase() === "REPLACE") && this.isWindshieldDamageLocation;
},
isWindshieldReplaceAvailable() {
return !(Array.isArray(this.windshieldAvailableReplacementOptions) && this.windshieldAvailableReplacementOptions.length < 1);
},
isSplitWindshieldOption() {
const options = this.windshieldAvailableReplacementOptions.toString().toUpperCase();
if (options.includes('DRIVER') && options.includes('PASSENGER')) {
return true;
}
return false;
},
showNoReplacementAvailableError() {
if (this.isReplaceOptionSelected && !this.isWindshieldReplaceAvailable) {
return true;
}
return false;
},
windshieldDamageTypeQuestionValidationRules() {
// Note: the validation rules string is not dynamic (it cannot be changed once component has been created)
let validationRules = "windshield-damage-type-required|checkForRepairAndReplace:@DamageLocationQuestion";
// if vehicle has no windshield replacement option
if (!this.isWindshieldReplaceAvailable) {
validationRules = validationRules.concat('|repairOnly');
}
return validationRules;
}
},
components: {
windshieldDamageTypeQuestion,
windshieldChipCountQuestion,
replaceOptionsQuestion
replaceOptionsQuestion,
alert,
},
})
</script>

View file

@ -0,0 +1,167 @@
// Components
import vinLookup from "@/layouts/vin-lookup/vin-lookup.vue";
import vehicleDamage from "@/layouts/vehicle-damage/vehicle-damage.vue";
import funnelHeader from "@/common-components/funnel-header/funnel-header";
import funnelFooter from "@/common-components/funnel-footer/funnel-footer";
import vehicleBanner from "@/common-components/vehicle-banner/vehicle-banner";
import funnelSubHeader from "@/common-components/funnel-sub-header/funnel-sub-header";
import sideDoorOptions from "@/layouts/vehicle-damage/side-door-options/side-door-options";
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";
// Supporting Files
import { settleAllPromises } from "@/helpers/layout-helper.js";
import baseMixin from "@/mixins/base-mixin";
import { fetchCmsContentForPage } from "@/helpers/cms-content-helper";
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.
jest.mock("@/helpers/layout-helper.js", () => ({
settleAllPromises: jest.fn(),
}));
// Mock fetchCmsContentForPage
jest.mock("@/helpers/cms-content-helper", () => ({
fetchCmsContentForPage: jest.fn(),
}));
// Mock Store
jest.mock("@/store", () => ({
commit: jest.fn(),
dispatch: jest.fn(),
getters: {
vehicle: {
carId: "C00000000",
image: "test.jpg",
},
eventBusItem: jest.fn(),
damage: {
glassToReplace: []
},
},
}));
describe("vin-lookup.vue", () => {
test("Call resetDependentState", async() => {
const {wrapper} = setupMocks({});
wrapper.vm.resetDependentState();
expect(store.dispatch).toBeCalledWith(storeActions.RESET_REGISTRATION_STATE_AND_DEPENDENCIES);
});
});
// TEMP
function setupMocks({
pageHeaderWidgetHeaderText = {},
mountOptionsMockData = {
router: {
navigate: jest.fn(),
},
store: {
getters: {
vehicle: {},
},
},
},
}) {
//Mock api responses
baseMixin.methods.dispatchNonBlockingStoreAction = jest.fn();
const apiResponses = {
cmsContent: {
FunnelSubHeaderWidget: pageHeaderWidgetHeaderText,
VehicleBannerWidget: {
GenericVehicleImage:
"https://digitalconsumercms-dev.safelite.com/images/default-source/default-album/blurred-image.jpg?sfvrsn=a6ce3034_3",
},
FunnelHeaderWidget: {
LogoImage:
"https://digitalconsumercms-dev.safelite.com/images/default-source/default-album/safelite-logo.svg?sfvrsn=45e7ed06_3",
},
},
damageOptions: {
driverSideOptions: {
availableReplacementOptions: ["Front", "Back", "Side"],
},
passengerSideOptions: {
availableReplacementOptions: ["Front", "Back", "Side"],
},
windshieldOptions: {
availableReplacementOptions: ["Single", "Driver", "Passenger"],
},
backGlassOptions: {
availableReplacementOptions: ["Front", "Back", "Side"],
},
},
};
const apiPromise = Promise.resolve(apiResponses);
settleAllPromises.mockImplementation(() => apiPromise);
fetchCmsContentForPage.mockImplementation(() => Promise.resolve());
//Mock damage initialize methods
funnelHeader.methods = {
initializeComponent: jest.fn(),
};
vehicleBanner.methods = {
initializeComponent: jest.fn(),
};
funnelSubHeader.methods = {
initializeComponent: jest.fn(),
};
damageLocationQuestion.methods = {
initializeComponent: jest.fn(),
};
sideDoorOptions.methods = {
initializeComponent: jest.fn(),
};
windshieldOptions.methods = {
initializeComponent: jest.fn(),
};
replaceOptionsQuestion.methods = {
initializeComponent: jest.fn(),
updateSelectedValues: jest.fn(),
};
funnelFooter.methods = {
initializeComponent: jest.fn(),
}
const mountOptions = getMountOptions(mountOptionsMockData);
mountOptions['attachTo'] = document.body; // append wrapper to document.body to test DOM methods
const wrapper = mount(vinLookup, mountOptions);
const funnelHeaderWrapper = wrapper.findComponent({ name: "funnelHeader" });
funnelHeaderWrapper.vm.initializeComponent =
funnelHeader.methods.initializeComponent;
const vehicleBannerWrapper = wrapper.findComponent({ name: "vehicleBanner" });
vehicleBannerWrapper.vm.initializeComponent =
vehicleBanner.methods.initializeComponent;
const funnelSubHeaderWrapper = wrapper.findComponent({ name: "funnelSubHeader" });
funnelSubHeaderWrapper.vm.initializeComponent =
funnelSubHeader.methods.initializeComponent;
const funnelFooterWrapper = wrapper.findComponent({ name: "funnelFooter" });
funnelFooterWrapper.vm.initializeComponent =
funnelFooter.methods.initializeComponent;
return { wrapper, apiPromise };
}

View file

@ -0,0 +1,113 @@
<template>
<div class="container-fluid shadow rounded-3 p-2 position-relative make-tall">
<funnelHeader ref="funnelHeader" />
<vehicleBanner ref="vehicleBanner" />
<funnelSubHeader ref="funnelSubHeader" />
<h1>VIN Lookup Placeholder Page</h1>
<Form
@submit="onSubmit"
@invalid-submit="onInvalidSubmit"
ref="theForm"
v-slot="{ meta }"
class="d-flex flex-column h-100"
>
<funnel-footer
ref="funnelFooter"
:isDisabled="!meta.valid"
@back-clicked="backButtonAction"
@ForwardClicked="forwardButtonAction"
/>
</Form>
</div>
</template>
<script>
// Components
import funnelHeader from "@/common-components/funnel-header/funnel-header";
import vehicleBanner from "@/common-components/vehicle-banner/vehicle-banner";
import funnelSubHeader from "@/common-components/funnel-sub-header/funnel-sub-header";
import funnelFooter from "@/common-components/funnel-footer/funnel-footer";
// 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 { storeMutations } from "@/constants/store-mutations";
import baseMixin from "@/mixins/base-mixin";
import { Form, defineRule } from "vee-validate";
import { required } from "@/helpers/validation-rules";
import { errorMessages } from "@/constants/error-messages";
// DEFINE VALIDATION RULES
defineRule("replace-options-required", required(errorMessages.REPLACE_OPTIONS_REQUIRED));
export default {
name: "vin-lookup",
async beforeRouteEnter(to, from, next) {
// Call APIs
const cmsContentPromise = fetchCmsContentForPage(to.query.fmgPage);
const damageOptionsPromise =
baseMixin.methods.dispatchNonBlockingStoreAction(
storeActions.GET_DAMAGE_OPTIONS,
{ carId: store.getters.vehicle.carId }
);
// Settle promises and get results
const promiseResultMap = [
{
resultKey: "cmsContent",
promise: cmsContentPromise,
},
{
resultKey: "damageOptions",
promise: damageOptionsPromise,
},
];
const resultMap = await settleAllPromises(promiseResultMap);
// Call the "next" function to complete the transition to this page.
next((vm) => {
vm.$refs.funnelHeader.initializeComponent(
resultMap.cmsContent.FunnelHeaderWidget
);
vm.$refs.vehicleBanner.initializeComponent(
resultMap.cmsContent.VehicleBannerWidget
);
vm.$refs.funnelSubHeader.initializeComponent(
resultMap.cmsContent.FunnelSubHeaderWidget
);
vm.$refs.funnelFooter.initializeComponent(
resultMap.cmsContent.FunnelFooterWidget
);
});
},
methods: {
arePagePrerequisitesValid() {
return true;
},
resetDependentState() {
store.dispatch(storeActions.RESET_REGISTRATION_STATE_AND_DEPENDENCIES);
},
backButtonAction() {
// route to move backwards
this.$router.navigate(
this.navigationScenarios.CLICKED_BACK,
this.$route
);
},
},
components: {
funnelHeader,
vehicleBanner,
funnelSubHeader,
funnelFooter,
Form,
},
};
</script>
<style lang="scss">
</style>

View file

@ -20,7 +20,19 @@ export default {
},
savePageDataToStore(page, data){
store.commit(storeMutations.UPDATE_PAGE_DATA, { page: page, data: data });
}
},
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();
}
},
},
computed: {
storeActions() {

View file

@ -3,6 +3,7 @@ import { storeActions } from "@/constants/store-actions.js";
import { widgetNames } from "@/constants/widget-names.js";
import { storeMutations } from "@/constants/store-mutations.js";
import { navigationScenarios } from "@/router/router-constants/navigation-scenarios";
import { vehicleCategories } from "@/constants/vehicle-categories.js";
import store from "@/store";
describe("baseMixin.js", () => {
@ -77,6 +78,36 @@ describe("baseMixin.js", () => {
// Assert
expect(widgetNamesForTest).toEqual(widgetNames);
});
test("computed: vehicleCategories should be equal to import object", () => {
// Arrange
const mixIn = getMixInInstance({});
// Act
let vehicleCategoriesForTest = mixIn.computed.vehicleCategories();
// Assert
expect(vehicleCategoriesForTest).toEqual(vehicleCategories);
});
test("onInvalidSubmit: puts focus on first error", () => {
// Arrange
const mixIn = getMixInInstance({});
const validationData = {
errors: {
fieldOne: 'error message 1',
fieldTwo: 'error message 2',
}
};
global.document.querySelector = jest.fn();
// Act
mixIn.methods.onInvalidSubmit(validationData);
// Assert
expect(global.document.querySelector).toBeCalledWith("[data-focus-target='fieldOne']");
});
});
function getMixInInstance({ isDispatchSuccess = true }) {
@ -101,6 +132,7 @@ function getMixInInstance({ isDispatchSuccess = true }) {
baseMixIn.methods.$route = route;
baseMixIn.methods.storeActions = storeActions;
baseMixIn.methods.widgetNames = widgetNames;
baseMixIn.methods.vehicleCategories = vehicleCategories;
store.dispatch = storeDispatch;
store.commit = jest.fn();

View file

@ -5,9 +5,10 @@ const fmgPageValues = {
VEHICLE_STYLE: "vehicle-style",
VEHICLE_DAMAGE: "vehicle-damage",
ADDRESS_LOOKUP: "address-lookup",
VIN_LOOKUP: "vin-lookup",
VEHICLE_PARTS: "vehicle-parts",
PART_QUESTIONS: "part-questions",
REVEAL : "reveal",
};
export { fmgPageValues };
export { fmgPageValues };

View file

@ -84,6 +84,32 @@ const routingTable = [
},
],
},
{
fmgPageValue: fmgPageValues.REVEAL,
maps: [
{
scenario: navigationScenarios.CLICKED_BACK,
destinationFmgPageValue: fmgPageValues.VEHICLE_DAMAGE,
},
{
scenario: navigationScenarios.SELECTED_PARTS,
destinationFmgPageValue: fmgPageValues.VIN_LOOKUP,
},
],
},
{
fmgPageValue: fmgPageValues.VIN_LOOKUP,
maps: [
{
scenario: navigationScenarios.CLICKED_BACK,
destinationFmgPageValue: fmgPageValues.VEHICLE_DAMAGE,
},
{
scenario: navigationScenarios.VIN_LOOKUP,
destinationFmgPageValue: fmgPageValues.PART_QUESTIONS,
},
],
},
];
export { routingTable };

View file

@ -19,6 +19,21 @@ export const state = {
imageUrl: null,
imageVifNumber: null,
imageColor: null,
registration: {
licensePlate: null,
address: null,
city: null,
state: null,
zipCode: null,
firstName: null,
lastName: null,
},
},
serviceLocation: {
zip: null,
},
customer: {
emailAddress: null,
},
damage: {
isRepair: null,
@ -81,6 +96,33 @@ export const mutations = {
updatePageData(state, pageData){
state.applicationUser.pageData[pageData.page] = pageData.data;
},
updateRegistrationAddress(state, registrationAddress){
state.order.vehicle.registration.address = registrationAddress;
},
updateRegistrationCity(state, registrationCity){
state.order.vehicle.registration.city = registrationCity;
},
updateRegistrationState(state, registrationState){
state.order.vehicle.registration.state = registrationState;
},
updateRegistrationZipCode(state, registrationZipCode){
state.order.vehicle.registration.zipCode = registrationZipCode;
},
updateRegistrationFirstName(state, registrationFirstName){
state.order.vehicle.registration.firstName = registrationFirstName;
},
updateRegistrationLastName(state, registrationLastName){
state.order.vehicle.registration.lastName = registrationLastName;
},
updateRegistrationLicensePlate(state, registrationLicensePlate){
state.order.vehicle.registration.licensePlate = registrationLicensePlate;
},
updateServiceLocationZip(state, serviceLocationZip){
state.order.vehicle.serviceLocation.zip = serviceLocationZip;
},
updateCustomerEmailAddress(state, customerEmailAddress){
state.order.vehicle.customer.emailAddress = customerEmailAddress;
},
// EVENT BUS MUTATIONS
addEventToBus(state, event) {
@ -112,14 +154,16 @@ export const mutations = {
resetDamageState(state) {
state.order.damage.isRepair = null;
state.order.damage.numberOfChips = null;
state.order.damage.windshieldGlassToReplace = null;
state.order.damage.driverSideGlassToReplace = null;
state.order.damage.passengerSideGlassToReplace = null;
state.order.damage.rearGlassToReplace = null;
state.order.damage.glassToReplace = null;
},
resetRegistrationState(state) {
state.order.vehicle.registration.licensePlate = null;
state.order.vehicle.registration.address = null;
state.order.vehicle.registration.city = null;
state.order.vehicle.registration.state = null;
state.order.vehicle.registration.zipCode = null;
state.order.vehicle.registration.firstName = null;
state.order.vehicle.registration.lastName = null;
},
resetPartsState(state) {
state.order.lineItems.glassParts = null;
@ -292,4 +336,4 @@ export default createStore({
mutations,
getters,
actions,
});
});

View file

@ -139,19 +139,13 @@ describe("Mutations", () => {
storeState.order.damage = {
isRepair: true,
numberOfChips: 2,
windshieldGlassToReplace: "Front",
driverSideGlassToReplace: "Rear",
passengerSideGlassToReplace: "Rear",
rearGlassToReplace: "Slider"
glassToReplace: [{location: 'Rear', name: 'Stationary'}]
}
// Expect
expect(storeState.order.damage.isRepair).toEqual(true);
expect(storeState.order.damage.numberOfChips).toEqual(2);
expect(storeState.order.damage.windshieldGlassToReplace).toEqual("Front");
expect(storeState.order.damage.driverSideGlassToReplace).toEqual("Rear");
expect(storeState.order.damage.passengerSideGlassToReplace).toEqual("Rear");
expect(storeState.order.damage.rearGlassToReplace).toEqual("Slider");
expect(storeState.order.damage.glassToReplace).toStrictEqual([{location: 'Rear', name: 'Stationary'}]);
// Act
mutations.resetDamageState(storeState);
@ -159,11 +153,7 @@ describe("Mutations", () => {
// Expect
expect(storeState.order.damage.isRepair).toEqual(null);
expect(storeState.order.damage.numberOfChips).toEqual(null);
expect(storeState.order.damage.windshieldGlassToReplace).toEqual(null);
expect(storeState.order.damage.driverSideGlassToReplace).toEqual(null);
expect(storeState.order.damage.passengerSideGlassToReplace).toEqual(null);
expect(storeState.order.damage.rearGlassToReplace).toEqual(null);
expect(storeState.order.damage.glassToReplace).toEqual(null);
});
it("Updates number of chips in state", () => {

View file

@ -3,6 +3,7 @@
body {
font-size: 16px;
background-color: #fff;
color: #4D5151;
.container-fluid {
max-width: 576px; //Remove once desktop app is complete
&.container-shadow {
@ -22,7 +23,7 @@ body {
}
.container,
.container-fluid {
overflow-x: hidden;
overflow-x: hidden !important;
}
.sr-only {
@ -34,4 +35,3 @@ body {
overflow: hidden;
}
}
// Hide horizontal scrollbar

View file

@ -107,6 +107,9 @@ $theme-colors: (
"dark": $dark
);
//Default font color
$body-color: $gray-600;
//Fonts
$font-family-sans-serif: Roboto, Arial, Helvetica, sans-serif;
$font-family-monospace: SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;

View file

@ -1 +1,30 @@
test.todo("some test to be written in the future");
import { shallowMount } from "@vue/test-utils";
import alert from "./alert";
describe("alert.vue", () => {
it("Should set isMultiParagraph to true if alertCopy is an array of strings", async () => {
// Arrange
const wrapper = shallowMount(alert, {
propsData: {
alertCopy: ["one", "two"]
},
});
// Assert
expect(wrapper.vm.isMultiParagraph).toBe(true);
});
it("Should set isMultiParagraph to false if alertCopy is a single string", async () => {
// Arrange
const wrapper = shallowMount(alert, {
propsData: {
alertCopy: "three"
},
});
// Assert
expect(wrapper.vm.isMultiParagraph).toBe(false);
});
});

View file

@ -5,7 +5,12 @@
:class="[isDismissible ? 'alert-dismissible' : '', this.alertClass]"
>
<p class="m-0 fw-bold small alert-heading">{{ alertHeadline }}</p>
<p class="m-0 text-body small">{{ alertCopy }}</p>
<span v-if="isMultiParagraph">
<p class="m-1 text-body small" v-for="(para, index) in alertCopy" :key="index">
{{ para }}
</p>
</span>
<p class="m-0 text-body small" v-else>{{ alertCopy }}</p>
<button
type="button"
class="btn-close p-2"
@ -30,7 +35,7 @@ export default {
name: "alert",
props: {
alertHeadline: String,
alertCopy: String,
alertCopy: [Array, String],
isDismissible: Boolean,
/*
alertClass class names:
@ -41,6 +46,14 @@ export default {
*/
alertClass: String,
},
computed: {
isMultiParagraph() {
if (typeof this.alertCopy == "string") {
return false;
}
return true;
}
},
};
</script>

View file

@ -2,6 +2,8 @@
<div
class="list-group list-button-horizontal d-flex flex-column w-100 mb-2"
:class="[errors.length > 0 ? 'has-error' : '', hasError ? 'has-error' : '']"
@mouseup="handleClick(value)"
@keyup.space="handleClick(value)"
>
<input
:type="isMultiSelect ? 'checkbox' : 'radio'"
@ -10,7 +12,6 @@
:value="value"
:aria-required="isRequired"
:data-focus-target="groupName"
@click="handleClick(value)"
v-model="checkValue"
@change="!selectingInitiatesLoad ? handleCheckChange() : ''"
/>
@ -108,14 +109,18 @@ export default {
const fieldOptions = {
type: inputType,
checkedValue: props.value,
potentialInitialValue: props.selectedValues,
};
if (Array.isArray(props.selectedValues) && props.selectedValues.length == 1) {
fieldOptions['initialValue'] = fieldOptions.checkedValue;
// Set initialValue for validation setup if pre-selected
// NOTE: props.selectedValues could be an array of strings, or an array of integers...
if (props.selectedValues && (props.selectedValues.includes(props.value) || props.selectedValues.includes(parseInt(props.value)))) {
fieldOptions['initialValue'] = fieldOptions.potentialInitialValue;
}
const {
checked,
handleChange,
const {
checked,
handleChange,
errors,
} = useField(props.groupName, props.validationRules, fieldOptions);
@ -123,6 +128,7 @@ export default {
checked,
handleChange,
errors,
fieldOptions, // only need to expose this for unit test purposes
};
},
};
@ -135,6 +141,7 @@ export default {
opacity: 0;
width: 0;
height: 0;
position: absolute;
&:focus-visible + label {
box-shadow: 0 0 0 2px $blue;
z-index: 2;

View file

@ -166,11 +166,15 @@ describe("list-button.vue", () => {
isRequired: true,
isWide: false,
modelValue: ["List Card Checkbox"],
buttonID: 'list-card-id'
},
});
wrapper.vm.handleCheckChange();
// 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, buttonId: 'list-card-id'}]);
});
it("Should set checkValue data if selectedButtonIDs has value(s)", async () => {
@ -192,4 +196,5 @@ describe("list-button.vue", () => {
// Assert
expect(wrapper.componentVM.checkValue).toEqual("Car-Front");
});
});

View file

@ -2,6 +2,8 @@
<div
class="list-group list-button d-flex flex-column w-100 mb-2"
:class="[errors.length > 0 ? 'has-error' : '', hasError ? 'has-error' : '']"
@mouseup="handleClick(value)"
@keyup.space="handleClick(value)"
>
<input
:type="isMultiSelect ? 'checkbox' : 'radio'"
@ -10,7 +12,6 @@
:value="value"
:aria-required="isRequired"
:data-focus-target="groupName"
@click="handleClick(value)"
v-model="checkValue"
@change="!selectingInitiatesLoad ? handleCheckChange() : ''"
>
@ -99,7 +100,13 @@ export default {
handleCheckChange(newValue, oldValue){
const isInitialization = typeof(oldValue) === 'function';
if (!isInitialization) {
this.$emit('isCheckedChanged', { checkValue: this.checkValue, value: this.value.toString() });
const emitEvent = {
checkValue: this.checkValue,
value: this.value.toString(),
buttonId: this.buttonID.toString(),
};
this.$emit('isCheckedChanged', emitEvent);
this.$emit("update:modelValue", emitEvent);
}
},
},
@ -108,14 +115,19 @@ export default {
},
setup(props) {
const inputType = props.isMultiSelect ? "checkbox" : "radio";
const fieldOptions = {
type: inputType,
checkedValue: props.value,
potentialInitialValue: props.selectedValues,
};
if (Array.isArray(props.selectedValues) && props.selectedValues.length == 1) {
fieldOptions['initialValue'] = fieldOptions.checkedValue;
// Set initialValue for validation setup if pre-selected
// NOTE: props.selectedValues could be an array of strings, or an array of integers...
if (props.selectedValues && (props.selectedValues.includes(props.value) || props.selectedValues.includes(parseInt(props.value)))) {
fieldOptions['initialValue'] = fieldOptions.potentialInitialValue;
}
const {
checked,
handleChange,
@ -126,6 +138,7 @@ export default {
checked,
handleChange,
errors,
fieldOptions, // only need to expose this for unit test purposes
};
},
};

View file

@ -232,5 +232,20 @@ describe("list-card.vue", () => {
expect(wrapper.componentVM.checkValue).toEqual("Car-Front");
});
it("Should set an initial value for validation if selectedValues include the value", async () => {
// Arrange
const wrapper = shallowMount(listCard, {
propsData: {
value: "Windshield",
groupName: "radio 1",
modelValue: ["Windshield"],
selectedValues: ["Windshield"],
},
});
// Assert
expect(wrapper.vm.fieldOptions.initialValue).toEqual([ 'Windshield' ]);
});
});

View file

@ -7,6 +7,8 @@
errors.length > 0 ? 'has-error' : '',
hasError ? 'has-error' : '',
]"
@mouseup="handleChange(value)"
@keyup.space="handleChange(value)"
>
<input
:type="isMultiSelect ? 'checkbox' : 'radio'"
@ -15,7 +17,6 @@
:value="value"
:aria-required="isRequired"
:data-focus-target="groupName"
@click="handleChange(value)"
v-model="checkValue"
@change="handleCheckChange(value)"
/>
@ -57,6 +58,7 @@
<script>
import { useField } from "vee-validate";
export default {
name: "listCard",
props: {
@ -134,11 +136,14 @@ export default {
const fieldOptions = {
type: inputType,
checkedValue: props.value,
checkedValue: props.value, // EX: "Single" or "Passenger"
potentialInitialValue: props.selectedValues,
};
if (Array.isArray(props.selectedValues) && props.selectedValues.length == 1) {
fieldOptions['initialValue'] = fieldOptions.checkedValue;
// Set initialValue for validation setup if pre-selected
// NOTE: props.selectedValues could be an array of strings, or an array of integers...
if (props.selectedValues && (props.selectedValues.includes(props.value) || props.selectedValues.includes(parseInt(props.value)))) {
fieldOptions['initialValue'] = fieldOptions.potentialInitialValue;
}
const {
@ -149,6 +154,7 @@ export default {
return {
handleChange,
errors,
fieldOptions, // only need to expose this for unit test purposes
};
},
};
@ -183,6 +189,7 @@ export default {
opacity: 0;
width: 0;
height: 0;
position: absolute;
+ label {
display: block;
@ -262,6 +269,7 @@ export default {
height: 6px;
width: 11px;
transform: rotate(-45deg);
z-index: 1;
}
}