Merge branch 'develop' into feature/CSR-1422

This commit is contained in:
Leah Schumann 2023-08-02 09:39:02 -04:00
commit abcabbe1d6
33 changed files with 2069 additions and 246 deletions

View file

@ -26,8 +26,7 @@ module.exports = {
testMatch: ["**/*.spec.(js|jsx|ts|tsx)|**/__tests__/*.(js|jsx|ts|tsx)"],
coverageThreshold: {
global: {
statements: 75,
// Got the go ahead from Mark to temporarily lower this. Taking out initialize component made the year,make,model and style coverage drop a bit. Once unit tests for license plate lookup, vin lookup and address lookup are in the coverage should go back up to 90
statements: 80,
},
},
// Uncomment this to avoid the massive amount of warnings we are getting for onSubmit and onInvalidSubmit

View file

@ -12,6 +12,7 @@ const GaEvents = {
const GaCategories = {
API_RESPONSE: "Api_Response",
EVOX: "Evox",
FUNNEL_ENTRY: "funnel_entry",
};
const GaActions = {
@ -20,6 +21,7 @@ const GaActions = {
VIF: "vif",
SUBMITTED: "Submitted",
DISPLAYED: "Displayed",
ZIP_CODE_PROVIDED: "zip_code_provided",
};
const GaLabels = {

View file

@ -62,6 +62,7 @@ const storeActions = {
SAVE_VEHICLE_MAKE: "saveVehicleMake",
SAVE_VEHICLE_MODEL: "saveVehicleModel",
SAVE_VEHICLE_STYLE: "saveVehicleStyle",
SAVE_VEHICLE: "saveVehicle",
SAVE_VEHICLE_DAMAGE: "saveVehicleDamage",
SAVE_VIN_LOOKUP: "saveVinLookup",
SAVE_SERVICE_ZIP_CODE_INFO: "saveServiceZipCodeInfo",

View file

@ -74,6 +74,9 @@ https://safelite.atlassian.net/wiki/spaces/DC/pages/76644418/Button+Question+Com
</div>
<div class="row form-test-error">
<error-message
role="comment"
aria-atomic="true"
aria-live="polite"
class="small mt-1"
:name="formatString(groupName)"
v-if="!suppressError"></error-message>

View file

@ -44,7 +44,8 @@
type="radio"
name="day-of-month"
v-model="selectedDate"
@click="fireDateClickedEvent"
@click="fireDateSelectedEvent"
@keypress.enter="fireDateSelectedEvent"
:value="date.inputValue"
:id="`${month.monthLabel}-${date.dateNum.toString()}`" />
<label :for="`${month.monthLabel}-${date.dateNum.toString()}`">
@ -56,7 +57,7 @@
:class="[!isLoading ? 'date-picker-hidden' : '']"
loaderColor="blue"
loaderPosition="center" />
<div class="row form-test-error">
<div class="row form-test-error" id="date-of-month-error">
<ErrorMessage name="date-of-month" class="small mt-1"></ErrorMessage>
</div>
<button
@ -190,7 +191,11 @@ export default {
initializeComponent(initialData) {
this.setCalendarData(initialData);
},
fireDateClickedEvent() {
fireDateSelectedEvent(event) {
// Ignore if arrow key selected radioButton
if (event.screenX === 0 && event.screenY === 0) {
return;
}
this.$emit("date-clicked");
},
getWeekStartDate(dateString) {
@ -685,6 +690,12 @@ export default {
value: newValue,
});
},
errorMessage(newValue, oldValue) {
// Check for changing from no error message to some error message.
if (newValue && !oldValue) {
this.scrollToElement("date-of-month-error");
}
},
},
components: {
loader,
@ -700,7 +711,7 @@ export default {
}
.date-picker {
position: relative;
flex-grow: 1;
flex-grow: 0;
display: flex;
flex-direction: column;

View file

@ -16,14 +16,18 @@
:disabled="isDisabled"
:aria-required="isRequired"
:validationRules="validationRules"
:placeHolderText="placeHolderText">
:placeHolderText="placeHolderText"
:valueAsKey="valueAsKey">
<option v-if="placeHolderText" value="" selected>{{ placeHolderText }}</option>
<option v-for="(value, name, index) in options" :value="name" :key="index">
<option
v-for="(value, name, index) in options"
:value="valueAsKey == true ? value : name"
:key="valueAsKey == true ? value : index">
{{ value }}
</option>
</select>
<div v-show="errorMessage" class="row mt-2 form-test-error">
<span role="alert">{{ errorMessage }}</span>
<span aria-atomic="true" aria-live="polite">{{ errorMessage }}</span>
</div>
</div>
</template>
@ -47,6 +51,11 @@ export default {
placeHolderText: String,
cmsWidgetName: String,
hasError: Boolean,
valueAsKey: {
type: Boolean,
required: false,
default: false,
},
},
setup(props) {
const uuid = uuidv4();

View file

@ -68,7 +68,8 @@
<div v-show="errorMessage" class="row form-test-error">
<span
class="d-inline-flex small my-1"
role="alert"
aria-atomic="true"
aria-live="polite"
:class="[centerErrorMessage ? 'center-error-message' : '']"
>{{ errorMessage }}</span
>

View file

@ -19,15 +19,13 @@
tabindex="-1"
aria-labelledby="footerModalLabel"
aria-hidden="true"
v-on="{ 'show.bs.modal': show, 'hide.bs.modal': hide }"
:style="`height: calc(100% - ${currentFooterAndHeaderHeight}px);`">
v-on="{ 'show.bs.modal': show, 'hide.bs.modal': hide }">
<div class="menu-modal-container">
<button
class="menu-button"
type="button"
:class="[isActive ? 'active' : '']"
data-bs-toggle="modal"
data-bs-target="#footerModal"
@click="toggleModal"
aria-label="Hamburger Menu (modal window)">
<div class="bar1"></div>
<div class="bar2"></div>
@ -86,7 +84,6 @@ export default {
data() {
return {
isActive: false,
currentFooterAndHeaderHeight: 0,
};
},
methods: {
@ -104,7 +101,6 @@ export default {
Modal.getInstance(document.getElementById("footerModal")).hide();
},
show() {
this.currentFooterAndHeaderHeight = this.getFooterInfoBoxHeight() + 72;
this.isActive = true;
this.scrollToPageTop();
},

View file

@ -338,12 +338,10 @@ export default {
false
);
await this.dispatchStoreAction(
storeActions.SAVE_SERVICE_LOCATION,
storeActions.SAVE_SERVICE_ZIP_CODE_INFO,
{
address: this.customerQuestions.addressQuestions.streetAddress,
city: this.customerQuestions.addressQuestions.city,
zipCode: this.serviceZipCode,
state: resultMap.serviceZipValidationResponse.state,
zipCode: this.serviceZipCode,
zipCodeCtu: resultMap.serviceZipValidationResponse.zipCodeCtu,
},
false

View file

@ -4,11 +4,6 @@ import customerDetails from "@/layouts/customer-details/customer-details.vue";
// Supporting Files
import { shallowMount } from "@vue/test-utils";
import { getMountOptions } from "@/helpers/unit-test-helper.js";
import * as navigateToHeritage from "@/helpers/heritage-integration/navigation-helper";
jest.mock("@/helpers/heritage-integration/navigation-helper", () => ({
navigateToHeritageFunnel: jest.fn(),
}));
// Mock our module for promises.
jest.mock("@/helpers/layout-helper.js", () => ({
@ -36,7 +31,7 @@ describe("customer-details.vue", () => {
await wrapper.vm.forwardButtonAction();
// Assert
expect(wrapper.vm.$router.navigateWithoutSaving).toHaveBeenCalled();
expect(wrapper.vm.$router.navigateWithSaving).toHaveBeenCalled();
});
});
});

View file

@ -150,7 +150,7 @@ export default {
(store.getters.order.damage.isRepair ||
(store.getters.order.lineItems?.glassParts != null &&
store.getters.order.lineItems.glassParts.length > 0)) &&
!store.getters.payment.insuranceCoverage.isVerified
store.getters.order.referralNumber?.length !== 6
);
},
getDefaultIsInsuranceSelectedValue(availableLineItems) {

View file

@ -4,7 +4,7 @@
<textBlock
:cmsWidgetName="headerCmsWidgetName"
typeStyle="body small bold dark"
margin="mt-0" />
marginTopSizeOverride="0" />
<textLink
linkType="textSmall"
text="Edit"
@ -12,9 +12,12 @@
@click-event="linkClicked"
href="javascript:void(0)" />
</div>
<div class="small" v-for="item in content" :key="item" data-test="contentLine">
{{ item }}
</div>
<div
class="small review-block-content"
v-for="item in content"
:key="item"
data-test="contentLine"
v-html="item"></div>
</div>
</template>
@ -44,3 +47,9 @@ export default {
},
};
</script>
<style lang="scss" scoped>
:deep(.review-block-content > ul) {
margin-bottom: 0rem;
}
</style>

View file

@ -1,19 +1,258 @@
import { shallowMount } from "@vue/test-utils";
import { getMountOptions } from "@/helpers/unit-test-helper.js";
import damageReview from "@/layouts/review/review-sections/damage-review/damage-review";
jest.mock("@/helpers/cms-content-helper", () => ({
fetchCmsContentForPage: () => Promise.resolve("content"),
}));
import damageReview from "@/layouts/review/review-sections/damage-review/damage-review";
import { damageLocationsSelected as glassConstants } from "@/constants/damage-locations-selected";
const testConstants = {
cmsConstants: {
widgetNames: {
header: "DamageReviewWidget",
locations: "DamageLocationsWidget",
driverDamages: "DriverDamagesWidget",
passengerDamages: "PassengerDamagesWidget",
},
header: {
text: "Damage",
},
damageLocations: {
windshield: glassConstants.WINDSHIELD,
driver: glassConstants.DRIVER,
passenger: glassConstants.PASSENGER,
rear: glassConstants.REAR,
},
damageNames: {
vent: glassConstants.VENT,
front: glassConstants.FRONT,
back: glassConstants.BACK,
quarter: glassConstants.QUARTER,
side: glassConstants.SIDEDOOR,
},
locationCopy: {
windshield: "Windshield copy",
driver: "Driver copy",
passenger: "Passenger copy",
rear: "Rear copy",
},
damageCopy: {
vent: "Vent copy",
front: "Front copy",
back: "Back copy",
quarter: "Quarter copy",
side: "Side copy",
},
imageId: "00000000-0000-0000-0000-000000000000",
},
makeBulletedList: (items) => {
let list = "<ul>";
items.forEach((item) => {
list += `<li>${item}</li>`;
});
list += "</ul>";
return list;
},
glassItems: {
windshield: {
glassLocation: glassConstants.WINDSHIELD,
glassName: glassConstants.SINGLE,
},
rear: {
glassLocation: glassConstants.REAR,
glassName: glassConstants.STATIONARY,
},
passengerItems: {
vent: {
glassLocation: glassConstants.PASSENGER,
glassName: glassConstants.VENT,
},
front: {
glassLocation: glassConstants.PASSENGER,
glassName: glassConstants.FRONT,
},
back: {
glassLocation: glassConstants.PASSENGER,
glassName: glassConstants.BACK,
},
quarter: {
glassLocation: glassConstants.PASSENGER,
glassName: glassConstants.QUARTER,
},
side: {
glassLocation: glassConstants.PASSENGER,
glassName: glassConstants.SIDEDOOR,
},
},
driverItems: {
vent: {
glassLocation: glassConstants.DRIVER,
glassName: glassConstants.VENT,
},
front: {
glassLocation: glassConstants.DRIVER,
glassName: glassConstants.FRONT,
},
back: {
glassLocation: glassConstants.DRIVER,
glassName: glassConstants.BACK,
},
quarter: {
glassLocation: glassConstants.DRIVER,
glassName: glassConstants.QUARTER,
},
side: {
glassLocation: glassConstants.DRIVER,
glassName: glassConstants.SIDEDOOR,
},
},
},
};
let cmsContent;
describe("Damage Review Block", () => {
beforeEach(() => {
cmsContent = {
DamageReviewWidget: {
Text: testConstants.cmsConstants.header.text,
},
DamageLocationsWidget: {
Answers: [
{
Name: testConstants.cmsConstants.damageLocations.windshield,
Text: testConstants.cmsConstants.locationCopy.windshield,
SubText: "",
ImageId: testConstants.cmsConstants.imageId,
Image: "",
SubWidgetName: "",
},
{
Name: testConstants.cmsConstants.damageLocations.driver,
Text: testConstants.cmsConstants.locationCopy.driver,
SubText: "",
ImageId: testConstants.cmsConstants.imageId,
Image: "",
SubWidgetName: testConstants.cmsConstants.widgetNames.driverDamages,
},
{
Name: testConstants.cmsConstants.damageLocations.passenger,
Text: testConstants.cmsConstants.locationCopy.passenger,
SubText: "",
ImageId: testConstants.cmsConstants.imageId,
Image: "",
SubWidgetName: testConstants.cmsConstants.widgetNames.passengerDamages,
},
{
Name: testConstants.cmsConstants.damageLocations.rear,
Text: testConstants.cmsConstants.locationCopy.rear,
SubText: "",
ImageId: testConstants.cmsConstants.imageId,
Image: "",
SubWidgetName: "",
},
],
},
DriverDamagesWidget: {
Answers: [
{
Name: testConstants.cmsConstants.damageNames.vent,
Text: testConstants.cmsConstants.damageCopy.vent,
SubText: "",
ImageId: testConstants.cmsConstants.imageId,
Image: "",
SubWidgetName: "",
},
{
Name: testConstants.cmsConstants.damageNames.front,
Text: testConstants.cmsConstants.damageCopy.front,
SubText: "",
ImageId: testConstants.cmsConstants.imageId,
Image: "",
SubWidgetName: "",
},
{
Name: testConstants.cmsConstants.damageNames.back,
Text: testConstants.cmsConstants.damageCopy.back,
SubText: "",
ImageId: testConstants.cmsConstants.imageId,
Image: "",
SubWidgetName: "",
},
{
Name: testConstants.cmsConstants.damageNames.quarter,
Text: testConstants.cmsConstants.damageCopy.quarter,
SubText: "",
ImageId: testConstants.cmsConstants.imageId,
Image: "",
SubWidgetName: "",
},
{
Name: testConstants.cmsConstants.damageNames.side,
Text: testConstants.cmsConstants.damageCopy.side,
SubText: "",
ImageId: testConstants.cmsConstants.imageId,
Image: "",
SubWidgetName: "",
},
],
},
PassengerDamagesWidget: {
Answers: [
{
Name: testConstants.cmsConstants.damageNames.vent,
Text: testConstants.cmsConstants.damageCopy.vent,
SubText: "",
ImageId: testConstants.cmsConstants.imageId,
Image: "",
SubWidgetName: "",
},
{
Name: testConstants.cmsConstants.damageNames.front,
Text: testConstants.cmsConstants.damageCopy.front,
SubText: "",
ImageId: testConstants.cmsConstants.imageId,
Image: "",
SubWidgetName: "",
},
{
Name: testConstants.cmsConstants.damageNames.back,
Text: testConstants.cmsConstants.damageCopy.back,
SubText: "",
ImageId: testConstants.cmsConstants.imageId,
Image: "",
SubWidgetName: "",
},
{
Name: testConstants.cmsConstants.damageNames.quarter,
Text: testConstants.cmsConstants.damageCopy.quarter,
SubText: "",
ImageId: testConstants.cmsConstants.imageId,
Image: "",
SubWidgetName: "",
},
{
Name: testConstants.cmsConstants.damageNames.side,
Text: testConstants.cmsConstants.damageCopy.side,
SubText: "",
ImageId: testConstants.cmsConstants.imageId,
Image: "",
SubWidgetName: "",
},
],
},
};
});
describe("Correctly assembles damage info into a display string", () => {
test("Basic Replace", async () => {
test("Shows windshield copy when windshield damage is included", async () => {
// Arrange
const { wrapper } = setupMocks({
propsData: {
cmsWidgetName: testConstants.cmsConstants.widgetNames.header,
damageLocationsWidgetName: testConstants.cmsConstants.widgetNames.locations,
damage: {
isRepair: false,
glassToReplace: [testConstants.glassItems.windshield],
},
},
});
@ -22,16 +261,21 @@ describe("Damage Review Block", () => {
await wrapper.vm.$nextTick();
// Assert
expect(wrapper.vm.displayContent).toStrictEqual(["Windshield crack"]);
expect(wrapper.vm.displayContent).toStrictEqual([
testConstants.cmsConstants.locationCopy.windshield,
]);
});
test("Basic Repair", async () => {
test("Windshield copy is shown when order is a repair", async () => {
// Arrange
const { wrapper } = setupMocks({
propsData: {
cmsWidgetName: testConstants.cmsConstants.widgetNames.header,
damageLocationsWidgetName: testConstants.cmsConstants.widgetNames.locations,
damage: {
isRepair: true,
numberOfChips: 2,
glassToReplace: [],
},
},
});
@ -40,16 +284,21 @@ describe("Damage Review Block", () => {
await wrapper.vm.$nextTick();
// Assert
expect(wrapper.vm.displayContent).toStrictEqual(["Windshield repair - 2 chips"]);
expect(wrapper.vm.displayContent).toStrictEqual([
testConstants.cmsConstants.locationCopy.windshield,
]);
});
test("Basic Repair - no s with 1 chip", async () => {
test("Rear windshield copy shows when rear damage is present", async () => {
// Arrange
const { wrapper } = setupMocks({
propsData: {
cmsWidgetName: testConstants.cmsConstants.widgetNames.header,
damageLocationsWidgetName: testConstants.cmsConstants.widgetNames.locations,
damage: {
isRepair: true,
numberOfChips: 1,
isRepair: false,
numberOfChips: null,
glassToReplace: [testConstants.glassItems.rear],
},
},
});
@ -58,7 +307,116 @@ describe("Damage Review Block", () => {
await wrapper.vm.$nextTick();
// Assert
expect(wrapper.vm.displayContent).toStrictEqual(["Windshield repair - 1 chip"]);
expect(wrapper.vm.displayContent).toStrictEqual([
testConstants.cmsConstants.locationCopy.rear,
]);
});
test("Driver side copy and items are shown when driver side damage is present", async () => {
// Arrange
const { wrapper } = setupMocks({
propsData: {
cmsWidgetName: testConstants.cmsConstants.widgetNames.header,
damageLocationsWidgetName: testConstants.cmsConstants.widgetNames.locations,
damage: {
isRepair: false,
numberOfChips: null,
glassToReplace: [
testConstants.glassItems.driverItems.back,
testConstants.glassItems.driverItems.front,
],
},
},
});
// Act
await wrapper.vm.$nextTick();
// Assert
expect(wrapper.vm.displayContent).toStrictEqual([
testConstants.cmsConstants.locationCopy.driver,
testConstants.makeBulletedList([
testConstants.cmsConstants.damageCopy.front,
testConstants.cmsConstants.damageCopy.back,
]),
]);
});
test("Passenger side copy and items are shown when passenger side damage is present", async () => {
// Arrange
const { wrapper } = setupMocks({
propsData: {
cmsWidgetName: testConstants.cmsConstants.widgetNames.header,
damageLocationsWidgetName: testConstants.cmsConstants.widgetNames.locations,
damage: {
isRepair: false,
numberOfChips: null,
glassToReplace: [
testConstants.glassItems.passengerItems.quarter,
testConstants.glassItems.passengerItems.vent,
testConstants.glassItems.passengerItems.side,
],
},
},
});
// Act
await wrapper.vm.$nextTick();
// Assert
expect(wrapper.vm.displayContent).toStrictEqual([
testConstants.cmsConstants.locationCopy.passenger,
testConstants.makeBulletedList([
testConstants.cmsConstants.damageCopy.vent,
testConstants.cmsConstants.damageCopy.quarter,
testConstants.cmsConstants.damageCopy.side,
]),
]);
});
test("All relevant sections are shown in order in multiglass scenario", async () => {
// Arrange
const { wrapper } = setupMocks({
propsData: {
cmsWidgetName: testConstants.cmsConstants.widgetNames.header,
damageLocationsWidgetName: testConstants.cmsConstants.widgetNames.locations,
damage: {
isRepair: false,
numberOfChips: null,
glassToReplace: [
testConstants.glassItems.windshield,
testConstants.glassItems.rear,
testConstants.glassItems.driverItems.vent,
testConstants.glassItems.driverItems.front,
testConstants.glassItems.driverItems.back,
testConstants.glassItems.passengerItems.quarter,
testConstants.glassItems.passengerItems.back,
testConstants.glassItems.passengerItems.side,
],
},
},
});
// Act
await wrapper.vm.$nextTick();
// Assert
expect(wrapper.vm.displayContent).toStrictEqual([
testConstants.cmsConstants.locationCopy.windshield,
testConstants.cmsConstants.locationCopy.driver,
testConstants.makeBulletedList([
testConstants.cmsConstants.damageCopy.vent,
testConstants.cmsConstants.damageCopy.front,
testConstants.cmsConstants.damageCopy.back,
]),
testConstants.cmsConstants.locationCopy.passenger,
testConstants.makeBulletedList([
testConstants.cmsConstants.damageCopy.back,
testConstants.cmsConstants.damageCopy.quarter,
testConstants.cmsConstants.damageCopy.side,
]),
testConstants.cmsConstants.locationCopy.rear,
]);
});
});
});
@ -66,6 +424,16 @@ describe("Damage Review Block", () => {
function setupMocks(customMountOptions) {
const mountOptions = getMountOptions(customMountOptions);
const mockMixin = {
methods: {
getCmsContent: jest.fn((widgetName, cmsFieldName) => {
return cmsContent?.[widgetName]?.[cmsFieldName] ?? "";
}),
},
};
mountOptions.global.mixins = [mockMixin];
const wrapper = shallowMount(damageReview, mountOptions);
wrapper.vm.setCmsContent = jest.fn();
return { wrapper };

View file

@ -7,11 +7,13 @@
<script>
import reviewBlock from "@/layouts/review/review-block/review-block";
import { damageLocationsSelected } from "@/constants/damage-locations-selected.js";
export default {
name: "damage-review",
props: {
cmsWidgetName: String,
damageLocationsWidgetName: String,
damage: Object,
},
data() {
@ -21,17 +23,115 @@ export default {
editClicked() {
this.$emit("edit-clicked");
},
getAnswersNullSafe(widgetName) {
const rawAnswers = this.getCmsContent(widgetName, "Answers");
return rawAnswers ? rawAnswers : [];
},
getLocationAnswer(damageLocation) {
return this.locationAnswers?.find((answer) => answer.Name === damageLocation);
},
getGlassPieces(damageLocation) {
return this.damage?.glassToReplace?.filter(
(item) => item.glassLocation === damageLocation
);
},
generateBulletedListFromAnswers(answers) {
let list = "<ul>";
answers.forEach((answer) => {
list += `<li>${answer.Text}</li>`;
});
list += "</ul>";
return list;
},
},
computed: {
displayContent() {
return [
this.damage.isRepair
? `Windshield repair - ${this.damage.numberOfChips} chip${
this.damage.numberOfChips !== 1 ? "s" : ""
}`
: "Windshield crack",
...this.windshieldCopy,
...this.driverSideCopy,
...this.passengerSideCopy,
...this.rearCopy,
];
},
hasWindshieldDamage() {
const windshieldPieces = this.getGlassPieces(damageLocationsSelected.WINDSHIELD);
return this.damage.isRepair || !!windshieldPieces?.length;
},
hasDriverSideDamage() {
const driverPieces = this.getGlassPieces(damageLocationsSelected.DRIVER);
return !!driverPieces?.length;
},
hasPassengerSideDamage() {
const passengerPieces = this.getGlassPieces(damageLocationsSelected.PASSENGER);
return !!passengerPieces?.length;
},
hasRearDamage() {
const rearPieces = this.getGlassPieces(damageLocationsSelected.REAR);
return !!rearPieces?.length;
},
windshieldCopy() {
const answerContent = this.getLocationAnswer(damageLocationsSelected.WINDSHIELD);
if (this.hasWindshieldDamage) {
return [answerContent?.Text];
} else {
return [];
}
},
driverSideCopy() {
const answerContent = this.getLocationAnswer(damageLocationsSelected.DRIVER);
const damageAnswers = this.getAnswersNullSafe(answerContent?.SubWidgetName);
const driverSideItems = this.getGlassPieces(damageLocationsSelected.DRIVER);
const damageAnswersOnOrder = damageAnswers?.filter((answer) =>
driverSideItems?.some((glassPiece) => answer.Name === glassPiece.glassName)
);
if (this.hasDriverSideDamage) {
return [
answerContent?.Text,
this.generateBulletedListFromAnswers(damageAnswersOnOrder),
];
} else {
return [];
}
},
passengerSideCopy() {
const answerContent = this.getLocationAnswer(damageLocationsSelected.PASSENGER);
const damageAnswers = this.getAnswersNullSafe(answerContent?.SubWidgetName);
const passengerSideItems = this.getGlassPieces(damageLocationsSelected.PASSENGER);
const damageAnswersOnOrder = damageAnswers?.filter((answer) =>
passengerSideItems?.some((glassPiece) => answer.Name === glassPiece.glassName)
);
if (this.hasPassengerSideDamage) {
return [
answerContent?.Text,
this.generateBulletedListFromAnswers(damageAnswersOnOrder),
];
} else {
return [];
}
},
rearCopy() {
const answerContent = this.getLocationAnswer(damageLocationsSelected.REAR);
if (this.hasRearDamage) {
return [answerContent?.Text];
} else {
return [];
}
},
locationAnswers() {
return this.getAnswersNullSafe(this.damageLocationsWidgetName);
},
},
components: {
reviewBlock,

View file

@ -0,0 +1,129 @@
import { shallowMount } from "@vue/test-utils";
import { getMountOptions } from "@/helpers/unit-test-helper.js";
import serviceLocationReview from "@/layouts/review/review-sections/service-location-review/service-location-review";
import { AppointmentTypeStrings } from "@/constants/schedule-constants";
const cmsContent = {
ServiceLocationTitleWidget: {
Text: "Title Text",
},
};
describe("Service Location Review Block", () => {
it("Should render mobile address if mobile appointment", async () => {
// Arrange
const props = generateDefaultProps();
const { wrapper } = setupMocks({
propsData: props,
});
// Act
await wrapper.vm.$nextTick();
// Assert
expect(wrapper.vm.displayContent).toEqual([
"Mobile Address 1, Mobile Address 2, Mobile City, MO 11111",
]);
});
it("Should render service location address if inshop appointment.", async () => {
// Arrange
let props = generateDefaultProps();
props.serviceLocation.appointmentType = AppointmentTypeStrings.IN_SHOP;
const { wrapper } = setupMocks({
propsData: props,
});
// Act
await wrapper.vm.$nextTick();
// Assert
expect(wrapper.vm.displayContent).toEqual([
"Service Location Address, Service Location City, SL 22222",
]);
});
it("Should render service location address if drop-off appointment", async () => {
// Arrange
let props = generateDefaultProps();
props.serviceLocation.appointmentType = AppointmentTypeStrings.DROP_OFF;
const { wrapper } = setupMocks({
propsData: props,
});
// Act
await wrapper.vm.$nextTick();
// Assert
expect(wrapper.vm.displayContent).toEqual([
"Service Location Address, Service Location City, SL 22222",
]);
});
it("Should not add comma or any text if address2 is null", async () => {
// Arrange
let props = generateDefaultProps();
props.serviceLocation.address2 = null;
const { wrapper } = setupMocks({
propsData: props,
});
// Act
await wrapper.vm.$nextTick();
// Assert
expect(wrapper.vm.displayContent).toEqual(["Mobile Address 1, Mobile City, MO 11111"]);
});
});
function generateDefaultProps() {
return {
cmsWidgetName: "ServiceLocationTitleWidget",
serviceLocation: {
address: "Mobile Address 1",
address2: "Mobile Address 2",
city: "Mobile City",
state: "MO",
zipCode: "11111",
zipCodeCtu: "",
appointmentType: AppointmentTypeStrings.MOBILE,
isVehicleProtected: false,
provider: {
providerNumber: "",
address: {
streetAddress: "Service Location Address",
city: "Service Location City",
state: "SL",
zipCode: "22222",
zipCodeCtu: "",
},
},
},
};
}
function setupMocks(customMountOptions) {
const mountOptions = getMountOptions(customMountOptions);
const mockMixin = {
methods: {
getCmsContent: jest.fn((widgetName, cmsFieldName) => {
return cmsContent?.[widgetName]?.[cmsFieldName] ?? "";
}),
},
};
mountOptions.global.mixins = [mockMixin];
const wrapper = shallowMount(serviceLocationReview, mountOptions);
wrapper.vm.setCmsContent = jest.fn();
return { wrapper };
}

View file

@ -0,0 +1,58 @@
<template>
<reviewBlock
:headerCmsWidgetName="cmsWidgetName"
:content="displayContent"
@edit-clicked="editClicked" />
</template>
<script>
import reviewBlock from "@/layouts/review/review-block/review-block";
import { AppointmentTypeStrings } from "@/constants/schedule-constants";
export default {
name: "service-package-review",
props: {
cmsWidgetName: String,
serviceLocation: Object,
},
data() {
return {};
},
methods: {
editClicked() {
this.$emit("edit-clicked");
},
},
computed: {
displayContent() {
return [
`${this.addressInfo.address}${this.addressInfo.address2 ? ", " : ""}${
this.addressInfo.address2
}, ${this.addressInfo.city}, ${this.addressInfo.state} ${this.addressInfo.zipCode}`,
];
},
addressInfo() {
if (this.serviceLocation?.appointmentType === AppointmentTypeStrings.MOBILE) {
return {
address: this.serviceLocation?.address,
address2: this.serviceLocation?.address2 ?? "",
city: this.serviceLocation?.city,
state: this.serviceLocation?.state,
zipCode: this.serviceLocation?.zipCode,
};
} else {
return {
address: this.serviceLocation?.provider?.address?.streetAddress,
address2: "",
city: this.serviceLocation?.provider?.address?.city,
state: this.serviceLocation?.provider?.address?.state,
zipCode: this.serviceLocation?.provider?.address?.zipCode,
};
}
},
},
components: {
reviewBlock,
},
};
</script>

View file

@ -77,11 +77,8 @@ const figmaScenarios = [
{
name: "05_01_CSR_Quote_Cash",
params: {
availableVaps: [
testConstants.parts.frontWiperPart,
testConstants.parts.rearWiperPart,
testConstants.parts.rainDefensePart,
],
wiperResponse: [testConstants.parts.frontWiperPart, testConstants.parts.rearWiperPart],
rainDefenseResponse: testConstants.parts.rainDefensePart,
damage: {
isRepair: false,
glassToReplace: [testConstants.damages.frontWindshield],
@ -142,11 +139,8 @@ const figmaScenarios = [
{
name: "05_01_CSR_Quote_Standard_Repair",
params: {
availableVaps: [
testConstants.parts.frontWiperPart,
testConstants.parts.rearWiperPart,
testConstants.parts.rainDefensePart,
],
wiperResponse: [testConstants.parts.frontWiperPart, testConstants.parts.rearWiperPart],
rainDefenseResponse: testConstants.parts.rainDefensePart,
damage: {
isRepair: true,
glassToReplace: [],
@ -206,11 +200,8 @@ const figmaScenarios = [
{
name: "05_01_CSR_Quote_Recal",
params: {
availableVaps: [
testConstants.parts.frontWiperPart,
testConstants.parts.rearWiperPart,
testConstants.parts.rainDefensePart,
],
wiperResponse: [testConstants.parts.frontWiperPart, testConstants.parts.rearWiperPart],
rainDefenseResponse: testConstants.parts.rainDefensePart,
damage: {
isRepair: false,
glassToReplace: [testConstants.damages.frontWindshield],
@ -271,11 +262,8 @@ const figmaScenarios = [
{
name: "05_01_CSR_Quote_RearGlass+NonWindshield",
params: {
availableVaps: [
testConstants.parts.frontWiperPart,
testConstants.parts.rearWiperPart,
testConstants.parts.rainDefensePart,
],
wiperResponse: [testConstants.parts.frontWiperPart, testConstants.parts.rearWiperPart],
rainDefenseResponse: testConstants.parts.rainDefensePart,
damage: {
isRepair: false,
glassToReplace: [testConstants.damages.rearWindshield],
@ -343,11 +331,8 @@ const figmaScenarios = [
{
name: "05_01_CSR_Quote_RearGlass+Windshield",
params: {
availableVaps: [
testConstants.parts.frontWiperPart,
testConstants.parts.rearWiperPart,
testConstants.parts.rainDefensePart,
],
wiperResponse: [testConstants.parts.frontWiperPart, testConstants.parts.rearWiperPart],
rainDefenseResponse: testConstants.parts.rainDefensePart,
damage: {
isRepair: false,
glassToReplace: [
@ -461,7 +446,8 @@ const figmaScenarios = [
{
name: "05_01_CSR_Quote_RearGlassNoFrontFit",
params: {
availableVaps: [testConstants.parts.rearWiperPart, testConstants.parts.rainDefensePart],
wiperResponse: [testConstants.parts.rearWiperPart],
rainDefenseResponse: testConstants.parts.rainDefensePart,
damage: {
isRepair: false,
glassToReplace: [testConstants.damages.rearWindshield],
@ -518,11 +504,8 @@ const figmaScenarios = [
{
name: "05_01_CSR_Quote_Windshield+SideGlass",
params: {
availableVaps: [
testConstants.parts.frontWiperPart,
testConstants.parts.rearWiperPart,
testConstants.parts.rainDefensePart,
],
wiperResponse: [testConstants.parts.frontWiperPart, testConstants.parts.rearWiperPart],
rainDefenseResponse: testConstants.parts.rainDefensePart,
damage: {
isRepair: false,
glassToReplace: [
@ -586,11 +569,8 @@ const figmaScenarios = [
{
name: "05_01_CSR_Quote_SideGlass",
params: {
availableVaps: [
testConstants.parts.frontWiperPart,
testConstants.parts.rearWiperPart,
testConstants.parts.rainDefensePart,
],
wiperResponse: [testConstants.parts.frontWiperPart, testConstants.parts.rearWiperPart],
rainDefenseResponse: testConstants.parts.rainDefensePart,
damage: {
isRepair: false,
glassToReplace: [testConstants.damages.sideGlass],
@ -678,7 +658,8 @@ const figmaScenarios = [
{
name: "05_01_CSR_Quote_NoWiperFit",
params: {
availableVaps: [testConstants.parts.rainDefensePart],
wiperResponse: [],
rainDefenseResponse: testConstants.parts.rainDefensePart,
damage: {
isRepair: false,
glassToReplace: [testConstants.damages.frontWindshield],
@ -821,6 +802,8 @@ describe("Service Package Review Block", () => {
propsData: props,
});
initializeWithDefault(wrapper);
// Act
await wrapper.vm.$nextTick();
@ -851,6 +834,8 @@ describe("Service Package Review Block", () => {
propsData: props,
});
initializeWithDefault(wrapper);
// Act
await wrapper.vm.$nextTick();
@ -874,6 +859,8 @@ describe("Service Package Review Block", () => {
// Act
await wrapper.vm.$nextTick();
initializeWithDefault(wrapper);
// Assert
const includesFrontWiperCopy = wrapper.vm.displayContent.includes(
testConstants.vapsCopy.frontWiperCopy
@ -893,6 +880,8 @@ describe("Service Package Review Block", () => {
propsData: generateDefaultProps(),
});
initializeWithDefault(wrapper);
// Act
await wrapper.vm.$nextTick();
@ -908,7 +897,6 @@ describe("Service Package Review Block", () => {
it(`Should match figma scenario "${scenario.name}", iteration "${iteration.name}"`, async () => {
// Arrange
let props = generateDefaultProps();
props.availableVaps = scenario.params.availableVaps;
props.damage = scenario.params.damage;
props.glassParts = scenario.params.glassParts;
props.lineItems.supportingItems = scenario.params.supportingItems;
@ -918,6 +906,11 @@ describe("Service Package Review Block", () => {
propsData: props,
});
wrapper.vm.initializeComponent({
wipers: scenario.params.wiperResponse,
rainDefense: scenario.params.rainDefenseResponse,
});
// Act
await wrapper.vm.$nextTick();
@ -932,12 +925,18 @@ describe("Service Package Review Block", () => {
});
});
function initializeWithDefault(wrapper) {
wrapper.vm.initializeComponent({
wipers: [testConstants.parts.frontWiperPart],
rainDefense: testConstants.parts.rainDefensePart,
});
}
function generateDefaultProps() {
return {
servicePackageOptionsCmsName: testConstants.cmsPropValues.servicePackageOptionsCmsName,
defaultPackageItemsCmsName: testConstants.cmsPropValues.defaultPackageItemsCmsName,
vapsItemsCmsName: testConstants.cmsPropValues.vapsItemsCmsName,
availableVaps: [testConstants.parts.frontWiperPart, testConstants.parts.rainDefensePart],
lineItems: {
glassParts: [],
supportingItems: [],

View file

@ -11,6 +11,9 @@ import {
getHighestFullySatisfiedTier,
containsLineItemWithPartType,
} from "@/helpers/service-package-helper";
import { settleAllPromises } from "@/helpers/layout-helper";
import baseMixin from "@/mixins/base-mixin.js";
import { storeActions } from "@/constants/store-actions";
export default {
name: "service-package-review",
@ -18,14 +21,39 @@ export default {
servicePackageOptionsCmsName: String,
defaultPackageItemsCmsName: String,
vapsItemsCmsName: String,
availableVaps: Array,
lineItems: Object,
damage: Object,
},
data() {
return {};
return {
availableVaps: [],
};
},
methods: {
loadInitialData() {
const wipersPromise = baseMixin.methods.dispatchStoreAction(storeActions.GET_WIPERS);
const rainDefensePromise = baseMixin.methods.dispatchStoreAction(
storeActions.GET_RAIN_DEFENSE
);
const promiseResultMap = [
{
resultKey: "wipers",
promise: wipersPromise,
},
{
resultKey: "rainDefense",
promise: rainDefensePromise,
},
];
return settleAllPromises(promiseResultMap);
},
initializeComponent(apiResponses) {
const vapsFromApi = [...apiResponses.wipers, apiResponses.rainDefense];
this.availableVaps = vapsFromApi;
},
editClicked() {
this.$emit("edit-clicked");
},

View file

@ -48,19 +48,27 @@
<damageReview
cmsWidgetName="DamageReviewWidget"
damageLocationsWidgetName="DamageLocationsWidget"
:damage="damageInfo"
@edit-clicked="editDamage" />
<hr class="my-0" />
<servicePackageReview
ref="servicePackageReview"
servicePackageOptionsCmsName="ServicePackageTitle"
defaultPackageItemsCmsName="DefaultPackageItemDescriptions"
vapsItemsCmsName="VapsItemDescriptions"
:damage="damageInfo"
:availableVaps="availableVaps"
:lineItems="lineItems"
@edit-clicked="editServicePackage" />
<hr class="my-0" />
<serviceLocationReview
cmsWidgetName="ServiceLocationTitleWidget"
:serviceLocation="serviceLocationInfo"
@edit-clicked="editServiceLocation" />
</div>
<div>
@ -85,6 +93,7 @@ import textBlock from "@/digital-components/text-block/text-block";
import vehicleReview from "@/layouts/review/review-sections/vehicle-review/vehicle-review";
import damageReview from "@/layouts/review/review-sections/damage-review/damage-review";
import servicePackageReview from "@/layouts/review/review-sections/service-package-review/service-package-review";
import serviceLocationReview from "@/layouts/review/review-sections/service-location-review/service-location-review";
import { fetchCmsContentForPage } from "@/helpers/cms-content-helper";
import { settleAllPromises } from "@/helpers/layout-helper";
@ -102,6 +111,8 @@ export default {
storeActions.GET_RAIN_DEFENSE
);
const servicePackageInitialDataPromise = servicePackageReview.methods.loadInitialData();
// Settle promises and get results
const promiseResultMap = [
{
@ -109,12 +120,8 @@ export default {
promise: cmsContentPromise,
},
{
resultKey: "wipers",
promise: wipersPromise,
},
{
resultKey: "rainDefense",
promise: rainDefensePromise,
resultKey: "servicePackageData",
promise: servicePackageInitialDataPromise,
},
];
@ -123,21 +130,19 @@ export default {
next((vm) => {
vm.setCmsContent(resultMap.cmsContent);
vm.availableWipers = resultMap.wipers;
vm.availableRainDefense = [resultMap.rainDefense];
vm.$refs.servicePackageReview.initializeComponent(resultMap.servicePackageData);
});
},
data() {
return {
availableWipers: null,
availableRainDefense: null,
};
return {};
},
methods: {
arePagePrerequisitesValid() {
return true;
},
backButtonAction() {},
backButtonAction() {
this.$router.navigateWithoutSaving(this.navigationScenarios.CLICKED_BACK, this.$route);
},
forwardButtonAction() {},
editVehicle() {
this.$router.navigateWithoutSaving(
@ -157,6 +162,12 @@ export default {
this.$route
);
},
editServiceLocation() {
this.$router.navigateWithoutSaving(
this.navigationScenarios.CLICKED_SERVICE_LOCATION_EDIT,
this.$route
);
},
},
computed: {
subHeaderTitle() {
@ -174,12 +185,12 @@ export default {
damageInfo() {
return this.$store.getters.damage;
},
availableVaps() {
return [...(this.availableWipers ?? []), ...(this.availableRainDefense ?? [])];
},
lineItems() {
return this.$store.getters.lineItems;
},
serviceLocationInfo() {
return this.$store.getters.order.serviceLocation;
},
},
components: {
funnelHeader,
@ -190,6 +201,7 @@ export default {
vehicleReview,
damageReview,
servicePackageReview,
serviceLocationReview,
},
};
</script>

View file

@ -0,0 +1,228 @@
import { shallowMount } from "@vue/test-utils";
import { getMountOptions } from "@/helpers/unit-test-helper.js";
import { applicationConfig } from "@/constants/application-config";
import { storeActions } from "@/constants/store-actions";
import schedule from "@/layouts/schedule/schedule.vue";
import store from "@/store";
import * as navigateToHeritage from "@/helpers/heritage-integration/navigation-helper";
import { nextTick } from "vue";
jest.mock("@/store", () => ({
commit: jest.fn(),
dispatch: jest.fn(),
}));
// Mock fetchCmsContentForPage
jest.mock("@/helpers/cms-content-helper", () => ({
fetchCmsContentForPage: () => Promise.resolve("content"),
splitCopyOnCMSPlaceHolder: jest.fn(() => ["A", "B"]),
}));
jest.mock(
"@/store",
() => {
return {};
},
{ virtual: true }
);
store.getters = {
order: {
schedule: {
date: "2020-01-01",
},
lineItems: {
glassParts: [],
supportingItems: [],
},
serviceLocation: {
appointmentType: "Inshop",
},
},
lineItems: {
glassParts: [],
supportingItems: [],
},
};
describe("schedule.vue", () => {
test("Should navigateWithoutSaving", async () => {
//Arrange
const { wrapper } = setupMocks({
customMountOptions: {
router: {
navigateWithoutSaving: jest.fn(),
},
route: { schedule },
},
});
wrapper.vm.dispatchStoreAction = jest.fn(() => {
return {
data: [],
};
});
//Act
await wrapper.vm.forwardButtonAction();
//Assert
expect(wrapper.vm.$router.navigateWithoutSaving).toHaveBeenCalled();
});
test("should pass arePagePrerequisitesValid with a mobile order and no providerNumber", () => {
//Arrange
const { wrapper } = setupMocks({});
store.getters = {
order: {
schedule: {
date: "2020-01-01",
},
serviceLocation: {
zipCode: "12345",
zipCodeCtu: "value",
appointmentType: "Mobile",
},
damage: {
isRepair: true,
},
referralNumber: "1234567",
},
payment: {
isInsurance: true,
},
lineItems: {
supportingItems: [],
},
};
let arePagePrerequisitesValid = wrapper.vm.arePagePrerequisitesValid();
expect(arePagePrerequisitesValid).toBe(true);
});
test("should pass arePagePrerequisitesValid with a inshop order and providerNumber", () => {
//Arrange
const { wrapper } = setupMocks({});
store.getters = {
order: {
schedule: {
date: "2020-01-01",
},
serviceLocation: {
zipCode: "12345",
zipCodeCtu: "value",
appointmentType: "Inshop",
provider: {
providerNumber: "5",
},
},
damage: {
isRepair: true,
},
referralNumber: "1234567",
},
payment: {
isInsurance: true,
},
lineItems: {
supportingItems: [],
},
};
let arePagePrerequisitesValid = wrapper.vm.arePagePrerequisitesValid();
expect(arePagePrerequisitesValid).toBe(true);
});
test("should fail arePagePrerequisitesValid with a replace with no glass parts", () => {
//Arrange
const { wrapper } = setupMocks({});
store.getters = {
order: {
schedule: {
date: "2020-01-01",
},
serviceLocation: {
zipCode: "12345",
zipCodeCtu: "value",
appointmentType: "Inshop",
provider: {
providerNumber: "5",
},
},
damage: {
isRepair: false,
},
referralNumber: "1234567",
},
payment: {
isInsurance: true,
},
lineItems: {
supportingItems: [],
glassParts: [],
},
};
let arePagePrerequisitesValid = wrapper.vm.arePagePrerequisitesValid();
expect(arePagePrerequisitesValid).toBe(false);
});
test("should fail arePagePrerequisitesValid without isInsurance", () => {
//Arrange
const { wrapper } = setupMocks({});
store.getters = {
order: {
schedule: {
date: "2020-01-01",
},
serviceLocation: {
zipCode: "12345",
zipCodeCtu: "value",
appointmentType: "Inshop",
provider: {
providerNumber: "5",
},
},
damage: {
isRepair: true,
},
referralNumber: "1234567",
},
payment: {
isInsurance: null,
},
lineItems: {
supportingItems: [],
glassParts: [],
},
};
let arePagePrerequisitesValid = wrapper.vm.arePagePrerequisitesValid();
expect(arePagePrerequisitesValid).toBe(false);
});
});
const mockCmsContent = {};
function setupMocks({ customMountOptions }) {
const mountOptions = getMountOptions({
...customMountOptions,
});
mountOptions.global.mocks["$store"] = store;
mountOptions["attachTo"] = document.body;
mountOptions.mixins = [
{
methods: {
getCmsContent: jest.fn().mockImplementation((widgetName, fieldName) => {
if (mockCmsContent[widgetName] && mockCmsContent[widgetName][fieldName])
return mockCmsContent[widgetName][fieldName];
}),
},
},
];
const wrapper = shallowMount(schedule, mountOptions);
wrapper.vm.setCmsContent = jest.fn();
return { wrapper };
}

View file

@ -0,0 +1,712 @@
import { shallowMount, flushPromises } from "@vue/test-utils";
import { getMountOptions } from "@/helpers/unit-test-helper.js";
import timeSlotModalQuestion from "@/layouts/schedule/time-slot-modal-question/time-slot-modal-question.vue";
import store from "@/store";
import {
RouteCodeFlags,
PREMIUM_TIME_SLOT_ID_FLAG,
PREMIUM_FEE_PART_TYPE,
} from "@/constants/schedule-constants";
describe("time-slot-modal-list-button-question.vue", () => {
test("When selected time slot is changed, a correctly formatted selectedTimeSlot should be emitted", async () => {
//Arrange
const date = "2023-07-01";
const timeSlotId = "testRouteCodeId";
const startTime = "8:00 AM";
const endTime = "8:30 AM";
const jobMaxMinutes = 100;
const expectedEmit = [
[
{
date: date,
routeCode: timeSlotId,
startTime: startTime,
endTime: endTime,
jobMaxMinutes: jobMaxMinutes.toString(),
},
],
];
const { wrapper } = setupMocks({
customMountOptions: {
propsData: {
estimatedServiceMinutesMaximum: jobMaxMinutes,
dateAndTimeSlotData: {
date: date,
timeSlots: [
{
id: timeSlotId,
startTime: startTime,
endTime: endTime,
},
],
},
},
},
});
wrapper.vm.$refs.timeSlots.closeModal = jest.fn();
wrapper.vm.selectedTimeSlotId = timeSlotId;
//Act
wrapper.vm.setSelectedTimeSlot();
//Assert
expect(wrapper.emitted()["update:modelValue"]).toEqual(expectedEmit);
});
});
describe("time-slot-modal-list-button-question.vue Supplemental information", () => {
test("The correct supplemental information should be obtained from CMS - Mobile - Not Premium ", async () => {
//Arrange
const mobileCmsWidgetName = "MobileNotPremium";
const { wrapper } = setupMocks({
customMountOptions: {
propsData: {
modelValue: { routeCode: "test" },
appointmentType: "Mobile",
mobileCmsWidgetName: mobileCmsWidgetName,
},
},
});
//Assert
expect(wrapper.vm.supplementalInformationBlock).toEqual(
mockCmsContent[mobileCmsWidgetName]["BodyText"]
);
});
test("The correct supplemental information should be obtained from CMS - Mobile - Premium", async () => {
//Arrange
const mobileCmsWidgetName = "MobilePremium";
const { wrapper } = setupMocks({
customMountOptions: {
propsData: {
modelValue: { routeCode: "test" + PREMIUM_TIME_SLOT_ID_FLAG },
appointmentType: "Mobile",
mobileCmsWidgetName: mobileCmsWidgetName,
},
},
});
//Assert
expect(wrapper.vm.supplementalInformationBlock).toEqual(
mockCmsContent[mobileCmsWidgetName]["BodyText"]
);
});
test("No supplemental information should be obtained from CMS - Inshop", async () => {
//Arrange
const inshopWidgetName = "Inshop";
const { wrapper } = setupMocks({
customMountOptions: {
propsData: {
modelValue: { routeCode: "test" },
appointmentType: "Inshop",
cmsWidgetName: inshopWidgetName,
},
},
});
//Assert
expect(wrapper.vm.supplementalInformationBlock).toBeNull();
});
test("No supplemental information should be obtained from CMS - Drop off, nothing selected", async () => {
//Arrange
const dropOffWidgetName = "Dropoff";
const { wrapper } = setupMocks({
customMountOptions: {
propsData: {
modelValue: null,
appointmentType: "Dropoff",
cmsWidgetName: dropOffWidgetName,
},
},
});
//Assert
expect(wrapper.vm.supplementalInformationBlock).toBeNull();
});
test("Correct supplemental information should be retrieved, Dropoff regular", async () => {
//Arrange
const dropOffWidgetName = "Dropoff";
const { wrapper } = setupMocks({
customMountOptions: {
propsData: {
modelValue: { routeCode: "Test" + RouteCodeFlags.ALL_DAY_DROP_OFF },
appointmentType: "Dropoff",
dropoffCmsWidgetName: dropOffWidgetName,
},
},
});
//Assert
expect(wrapper.vm.supplementalInformationBlock).toEqual(
mockCmsContent[dropOffWidgetName]["BodyText"]
);
});
test("Correct supplemental information should be retrieved, Dropoff Overnight", async () => {
//Arrange
const overnightDropOffCmsWidgetName = "Overnight";
const { wrapper } = setupMocks({
customMountOptions: {
propsData: {
modelValue: { routeCode: "Test" + RouteCodeFlags.OVERNIGHT_DROP_OFF },
appointmentType: "Dropoff",
overnightDropOffCmsWidgetName: overnightDropOffCmsWidgetName,
},
},
});
//Assert
expect(wrapper.vm.supplementalInformationBlock).toEqual(
mockCmsContent[overnightDropOffCmsWidgetName]["BodyText"]
);
});
test("Correct supplemental information should be retrieved, Dropoff Sameday", async () => {
//Arrange
const sameDayDropOffCmsWidgetName = "Sameday";
const date = new Date().toISOString().split("T")[0];
const timeSlotId = "testRouteCodeId";
const startTime = "8:00 AM";
const endTime = "8:30 AM";
const jobMaxMinutes = 100;
const { wrapper } = setupMocks({
customMountOptions: {
propsData: {
estimatedServiceMinutesMaximum: jobMaxMinutes,
dateAndTimeSlotData: {
date: date,
timeSlots: [
{
id: timeSlotId,
startTime: startTime,
endTime: endTime,
},
],
},
modelValue: { routeCode: "Test" + RouteCodeFlags.ALL_DAY_DROP_OFF },
appointmentType: "Dropoff",
sameDayDropOffCmsWidgetName: sameDayDropOffCmsWidgetName,
},
},
});
//Assert
expect(wrapper.vm.supplementalInformationBlock).toEqual(
mockCmsContent[sameDayDropOffCmsWidgetName]["BodyText"]
);
});
});
describe("time-slot-modal-list-button-question.vue Disclaimer", () => {
test("The correct disclaimer information should be obtained from CMS - Dropoff - Sameday ", async () => {
//Arrange
const sameDayDropOffCmsWidgetName = "Sameday";
const date = new Date().toISOString().split("T")[0];
const timeSlotId = "testRouteCodeId";
const startTime = "8:00 AM";
const endTime = "8:30 AM";
const jobMaxMinutes = 100;
const { wrapper } = setupMocks({
customMountOptions: {
propsData: {
estimatedServiceMinutesMaximum: jobMaxMinutes,
dateAndTimeSlotData: {
date: date,
timeSlots: [
{
id: timeSlotId,
startTime: startTime,
endTime: endTime,
},
],
},
modelValue: { routeCode: "Test" + RouteCodeFlags.ALL_DAY_DROP_OFF },
appointmentType: "Dropoff",
sameDayDropOffCmsWidgetName: sameDayDropOffCmsWidgetName,
},
},
});
//Assert
expect(wrapper.vm.disclaimerTextBlockCopy).toEqual(
mockCmsContent[sameDayDropOffCmsWidgetName]["FooterText"]
);
});
test("Correct disclaimer information should be retrieved, Dropoff regular", async () => {
//Arrange
const dropOffWidgetName = "Dropoff";
const { wrapper } = setupMocks({
customMountOptions: {
propsData: {
modelValue: { routeCode: "Test" + RouteCodeFlags.ALL_DAY_DROP_OFF },
appointmentType: "Dropoff",
dropoffCmsWidgetName: dropOffWidgetName,
},
},
});
//Assert
expect(wrapper.vm.disclaimerTextBlockCopy).toEqual(
mockCmsContent[dropOffWidgetName]["FooterText"]
);
});
test("Correct disclaimer information should be retrieved, Dropoff overnight", async () => {
//Arrange
const overnightDropOffCmsWidgetName = "Overnight";
const { wrapper } = setupMocks({
customMountOptions: {
propsData: {
modelValue: { routeCode: "Test" + RouteCodeFlags.OVERNIGHT_DROP_OFF },
appointmentType: "Dropoff",
overnightDropOffCmsWidgetName: overnightDropOffCmsWidgetName,
},
},
});
//Assert
expect(wrapper.vm.disclaimerTextBlockCopy).toEqual(
mockCmsContent[overnightDropOffCmsWidgetName]["FooterText"]
);
});
test("No disclaimer information should be retrieved, non-dropoff", async () => {
//Arrange
const inshopWidgetName = "Inshop";
const { wrapper } = setupMocks({
customMountOptions: {
propsData: {
modelValue: { routeCode: "test" },
appointmentType: "Inshop",
cmsWidgetName: inshopWidgetName,
},
},
});
//Assert
expect(wrapper.vm.disclaimerTextBlockCopy).toBeNull();
});
});
describe("time-slot-modal-list-button-question.vue Duration", () => {
test("The correct duration information should be displayed - Dropoff - Sameday ", async () => {
//Arrange
const sameDayDropOffCmsWidgetName = "Sameday";
const date = new Date().toISOString().split("T")[0];
const timeSlotId = "testRouteCodeId";
const startTime = "8:00 AM";
const endTime = "8:30 AM";
const jobMaxMinutes = 100;
const { wrapper } = setupMocks({
customMountOptions: {
propsData: {
estimatedServiceMinutesMaximum: jobMaxMinutes,
dateAndTimeSlotData: {
date: date,
timeSlots: [
{
id: timeSlotId,
startTime: startTime,
endTime: endTime,
},
],
},
modelValue: { routeCode: "Test" + RouteCodeFlags.ALL_DAY_DROP_OFF },
appointmentType: "Dropoff",
sameDayDropOffCmsWidgetName: sameDayDropOffCmsWidgetName,
},
},
});
//Assert
expect(wrapper.vm.durationTextBlockCopy).toEqual(
mockCmsContent[sameDayDropOffCmsWidgetName]["SubheaderText"]
);
});
test("Correct duration information should be retrieved, Dropoff overnight", async () => {
//Arrange
const overnightDropOffCmsWidgetName = "Overnight";
const { wrapper } = setupMocks({
customMountOptions: {
propsData: {
modelValue: { routeCode: "Test" + RouteCodeFlags.OVERNIGHT_DROP_OFF },
appointmentType: "Dropoff",
overnightDropOffCmsWidgetName: overnightDropOffCmsWidgetName,
},
},
});
//Assert
expect(wrapper.vm.durationTextBlockCopy).toEqual(
mockCmsContent[overnightDropOffCmsWidgetName]["SubheaderText"]
);
});
test("Correct duration information should be retrieved, Dropoff", async () => {
//Arrange
const dropoffCmsWidgetName = "Dropoff";
const { wrapper } = setupMocks({
customMountOptions: {
propsData: {
modelValue: { routeCode: "Test" + RouteCodeFlags.ALL_DAY_DROP_OFF },
appointmentType: "Dropoff",
dropoffCmsWidgetName: dropoffCmsWidgetName,
},
},
});
//Assert
expect(wrapper.vm.durationTextBlockCopy).toEqual(
mockCmsContent[dropoffCmsWidgetName]["SubheaderText"]
);
});
test("Correct duration information should be retrieved, Inshop - convert to hours", async () => {
//Arrange
const cmsWidgetName = "Inshop";
const { wrapper } = setupMocks({
customMountOptions: {
propsData: {
estimatedServiceMinutesMaximum: 180,
estimatedServiceMinutesMinimum: 120,
modelValue: { routeCode: "Test" },
appointmentType: "Inshop",
cmsWidgetName: cmsWidgetName,
},
},
});
//Assert
expect(wrapper.vm.durationTextBlockCopy).toEqual(
mockCmsContent[cmsWidgetName]["SubheaderText"] + " 2 - 3 hours"
);
});
test("Correct duration information should be retrieved, Inshop - convert to minutes", async () => {
//Arrange
const cmsWidgetName = "Inshop";
const { wrapper } = setupMocks({
customMountOptions: {
propsData: {
estimatedServiceMinutesMaximum: 90,
estimatedServiceMinutesMinimum: 60,
modelValue: { routeCode: "Test" },
appointmentType: "Inshop",
cmsWidgetName: cmsWidgetName,
},
},
});
//Assert
expect(wrapper.vm.durationTextBlockCopy).toEqual(
mockCmsContent[cmsWidgetName]["SubheaderText"] + " 60 - 90 minutes"
);
});
test("Correct duration information should be retrieved, Inshop - min = maximum time", async () => {
//Arrange
const cmsWidgetName = "Inshop";
const { wrapper } = setupMocks({
customMountOptions: {
propsData: {
estimatedServiceMinutesMaximum: 240,
estimatedServiceMinutesMinimum: 240,
modelValue: { routeCode: "Test" },
appointmentType: "Inshop",
cmsWidgetName: cmsWidgetName,
},
},
});
//Assert
expect(wrapper.vm.durationTextBlockCopy).toEqual(
mockCmsContent[cmsWidgetName]["SubheaderText"] + " 4 hours"
);
});
test("No duration information should be displayed, Mobile", async () => {
//Arrange
const mobileCmsWidgetName = "Mobile";
const { wrapper } = setupMocks({
customMountOptions: {
propsData: {
estimatedServiceMinutesMaximum: 240,
estimatedServiceMinutesMinimum: 240,
modelValue: { routeCode: "Test" },
appointmentType: "Mobile",
mobileCmsWidgetName: mobileCmsWidgetName,
},
},
});
//Assert
expect(wrapper.vm.durationTextBlockCopy).toBeNull();
});
});
describe("time-slot-modal-list-button-question.vue Available Timeslots", () => {
test("Available Timeslots should be formatted correctly - Dropoff - Sameday ", async () => {
//Arrange
const sameDayDropOffCmsWidgetName = "Sameday";
const date = new Date().toISOString().split("T")[0];
const timeSlotId = "testRouteCodeId";
const startTime = "8:00 AM";
const endTime = "8:30 AM";
const jobMaxMinutes = 100;
const { wrapper } = setupMocks({
customMountOptions: {
propsData: {
estimatedServiceMinutesMaximum: jobMaxMinutes,
dateAndTimeSlotData: {
date: date,
timeSlots: [
{
id: timeSlotId,
startTime: startTime,
endTime: endTime,
},
],
},
appointmentType: "Dropoff",
sameDayDropOffCmsWidgetName: sameDayDropOffCmsWidgetName,
},
},
});
//Assert
const expectedTimeSlotObject = {
buttonLabel: mockCmsContent[sameDayDropOffCmsWidgetName]["HeaderText"],
value: timeSlotId,
};
expect(wrapper.vm.availableTimeSlots[0]).toEqual(expectedTimeSlotObject);
});
test("Available Timeslots should be formatted correctly - Dropoff - Overnight ", async () => {
//Arrange
const overnightDropOffCmsWidgetName = "Overnight";
const date = new Date().toISOString().split("T")[0];
const timeSlotId = "testRouteCodeId" + RouteCodeFlags.OVERNIGHT_DROP_OFF;
const startTime = "8:00 AM";
const endTime = "8:30 AM";
const jobMaxMinutes = 100;
const { wrapper } = setupMocks({
customMountOptions: {
propsData: {
estimatedServiceMinutesMaximum: jobMaxMinutes,
dateAndTimeSlotData: {
date: date,
timeSlots: [
{
id: timeSlotId,
startTime: startTime,
endTime: endTime,
},
],
},
appointmentType: "Dropoff",
overnightDropOffCmsWidgetName: overnightDropOffCmsWidgetName,
},
},
});
//Assert
const expectedTimeSlotObject = {
buttonLabel: mockCmsContent[overnightDropOffCmsWidgetName]["HeaderText"],
value: timeSlotId,
};
expect(wrapper.vm.availableTimeSlots[0]).toEqual(expectedTimeSlotObject);
});
test("Available Timeslots should be formatted correctly - Dropoff", async () => {
//Arrange
const dropoffCmsWidgetName = "Dropoff";
const date = "2020-01-01";
const timeSlotId = "testRouteCodeId" + RouteCodeFlags.ALL_DAY_DROP_OFF;
const startTime = "8:00 AM";
const endTime = "8:30 AM";
const jobMaxMinutes = 100;
const { wrapper } = setupMocks({
customMountOptions: {
propsData: {
estimatedServiceMinutesMaximum: jobMaxMinutes,
dateAndTimeSlotData: {
date: date,
timeSlots: [
{
id: timeSlotId,
startTime: startTime,
endTime: endTime,
},
],
},
appointmentType: "Dropoff",
dropoffCmsWidgetName: dropoffCmsWidgetName,
},
},
});
//Assert
const expectedTimeSlotObject = {
buttonLabel: mockCmsContent[dropoffCmsWidgetName]["HeaderText"],
value: timeSlotId,
};
expect(wrapper.vm.availableTimeSlots[0]).toEqual(expectedTimeSlotObject);
});
test("Available Timeslots should be formatted correctly - Mobile not premium", async () => {
//Arrange
const mobileCmsWidgetName = "MobileNotPremium";
const date = "2020-01-01";
const timeSlotId = "testRouteCodeId";
const startTime = "8:00";
const endTime = "8:30";
const jobMaxMinutes = 100;
const { wrapper } = setupMocks({
customMountOptions: {
propsData: {
estimatedServiceMinutesMaximum: jobMaxMinutes,
dateAndTimeSlotData: {
date: date,
timeSlots: [
{
id: timeSlotId,
startTime: startTime,
endTime: endTime,
},
],
},
appointmentType: "Mobile",
mobileCmsWidgetName: mobileCmsWidgetName,
},
},
});
//Assert
const expectedTimeSlotObject = {
buttonLabel: startTime + " AM - " + endTime + " AM",
value: timeSlotId,
};
expect(wrapper.vm.availableTimeSlots[0]).toEqual(expectedTimeSlotObject);
});
test("Available Timeslots should be formatted correctly - Mobile with premium", async () => {
//Arrange
const mobilePremiumCmsWidgetName = "MobilePremium";
const date = "2020-01-01";
const timeSlotId = "testRouteCodeId";
const startTime = "8:00";
const endTime = "8:30";
const jobMaxMinutes = 100;
const { wrapper } = setupMocks({
customMountOptions: {
propsData: {
estimatedServiceMinutesMaximum: jobMaxMinutes,
premiumAppointmentFee: {
partType: PREMIUM_FEE_PART_TYPE,
},
dateAndTimeSlotData: {
date: date,
timeSlots: [
{
id: timeSlotId,
startTime: startTime,
endTime: endTime,
offerPremium: true,
},
],
},
appointmentType: "Mobile",
mobilePremiumCmsWidgetName: mobilePremiumCmsWidgetName,
},
},
});
//Assert
const expectedTimeSlotObject = {
additionalButtonData: {
isPremiumAppointment: true,
},
buttonLabel: mockCmsContent[mobilePremiumCmsWidgetName]["HeaderText"],
buttonLabelSubCopy: "+$15.99",
value: timeSlotId + PREMIUM_TIME_SLOT_ID_FLAG,
};
expect(wrapper.vm.availableTimeSlots[0]).toEqual(expectedTimeSlotObject);
});
test("Available Timeslots should be formatted correctly - Inshop", async () => {
//Arrange
const cmsWidgetName = "Inshop";
const date = "2020-01-01";
const timeSlotId = "testRouteCodeId";
const startTime = "8:00";
const endTime = "8:30";
const jobMaxMinutes = 100;
const { wrapper } = setupMocks({
customMountOptions: {
propsData: {
estimatedServiceMinutesMaximum: jobMaxMinutes,
dateAndTimeSlotData: {
date: date,
timeSlots: [
{
id: timeSlotId,
startTime: startTime,
endTime: endTime,
},
],
},
appointmentType: "Inshop",
cmsWidgetName: cmsWidgetName,
},
},
});
//Assert
const expectedTimeSlotObject = { buttonLabel: startTime + " AM", value: timeSlotId };
expect(wrapper.vm.availableTimeSlots[0]).toEqual(expectedTimeSlotObject);
});
});
const mockCmsContent = {
MobileNotPremium: {
BodyText: "Mobile, not premium BodyText",
FooterText: "Mobile, not premium FooterText",
SubheaderText: "Mobile, not premium SubheaderText",
HeaderText: "Mobile, not premium HeaderText",
},
MobilePremium: {
"BodyText:": "Mobile, with premium BodyText",
FooterText: "Mobile, with premium FooterText",
SubheaderText: "Mobile, with premium SubheaderText",
HeaderText: "Mobile, with premium HeaderText",
},
Inshop: {
BodyText: "Inshop BodyText",
FooterText: "Inshop FooterText",
SubheaderText: "Inshop SubheaderText",
HeaderText: "Inshop HeaderText",
},
Dropoff: {
BodyText: "Dropoff BodyText",
FooterText: "Dropoff FooterText",
SubheaderText: "Dropoff SubheaderText",
HeaderText: "Dropoff HeaderText",
},
Overnight: {
BodyText: "Dropoff Overnight",
FooterText: "Dropoff, Overnight FooterText",
SubheaderText: "Dropoff, Overnight SubheaderText",
HeaderText: "Dropoff, Overnight HeaderText",
},
Sameday: {
BodyText: "Dropoff Sameday",
FooterText: "Dropoff, Sameday FooterText",
SubheaderText: "Dropoff, Sameday SubheaderText",
HeaderText: "Dropoff, Sameday HeaderText",
},
};
function setupMocks({ customMountOptions }) {
const mountOptions = getMountOptions({
...customMountOptions,
});
mountOptions.global.mocks["$store"] = store;
mountOptions["attachTo"] = document.body;
mountOptions.mixins = [
{
methods: {
getCmsContent: jest.fn().mockImplementation((widgetName, fieldName) => {
if (mockCmsContent[widgetName] && mockCmsContent[widgetName][fieldName])
return mockCmsContent[widgetName][fieldName];
}),
getTotalLineItemPrice: jest.fn().mockImplementation(() => 15.99),
},
},
];
const wrapper = shallowMount(timeSlotModalQuestion, mountOptions);
wrapper.vm.setCmsContent = jest.fn();
return { wrapper };
}

View file

@ -150,7 +150,7 @@ export default {
return this.modelValue?.routeCode;
},
set: function (newValue) {
// Button Question only supports Number, or String data types so we must get the full object to emit
// Button Question only supports Number, or String data types so we must convert to the full object before emitting
this.selectedValue = this.getSelectedTimeSlotObject(newValue);
},
},
@ -326,7 +326,7 @@ export default {
}
},
modal() {
return this.$refs["timeSlots"];
return this.$refs[this.modalName];
},
},
methods: {

View file

@ -17,7 +17,10 @@
@click-event="openModal" />
</div>
<div v-show="errorMessage" class="row my-1 form-test-error">
<span class="d-inline-flex small mt-0 center-error-message" role="alert">
<span
class="d-inline-flex small mt-0 center-error-message"
aria-atomic="true"
aria-live="polite">
{{ errorMessage }}
</span>
</div>

View file

@ -52,6 +52,7 @@ jest.mock("@/store", () => ({
damage: {
glassToReplace: [],
},
order: { serviceLocation: { zipCode: "11111" } },
},
}));
@ -133,6 +134,7 @@ describe("vehicle-damage.vue", () => {
getters: {
vehicle: {},
payment: { insuranceCoverage: { isVerified: false } },
order: { serviceLocation: { zipCode: "11111" } },
},
},
},
@ -175,7 +177,7 @@ describe("vehicle-damage.vue", () => {
expect(wrapper.vm.selectedGlassToReplace()).toEqual(expectedGlassToReplace);
expect(baseMixin.methods.dispatchStoreAction).toBeCalledWith(
storeActions.GET_DAMAGE_OPTIONS,
{ carId: "C00000000" }
{ carId: "C00000000", zipCode: "11111" }
);
});
@ -241,6 +243,7 @@ describe("vehicle-damage.vue", () => {
getters: {
vehicle: {},
payment: { insuranceCoverage: { isVerified: false } },
order: { serviceLocation: { zipCode: "11111" } },
},
},
},
@ -270,11 +273,10 @@ describe("vehicle-damage.vue", () => {
expect(wrapper.vm.selectedGlassToReplace()).toEqual(expectedGlassToReplace);
expect(baseMixin.methods.dispatchStoreAction).toBeCalledWith(
storeActions.GET_DAMAGE_OPTIONS,
{ carId: "C00000000" }
{ carId: "C00000000", zipCode: "11111" }
);
});
});
describe("alert", () => {
test("when displayVehicleChangeAlert router params is true, the alert: 'vehicleChangeAlert' should be rendered", () => {
// Arrange & Act
@ -321,7 +323,6 @@ describe("vehicle-damage.vue", () => {
expect(wrapper.findComponent({ ref: "vehicleChangeAlert" }).exists()).toBe(false);
});
});
describe("glass selections and corresponding variables", () => {
test("isWindshieldDamageLocation is true when windshield is selected", async () => {
//Arrange
@ -453,7 +454,6 @@ describe("vehicle-damage.vue", () => {
expect(wrapper.vm.isPassengerSideReplace).toEqual(true);
});
});
describe("state validations", () => {
test("CarId set, arePagePrerequisitesValid should be true ", async () => {
//Arrange
@ -474,7 +474,6 @@ describe("vehicle-damage.vue", () => {
expect(arePagePrerequisitesValid).toBe(true);
});
});
describe("input validations", () => {
// THE FOLLOWING TEST IS NOT NECESSARILY REQUIRED FOR COVERAGE
// BUT KEEP FOR AN EXAMPLE OF A VALIDATION TEST
@ -501,7 +500,6 @@ describe("vehicle-damage.vue", () => {
});
});
});
describe("get glass options from store", () => {
const damageLocations = [
["Windshield", [damageLocationsSelected.WINDSHIELD]],
@ -528,6 +526,7 @@ describe("vehicle-damage.vue", () => {
eventBusItem: jest.fn(),
damage: { glassToReplace: [{ glassLocation: damageLocation }] },
isRepair: true,
order: { serviceLocation: { zipCode: "11111" } },
};
var glassSelections = wrapper.vm.getDamageLocationsFromStore();
@ -605,6 +604,7 @@ describe("vehicle-damage.vue", () => {
isRepair: isRepair,
numberOfChips: 2,
},
order: { serviceLocation: { zipCode: "11111" } },
};
var windshieldSelections = wrapper.vm.getWindshieldOptionsFromStore();
@ -641,6 +641,7 @@ describe("vehicle-damage.vue", () => {
glassToReplace: [{ glassLocation: damageLocation, glassName: damageName }],
},
isRepair: true,
order: { serviceLocation: { zipCode: "11111" } },
};
var glassSelections = wrapper.vm.getDriverSideReplaceOptionsFromStore();
@ -677,6 +678,7 @@ describe("vehicle-damage.vue", () => {
glassToReplace: [{ glassLocation: damageLocation, glassName: damageName }],
},
isRepair: true,
order: { serviceLocation: { zipCode: "11111" } },
};
var glassSelections = wrapper.vm.getPassengerSideReplaceOptionsFromStore();
@ -711,6 +713,7 @@ describe("vehicle-damage.vue", () => {
glassToReplace: [{ glassLocation: damageLocation, glassName: damageName }],
},
isRepair: true,
order: { serviceLocation: { zipCode: "11111" } },
};
var glassSelections = wrapper.vm.getRearReplaceOptionsFromStore();
@ -720,7 +723,6 @@ describe("vehicle-damage.vue", () => {
}
);
});
describe("hide back button", () => {
test("if claim registration is delayed => should hide back button", () => {
// Arrange
@ -744,6 +746,7 @@ describe("vehicle-damage.vue", () => {
getters: {
vehicle: {},
payment: { insuranceCoverage: { isVerified: true } },
order: { serviceLocation: { zipCode: "11111" } },
},
},
},
@ -784,6 +787,7 @@ function setupMocks({ pageHeaderWidgetHeaderText, mountOptionsMockData, funnelCo
isVerified: false,
},
},
order: { serviceLocation: { zipCode: "11111" } },
},
},
};

View file

@ -81,7 +81,7 @@ import { getFunnelCookie } from "@/helpers/heritage-integration/cookie-helper";
import store from "@/store";
import baseMixin from "@/mixins/base-mixin";
import { queryStrings } from "@/constants/query-strings";
// DEFINE VALIDATION RULES
defineRule("replace-options-required", required(errorMessages.REPLACE_OPTIONS_REQUIRED));
@ -90,9 +90,20 @@ export default {
async beforeRouteEnter(to, from, next) {
// Call APIs
const cmsContentPromise = fetchCmsContentForPage(to.query.fmgPage);
const queryString = window.location.search;
const urlParams = new URLSearchParams(queryString);
const lowerCaseParams = new URLSearchParams();
for (const [name, value] of urlParams) {
lowerCaseParams.append(name.toLowerCase(), value);
}
const zip = lowerCaseParams.get(queryStrings.ZIP_CODE)
? lowerCaseParams.get(queryStrings.ZIP_CODE)
: store.getters.order.serviceLocation.zipCode;
const damageOptionsPromise = baseMixin.methods.dispatchStoreAction(
storeActions.GET_DAMAGE_OPTIONS,
{ carId: store.getters.vehicle.carId }
{ carId: store.getters.vehicle.carId, zipCode: zip }
);
// Settle promises and get results
@ -135,6 +146,7 @@ export default {
},
selectedWindshieldOptions: this.getWindshieldOptionsFromStore(),
selectedRearReplaceOptions: this.getRearReplaceOptionsFromStore(),
serviceZipCode: this.getZipFromStore() ?? this.$route.query.zipcode,
};
},
mounted() {
@ -157,6 +169,18 @@ export default {
true
);
}
if (this.serviceZipCode) {
this.pushEventToGA(
this.GaCategories.FUNNEL_ENTRY,
this.GaActions.ZIP_CODE_PROVIDED,
this.serviceZipCode,
true
);
}
},
getZipFromStore() {
return this.$store.getters.order.serviceLocation.zipCode;
},
backButtonAction() {

View file

@ -1,9 +1,5 @@
<template>
<dropdownQuestion
:options="values"
disableAutoFill
v-model="selectedValue"
:isDisabled="!values.length" />
<dropdownQuestion disableAutoFill v-model="selectedValue" :valueAsKey="true" />
</template>
<script>
@ -12,16 +8,8 @@ import dropdownQuestion from "@/digital-components/dropdown-question/dropdown-qu
export default {
name: "vehicle-question",
data() {
return {
values: [],
selectedIndex: null,
};
},
props: {
modelValue: String,
updateValues: Function,
},
components: {
@ -30,33 +18,13 @@ export default {
computed: {
selectedValue: {
get() {
return this.selectedIndex?.toString();
get: function () {
return this.modelValue?.toString();
},
set(newValue) {
this.selectedIndex = newValue;
newValue = newValue != null && newValue > -1 ? this.values[newValue] : null;
set: function (newValue) {
this.$emit("update:modelValue", newValue);
},
},
},
methods: {
async getNewValues(selectedYMMS) {
const results = await this.updateValues();
this.values = results?.data;
if (this.values.length == 1) {
this.selectedValue = 0;
} else {
this.selectedValue =
selectedYMMS != null ? this.values.indexOf(selectedYMMS) : null;
}
},
clearValues() {
this.values = [];
this.selectedValue = null;
},
},
};
</script>

View file

@ -15,8 +15,9 @@
ref="vehicleYearQuestion"
class="mb-2 mt-4"
v-model="selectedYear"
:isDisabled="!yearOptions.length"
:options="yearOptions"
cmsWidgetName="VehicleYearQuestion"
:updateValues="updateYearValues"
validationRules="year-required"
placeHolderText="Select year"
inputId="yearQuestionField" />
@ -25,8 +26,9 @@
ref="vehicleMakeQuestion"
class="mb-2 mt-4"
v-model="selectedMake"
:isDisabled="!makeOptions.length"
:options="makeOptions"
cmsWidgetName="VehicleMakeQuestion"
:updateValues="updateMakeValues"
validationRules="make-required"
placeHolderText="Select make"
inputId="makeQuestionField" />
@ -36,7 +38,8 @@
class="mb-2 mt-4"
v-model="selectedModel"
cmsWidgetName="VehicleModelQuestion"
:updateValues="updateModelValues"
:isDisabled="!modelOptions.length"
:options="modelOptions"
validationRules="model-required"
placeHolderText="Select model"
inputId="modelQuestionField" />
@ -46,7 +49,8 @@
class="mb-2 mt-4"
v-model="selectedStyle"
cmsWidgetName="VehicleStyleQuestion"
:updateValues="updateStyleValues"
:isDisabled="!styleOptions.length"
:options="styleOptions"
validationRules="style-required"
placeHolderText="Select style"
inputId="styleQuestionField" />
@ -101,10 +105,14 @@ export default {
name: "vehicle",
data() {
return {
selectedYear: null,
selectedMake: null,
selectedModel: null,
selectedStyle: null,
selectedYear: this.selectedYearfromStore(),
selectedMake: this.selectedMakefromStore(),
selectedModel: this.selectedModelfromStore(),
selectedStyle: this.selectedStylefromStore(),
yearOptions: [],
makeOptions: [],
modelOptions: [],
styleOptions: [],
};
},
@ -113,10 +121,6 @@ export default {
validationRules: String,
},
mounted() {
this.$refs["vehicleYearQuestion"].getNewValues(this.selectedYearfromStore);
},
async beforeRouteEnter(to, from, next) {
// Call APIs
@ -124,6 +128,43 @@ export default {
const experimentForLogging = store.getters.applicationUser.experiments.find(
(e) => e.universeName === experimentUniverses.CONCEPT_FUNNEL
);
const yearQuestionInitialDataPromise = baseMixin.methods.dispatchStoreAction(
storeActions.GET_VEHICLE_YEARS,
{}
);
var makeQuestionInitialDataPromise = null;
var modelQuestionInitialDataPromise = null;
var styleQuestionInitialDataPromise = null;
if (
store.getters.order.vehicle.make &&
store.getters.order.vehicle.model &&
store.getters.order.vehicle.style
) {
makeQuestionInitialDataPromise = baseMixin.methods.dispatchStoreAction(
storeActions.GET_VEHICLE_MAKES,
{
year: store.getters.order.vehicle.year,
}
);
modelQuestionInitialDataPromise = baseMixin.methods.dispatchStoreAction(
storeActions.GET_VEHICLE_MODELS,
{
year: store.getters.order.vehicle.year,
make: store.getters.order.vehicle.make,
}
);
styleQuestionInitialDataPromise = baseMixin.methods.dispatchStoreAction(
storeActions.GET_VEHICLE_STYLES,
{
year: store.getters.order.vehicle.year,
make: store.getters.order.vehicle.make,
model: store.getters.order.vehicle.model,
}
);
}
// If the concept funnel experiment is found, as it should be when coming from safelite.com, then log the experiment exposure.
if (experimentForLogging !== undefined) {
@ -146,6 +187,22 @@ export default {
resultKey: "cmsContent",
promise: cmsContentPromise,
},
{
resultKey: "yearQuestionInitialData",
promise: yearQuestionInitialDataPromise,
},
{
resultKey: "makeQuestionInitialData",
promise: makeQuestionInitialDataPromise,
},
{
resultKey: "modelQuestionInitialData",
promise: modelQuestionInitialDataPromise,
},
{
resultKey: "styleQuestionInitialData",
promise: styleQuestionInitialDataPromise,
},
];
let resultMap = await settleAllPromises(promiseResultMap);
@ -153,48 +210,74 @@ export default {
// Call the "next" function to complete the transition to this page.
next((vm) => {
vm.setCmsContent(resultMap.cmsContent);
vm.initializeYearComponent(resultMap.yearQuestionInitialData);
if (
makeQuestionInitialDataPromise &&
modelQuestionInitialDataPromise &&
styleQuestionInitialDataPromise
)
vm.initializeMMSComponent(
resultMap.makeQuestionInitialData,
resultMap.modelQuestionInitialData,
resultMap.styleQuestionInitialData
);
});
},
watch: {
selectedYear(year) {
const parsedYear = parseInt(year);
this.dispatchStoreAction(storeActions.SAVE_VEHICLE_YEAR, parsedYear);
async selectedYear(year) {
if (year) {
this.$refs["vehicleMakeQuestion"].getNewValues(this.selectedMakefromStore);
const result = await this.getMakeOptions(year);
this.makeOptions = result?.data;
if (this.selectedYearfromStore() !== year) {
this.selectedMake = null;
this.selectedModel = null;
this.selectedStyle = null;
}
} else {
this.$refs["vehicleMakeQuestion"].clearValues();
this.makeOptions = [];
}
},
selectedMake(make) {
this.dispatchStoreAction(storeActions.SAVE_VEHICLE_MAKE, make, false);
async selectedMake(make) {
if (make) {
this.$refs["vehicleModelQuestion"].getNewValues(this.selectedModelfromStore);
const result = await this.getModelOptions(this.selectedYear, make);
this.modelOptions = result?.data;
if (this.selectedMakefromStore() !== make) {
this.selectedModel = null;
this.selectedStyle = null;
}
} else {
this.$refs["vehicleModelQuestion"].clearValues();
this.modelOptions = [];
}
},
selectedModel(model) {
this.dispatchStoreAction(storeActions.SAVE_VEHICLE_MODEL, model, false);
async selectedModel(model) {
if (model) {
this.$refs["vehicleStyleQuestion"].getNewValues(this.selectedStylefromStore);
const result = await this.getStyleOptions(
this.selectedYear,
this.selectedMake,
model
);
this.styleOptions = result?.data;
if (this.selectedModelfromStore() !== model) {
this.selectedStyle = null;
}
} else {
this.$refs["vehicleStyleQuestion"].clearValues();
this.styleOptions = [];
}
},
selectedStyle(style) {
this.dispatchStoreAction(storeActions.SAVE_VEHICLE_STYLE, style, false);
this.setVehicle();
this.setVehicle(this.selectedYear, this.selectedMake, this.selectedModel, style);
},
},
methods: {
setVehicle() {
setVehicle(year, make, model, style) {
return this.dispatchStoreAction(this.storeActions.SET_VEHICLE, {
year: this.$store.getters.vehicle.year,
make: this.$store.getters.vehicle.make,
model: this.$store.getters.vehicle.model,
style: this.$store.getters.vehicle.style,
year: year,
make: make,
model: model,
style: style,
});
},
@ -207,42 +290,29 @@ export default {
},
navigateForward() {
this.dispatchStoreAction(
storeActions.SAVE_VEHICLE,
{
year: this.selectedYear,
make: this.selectedMake,
model: this.selectedModel,
style: this.selectedStyle,
},
false
);
this.$router.navigateWithSaving(this.navigationScenarios.CLICKED_FORWARD, this.$route);
},
async updateYearValues() {
return await baseMixin.methods.dispatchStoreAction(storeActions.GET_VEHICLE_YEARS, {});
initializeYearComponent(initialData) {
this.yearOptions = initialData;
},
async updateMakeValues() {
return await baseMixin.methods.dispatchStoreAction(storeActions.GET_VEHICLE_MAKES, {
year: store.getters.vehicle.year,
});
},
async updateModelValues() {
return await baseMixin.methods.dispatchStoreAction(storeActions.GET_VEHICLE_MODELS, {
year: store.getters.vehicle.year,
make: store.getters.vehicle.make,
});
},
async updateStyleValues() {
return baseMixin.methods.dispatchStoreAction(storeActions.GET_VEHICLE_STYLES, {
year: store.getters.vehicle.year,
make: store.getters.vehicle.make,
model: store.getters.vehicle.model,
});
},
},
computed: {
displayGeneric() {
return !this.selectedStyle;
initializeMMSComponent(makeOptions, modelOptions, styleOptions) {
this.makeOptions = makeOptions;
this.modelOptions = modelOptions;
this.styleOptions = styleOptions;
},
selectedYearfromStore() {
return store.getters.vehicle.year;
return store.getters.vehicle.year?.toString();
},
selectedMakefromStore() {
return store.getters.vehicle.make;
@ -253,6 +323,32 @@ export default {
selectedStylefromStore() {
return store.getters.vehicle.style;
},
async getMakeOptions(year) {
return await baseMixin.methods.dispatchStoreAction(storeActions.GET_VEHICLE_MAKES, {
year: year,
});
},
async getModelOptions(year, make) {
return await baseMixin.methods.dispatchStoreAction(storeActions.GET_VEHICLE_MODELS, {
year: year,
make: make,
});
},
async getStyleOptions(year, make, model) {
return await baseMixin.methods.dispatchStoreAction(storeActions.GET_VEHICLE_STYLES, {
year: year,
make: make,
model: model,
});
},
},
computed: {
displayGeneric() {
return !this.selectedStyle;
},
},
components: {

View file

@ -15,7 +15,9 @@
v-model="vin"
customInputId="vin"
isRequired
validationRules="vin-required|vin-format"
:validationRules="
!vinPopulatedOnPageLoad ? 'vin-required|vin-format' : ''
"
:isDisabled="vinPopulatedOnPageLoad"
maxLength="17"
:mask="vinMask"
@ -348,37 +350,22 @@ export default {
// If a VIN has already been found. Validate the Service Zip (in case of changes)
const zipCodeData = await this.getZipCodeData(this.serviceZipCode);
// Check if Service Zip entered is serviceable then save the ZIP info and email address
// Check if Service Zip entered is serviceable then save the ZIP info
if (zipCodeData.isServiceable) {
//Only save the service location if the zip changed or we lack zipCodeCtu
//Only save the zipCode, state, and zipCodeCtu if the zip changed or we lack zipCodeCtu
if (
this.$store.getters.order.serviceLocation.zipCode != this.serviceZipCode ||
!this.$store.getters.order.serviceLocation.zipCodeCtu
) {
const vehicleRegistrationInfo = this.$store.getters.vehicle.registration;
if (vehicleRegistrationInfo.zipCode == this.serviceZipCode) {
await this.dispatchStoreAction(
storeActions.SAVE_SERVICE_LOCATION,
{
address: vehicleRegistrationInfo.address,
city: vehicleRegistrationInfo.city,
state: vehicleRegistrationInfo.state,
zipCode: vehicleRegistrationInfo.zipCode,
zipCodeCtu: zipCodeData.zipCodeCtu,
},
false
);
} else {
await this.dispatchStoreAction(
storeActions.SAVE_SERVICE_ZIP_CODE_INFO,
{
state: zipCodeData.state,
zipCode: this.serviceZipCode,
zipCodeCtu: zipCodeData.zipCodeCtu,
},
false
);
}
await this.dispatchStoreAction(
storeActions.SAVE_SERVICE_ZIP_CODE_INFO,
{
state: zipCodeData.state,
zipCode: this.serviceZipCode,
zipCodeCtu: zipCodeData.zipCodeCtu,
},
false
);
}
await this.dispatchStoreAction(storeActions.SAVE_EMAIL, this.emailAddress, false);

View file

@ -446,7 +446,9 @@ export default {
const payment = store.getters.payment;
if (payment.isInsurance && payment.insuranceCoverage.isVerified) {
if (store.getters.order.referralNumber?.length === 6) {
navigateToHeritageFunnel({ loadingModal: self.$refs.loadingModal });
} else if (payment.isInsurance && payment.insuranceCoverage.isVerified) {
navigateToHeritageFunnel({ loadingModal: self.$refs.loadingModal });
} else {
self.$router.navigateWithSaving(

View file

@ -50,6 +50,7 @@ const navigationScenarios = {
CLICKED_VEHICLE_EDIT: "CLICKED_VEHICLE_EDIT",
CLICKED_DAMAGE_EDIT: "CLICKED_DAMAGE_EDIT",
CLICKED_SERVICE_PACKAGE_EDIT: "CLICKED_SERVICE_PACKAGE_EDIT",
CLICKED_SERVICE_LOCATION_EDIT: "CLICKED_SERVICE_LOCATION_EDIT",
};
export { navigationScenarios };

View file

@ -480,6 +480,10 @@ const routingTable = function (store) {
scenario: navigationScenarios.CLICKED_BACK,
destinationFmgPageValue: fmgPageValues.CUSTOMER_DETAILS,
},
{
scenario: navigationScenarios.CLICKED_SERVICE_LOCATION_EDIT,
destinationFmgPageValue: fmgPageValues.SERVICE_LOCATION,
},
],
},
];

View file

@ -535,6 +535,9 @@ export const getters = {
},
eventBus: (state) => state.applicationUser.eventBus,
damage: (state) => state.order.damage,
hasExactlyOneChip: (state) => {
return state.order.damage?.numberOfChips === 1;
},
hasAnyNonWindshieldGlassParts: (state) => {
const nonWindshieldItems = state.order.damage.glassToReplace?.filter(
(glassToReplace) => glassToReplace.glassLocation != "Windshield"
@ -766,11 +769,12 @@ export const actions = {
});
},
getDamageOptions(context, { carId }) {
getDamageOptions(context, { carId, zipCode }) {
return globalMethods.callHttpClient({
methods: endpoints.GetDamageOptions.method,
endpoint: `${endpoints.GetDamageOptions.url}/${carId}`,
payload: {},
additionalSuccessEventDataHandler: (response) => "QueryStringZip: " + zipCode,
});
},
@ -1622,6 +1626,25 @@ export const actions = {
}
},
saveVehicle(context, { year, make, model, style }) {
context.dispatch(storeActions.RESET_DAMAGE_STATE_AND_DEPENDENCIES);
context.dispatch(storeActions.RESET_REGISTRATION_STATE_AND_DEPENDENCIES);
context.dispatch(storeActions.RESET_PARTS_STATE_AND_DEPENDENCIES);
if (context.state.order.vehicle.year !== year) {
context.commit(storeMutations.UPDATE_YEAR, year);
}
if (context.state.order.vehicle.make !== make) {
context.commit(storeMutations.UPDATE_MAKE, make);
}
if (context.state.order.vehicle.model !== model) {
context.commit(storeMutations.UPDATE_MODEL, model);
}
if (context.state.order.vehicle.style !== style) {
context.commit(storeMutations.UPDATE_STYLE, style);
}
},
saveVehicleDamage(
context,
{ isWindshieldRepair, selectedGlassToReplace, selectedWindshieldChipCount }

View file

@ -1702,6 +1702,59 @@ describe("Actions", () => {
expect(commit).toBeCalledWith(storeMutations.UPDATE_VEHICLE_IMAGE_COLOR, null);
});
it("saveVehicle, should save vehicle info", () => {
// Arrange
const context = state;
context.state = {
order: {
vehicle: {
year: "2016",
make: "Toyota",
model: "Accord",
style: "SUV",
},
},
};
const commit = jest.fn();
const dispatch = jest.fn();
context.commit = commit;
context.dispatch = dispatch;
//Act
const payload = {
year: "2015",
make: "Honda",
model: "Civic",
style: "Sedan",
};
actions.saveVehicle(context, payload);
//Assert
expect(dispatch).toHaveBeenNthCalledWith(
1,
storeActions.RESET_DAMAGE_STATE_AND_DEPENDENCIES
);
expect(dispatch).toHaveBeenNthCalledWith(
2,
storeActions.RESET_REGISTRATION_STATE_AND_DEPENDENCIES
);
if (context.state.order.vehicle.year !== payload.year) {
expect(commit).toBeCalledWith(storeMutations.UPDATE_YEAR, payload.year);
}
if (context.state.order.vehicle.make !== payload.make) {
expect(commit).toBeCalledWith(storeMutations.UPDATE_MAKE, payload.make);
}
if (context.state.order.vehicle.model !== payload.model) {
expect(commit).toBeCalledWith(storeMutations.UPDATE_MODEL, payload.model);
}
if (context.state.order.vehicle.style !== payload.style) {
expect(commit).toBeCalledWith(storeMutations.UPDATE_STYLE, payload.style);
}
});
it("saveVehicleDamage, should wipe out damage if different", () => {
// Arrange
const context = state;