Merge branch 'feature/CSR-1088' into feature/CSR-1012

This commit is contained in:
Matt Caimi 2023-02-24 09:17:18 -05:00
commit 823e45f054
10 changed files with 524 additions and 53 deletions

12
package-lock.json generated
View file

@ -19150,9 +19150,9 @@
}
},
"node_modules/qs": {
"version": "6.5.2",
"resolved": "https://registry.npmjs.org/qs/-/qs-6.5.2.tgz",
"integrity": "sha512-N5ZAX4/LxJmF+7wN74pUD6qAh9/wnvdQcjq9TZjevvXzSUo7bfmw91saqMjzGS2xq91/odN2dW/WOl7qQHNDGA==",
"version": "6.5.3",
"resolved": "https://registry.npmjs.org/qs/-/qs-6.5.3.tgz",
"integrity": "sha512-qxXIEh4pCGfHICj1mAJQ2/2XVZkjCDTcEgfoSQxc/fYivUZxTkk7L3bDBJSoNrEzXI17oUO5Dp07ktqE5KzczA==",
"dev": true,
"engines": {
"node": ">=0.6"
@ -39784,9 +39784,9 @@
"dev": true
},
"qs": {
"version": "6.5.2",
"resolved": "https://registry.npmjs.org/qs/-/qs-6.5.2.tgz",
"integrity": "sha512-N5ZAX4/LxJmF+7wN74pUD6qAh9/wnvdQcjq9TZjevvXzSUo7bfmw91saqMjzGS2xq91/odN2dW/WOl7qQHNDGA==",
"version": "6.5.3",
"resolved": "https://registry.npmjs.org/qs/-/qs-6.5.3.tgz",
"integrity": "sha512-qxXIEh4pCGfHICj1mAJQ2/2XVZkjCDTcEgfoSQxc/fYivUZxTkk7L3bDBJSoNrEzXI17oUO5Dp07ktqE5KzczA==",
"dev": true
},
"query-string": {

View file

@ -32,7 +32,7 @@ const storeActions = {
LOAD_SESSION: "loadSession",
UPDATE_STORE_WITH_SAVE_SESSION_RESPONSE: "updateStoreWithSaveSessionResponse",
VALIDATE_ZIP: "validateZip",
PRICE_ORDER_ITEMS: "priceOrderItems",
PRICE_ORDER_ITEMS_AND_SAVE_SERVER_DATA: "priceOrderItemsAndSaveServerData",
LOG_EXPERIMENT_EXPOSURE: "logExperimentExposure",
LOG_PAGE_VIEW: "logPageView",
LOG_CUSTOM_EVENT: "logCustomEvent",

View file

@ -1,23 +1,20 @@
import { shallowMount, flushPromises } 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 quote from "@/layouts/quote/quote.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 our module for promises.
jest.mock("@/helpers/layout-helper.js", () => ({
settleAllPromises: jest.fn(),
}));
// Mock fetchCmsContentForPage
jest.mock("@/helpers/cms-content-helper", () => ({
fetchCmsContentForPage: jest.fn(),
fetchCmsContentForPage: () => Promise.resolve("content"),
}));
jest.mock("@/helpers/heritage-integration/navigation-helper", () => ({
@ -32,6 +29,38 @@ jest.mock(
{ virtual: true }
);
// START beforeRouteEnter mock arranging //
jest.mock("@/mixins/base-mixin", () => ({
methods: {
dispatchStoreAction(action, items, encode) {
if (action === mockPriceOrderStoreAction) return items;
else return mockStoreActionResults[action];
},
getTierOnePackagePrice() {
return mockTierOnePrice;
},
filterOutFees() {
return null;
},
},
}));
let mockTierOnePrice = 501;
const mockPriceOrderStoreAction = storeActions.PRICE_ORDER_ITEMS_AND_SAVE_SERVER_DATA;
const mockStoreActionResults = {};
mockStoreActionResults[storeActions.GET_WIPERS] = Promise.resolve([
{ partNumber: "SBB24", partType: "FRONT WIPER" },
]);
mockStoreActionResults[storeActions.GET_RAIN_DEFENSE] = Promise.resolve({
partNumber: "RAIN DEFENSE",
partType: "RAIN DEFENSE",
});
mockStoreActionResults[storeActions.GET_SUPPORTING_ITEMS] = Promise.resolve([
{ partNumber: "WSREPAIR", partType: "WSREPAIR" },
]);
// END beforeRouteEnter mock arranging
store.getters = {
order: {
accountNumber: applicationConfig.CASH_ACCOUNT_NUMBER,
@ -117,6 +146,269 @@ describe("quote.vue", () => {
//Assert
expect(navigateToHeritage.navigateToHeritageFunnel).toHaveBeenCalled();
});
test("should pass arePagePrerequisitesValid with a repair order", () => {
//Arrange
const { wrapper } = setupMocks({});
store.getters = {
order: {
serviceLocation: {
zipCode: "12345",
zipCodeCtu: "value",
},
damage: {
isRepair: true,
},
referralNumber: "1234567",
},
};
let arePagePrerequisitesValid = wrapper.vm.arePagePrerequisitesValid();
expect(arePagePrerequisitesValid).toBe(true);
});
test("should pass arePagePrerequisitesValid with a replace order", () => {
//Arrange
const { wrapper } = setupMocks({});
store.getters = {
order: {
serviceLocation: {
zipCode: "12345",
zipCodeCtu: "value",
},
damage: {
isRepair: false,
},
referralNumber: "1234567",
lineItems: {
glassParts: ["item", "item2"],
},
},
};
let arePagePrerequisitesValid = wrapper.vm.arePagePrerequisitesValid();
expect(arePagePrerequisitesValid).toBe(true);
});
test("should fail arePagePrerequisitesValid without glass parts or flagged as repair", () => {
//Arrange
const { wrapper } = setupMocks({});
store.getters = {
order: {
serviceLocation: {
zipCode: "12345",
zipCodeCtu: "value",
},
damage: {
isRepair: false,
},
referralNumber: "1234567",
},
};
let arePagePrerequisitesValid = wrapper.vm.arePagePrerequisitesValid();
expect(arePagePrerequisitesValid).toBe(false);
});
test("should have non-null values for necessary data members after 'beforeRouteEnter'", async () => {
//Arrange
const { wrapper } = setupMocks({});
store.getters = {
order: {
lineItems: {
glassParts: ["item", "item2"],
},
},
};
//mock this to avoid needing to populate this.$route in an unrelated test
wrapper.vm.getDefaultIsInsuranceSelectedValue = jest.fn();
//Act
await quote.beforeRouteEnter.call(
wrapper.vm,
{ query: { fmgPage: "quote" } },
undefined,
(c) => c(wrapper.vm)
);
//Assert
expect(wrapper.vm.pricedGlassParts !== null).toBe(true);
expect(wrapper.vm.supportingItems !== null).toBe(true);
expect(wrapper.vm.availableLineItems !== null).toBe(true);
// This should have its own test
//expect(vm.isInsuranceSelected !== null).toBe(true);
});
test("should default to insurance if query param 'isInsurance' is true", async () => {
//Arrange
const { wrapper } = setupMocks({});
store.getters = {
order: {
lineItems: {
glassParts: ["item", "item2"],
},
payment: {
isInsurance: null,
},
},
};
wrapper.vm.$route = { query: { isInsurance: "true" } };
//Act
await quote.beforeRouteEnter.call(
wrapper.vm,
{ query: { fmgPage: "quote" } },
undefined,
(c) => c(wrapper.vm)
);
//Assert
expect(wrapper.vm.isInsuranceSelected).toBe(true);
});
test("should default to cash if query param 'isInsurance' is false", async () => {
//Arrange
const { wrapper } = setupMocks({});
store.getters = {
order: {
lineItems: {
glassParts: ["item", "item2"],
},
payment: {
isInsurance: null,
},
},
};
wrapper.vm.$route = { query: { isInsurance: "false" } };
//Act
await quote.beforeRouteEnter.call(
wrapper.vm,
{ query: { fmgPage: "quote" } },
undefined,
(c) => c(wrapper.vm)
);
//Assert
expect(wrapper.vm.isInsuranceSelected).toBe(false);
});
test("should default to insurance if insurance selection is saved to store", async () => {
//Arrange
const { wrapper } = setupMocks({});
store.getters = {
order: {
lineItems: {
glassParts: ["item", "item2"],
},
payment: {
isInsurance: true,
},
},
};
// Ensure that query param isn't overriding selection
wrapper.vm.$route = { query: null };
//Act
await quote.beforeRouteEnter.call(
wrapper.vm,
{ query: { fmgPage: "quote" } },
undefined,
(c) => c(wrapper.vm)
);
//Assert
expect(wrapper.vm.isInsuranceSelected).toBe(true);
});
test("should default to cash if cash selection is saved to store", async () => {
//Arrange
const { wrapper } = setupMocks({});
store.getters = {
order: {
lineItems: {
glassParts: ["item", "item2"],
},
payment: {
isInsurance: false,
},
},
};
// Ensure that query param isn't overriding selection
wrapper.vm.$route = { query: null };
//Act
await quote.beforeRouteEnter.call(
wrapper.vm,
{ query: { fmgPage: "quote" } },
undefined,
(c) => c(wrapper.vm)
);
//Assert
expect(wrapper.vm.isInsuranceSelected).toBe(false);
});
test("should default to cash if total economy package price is under $500", async () => {
// Also needs no query parameter or previous selection in store to be present
//Arrange
const { wrapper } = setupMocks({});
store.getters = {
order: {
lineItems: {
glassParts: ["item", "item2"],
},
payment: {
isInsurance: null, // Ensure that previous selection isn't overriding selection
},
},
};
mockTierOnePrice = 200;
// Ensure that query param isn't overriding selection
wrapper.vm.$route = { query: null };
//Act
await quote.beforeRouteEnter.call(
wrapper.vm,
{ query: { fmgPage: "quote" } },
undefined,
(c) => c(wrapper.vm)
);
//Assert
expect(wrapper.vm.isInsuranceSelected).toBe(false);
});
test("should default to insurance if total economy package price is over $500", async () => {
// Also needs no query parameter or previous selection in store to be present
//Arrange
const { wrapper } = setupMocks({});
store.getters = {
order: {
lineItems: {
glassParts: ["item", "item2"],
},
payment: {
isInsurance: null, // Ensure that previous selection isn't overriding selection
},
},
};
mockTierOnePrice = 505;
// Ensure that query param isn't overriding selection
wrapper.vm.$route = { query: null };
//Act
await quote.beforeRouteEnter.call(
wrapper.vm,
{ query: { fmgPage: "quote" } },
undefined,
(c) => c(wrapper.vm)
);
//Assert
expect(wrapper.vm.isInsuranceSelected).toBe(true);
});
});
function setupMocks({ customMountOptions }) {
@ -128,5 +420,6 @@ function setupMocks({ customMountOptions }) {
mountOptions["attachTo"] = document.body;
const wrapper = shallowMount(quote, mountOptions);
wrapper.vm.setCmsContent = jest.fn();
return { wrapper };
}

View file

@ -114,7 +114,7 @@ export default {
];
const pricingResults = await baseMixin.methods.dispatchStoreAction(
storeActions.PRICE_ORDER_ITEMS,
storeActions.PRICE_ORDER_ITEMS_AND_SAVE_SERVER_DATA,
availableLineItems,
false
);

View file

@ -4,7 +4,6 @@
for="mobileLocationLinkPromptId"
:aria-label="mobileLocationLinkPromptText"
class="form-label fw-bold w-100 ps-4 pe-4 pt-4"
:class="[questionAlignment === 'center' ? 'text-center w-100 mb-5' : '']"
v-html="mobileLocationLinkPromptText"></label>
<div class="update-mobile-location-text-link">
<textLink
@ -63,7 +62,8 @@ export default {
return {
internalModel: this.copyModel(this.modelValue),
displayInvalidZipAlert: false,
mobileFee: "",
modalName: "MobileLocationModalWidget",
mobileFee: "",
};
},
props: {
@ -78,26 +78,22 @@ export default {
zipCode: "",
},
isVehicleProtected: null,
}),
serviceZipCode: ""
}),
},
isZipServiceableMobile: Boolean,
isZipServiceableInShop: Boolean,
serviceZipCode: String,
isZipServiceableInShop: Boolean,
linkWidgetName: String,
modalWidgetName: String,
mobileFeeDisclaimerWidgetName: String,
workspaceRequirementsWidgetName: String,
alertNonServiceableZipWidgetName: String,
alertInvalidZipWidgetName: String,
},
async mounted() {
this.mobileFee = await this.getMobileFee();
mounted() {
this.getMobileFee().then((result) => {
this.mobileFee = result;
});
},
computed: {
mobileFeeText() {
const cmsContentText = this.getCmsContent("MobileFeeDisclaimerWidget", "Text");
return cmsContentText.replaceAll("{custom:mobileFee}", this.mobileFee);
},
mobileLocationLinkPromptText() {
return this.getCmsContent(this.linkWidgetName, "HeaderText");
},
@ -119,12 +115,13 @@ export default {
modalHeaderText() {
return this.getCmsContent(this.modalWidgetName, "HeaderText");
},
mobileFeeText() {
const cmsContentText = this.getCmsContent("MobileFeeDisclaimerWidget", "Text");
return cmsContentText.replaceAll("{custom:mobileFee}", this.mobileFee);
},
modalFooterText() {
return this.getCmsContent(this.modalWidgetName, "FooterText");
},
modalName() {
return this.modalWidgetName;
},
addressModel: {
get: function () {
return this.modelValue.addressQuestions;
@ -143,12 +140,25 @@ export default {
zipCode: modelToCopy.addressQuestions.zipCode,
},
isVehicleProtected: modelToCopy.isVehicleProtected,
isZipServiceableMobile: modelToCopy.isZipServiceableMobile,
isZipServiceableInShop: modelToCopy.isZipServiceableInShop,
serviceZipCode: modelToCopy.serviceZipCode
};
},
setServiceZipCode(serviceZipCode) {
this.internalModel.serviceZipCode = serviceZipCode;
},
getServiceZipCodeFromStore() {
return this.$store.getters.order.serviceLocation.zipCode;
},
async getMobileFee() {
if (!this.internalModel.serviceZipCode) {
return "";
}
const zipCodeData = await this.getZipCodeData(this.internalModel.serviceZipCode);
// Get the Mobile Fee Part
const mobileFeePart = await baseMixin.methods.dispatchStoreAction(
storeActions.GET_MOBILE_FEE_PART,
@ -159,7 +169,11 @@ export default {
// Get the Mobile Fee Part Price
const pricingResults = await baseMixin.methods.dispatchStoreAction(
storeActions.PRICE_ORDER_ITEMS,
[mobileFeePart.data],
{
availableLineItems: [mobileFeePart.data],
serviceZipCode: zipCodeData.zipCode,
zipCodeCtu: zipCodeData.zipCodeCtu
},
false
);
@ -206,7 +220,15 @@ export default {
this.displayInvalidZipAlert = true;
} else {
// Update the page level model
this.internalModel.serviceZipCode = this.internalModel.addressQuestions.zipCode;
this.$emit("update:modelValue", this.internalModel);
// Emit the update service zip code information to the parent to update the service zipcode details
this.$emit("updated-zip-code", {
zipCode: this.internalModel.addressQuestions.zipCode,
state: zipCodeData.state,
isServiceable: zipCodeData.isServiceable
});
this.closeModal();
}
@ -216,6 +238,11 @@ export default {
modelValue(newValue) {
this.internalModel = this.copyModel(newValue);
},
"modelValue.serviceZipCode": {
async handler(newValue) {
this.mobileFee = await this.getMobileFee(newValue);
},
},
},
components: {
textLink,

View file

@ -1,3 +1,131 @@
describe("service-location.vue", () => {
test.todo("test this");
// Components
import serviceLocation from "@/layouts/service-location/service-location.vue";
// Supporting files
import { shallowMount } from "@vue/test-utils";
import { getMountOptions } from "@/helpers/unit-test-helper";
import baseMixin from "@/mixins/base-mixin";
import { nextTick } from "vue";
import { fetchCmsContentForPage } from "@/helpers/cms-content-helper";
import { settleAllPromises } from "@/helpers/layout-helper.js";
import store from "@/store";
// Define Mocks
jest.mock("@/helpers/layout-helper.js", () => ({
settleAllPromises: jest.fn(),
}));
jest.mock("@/helpers/cms-content-helper", () => ({
fetchCmsContentForPage: jest.fn(),
}));
jest.mock("@/store", () => ({
commit: jest.fn(),
dispatch: jest.fn(),
getters: {
lineItems: {
supportingItems: [
{
description: null,
kitPrice: 0,
laborAmount: 0,
partNumber: "SUPPLIES-REPAIR",
partType: "REPAIR FEE",
sellingPrice: 7.99,
},
],
},
order: {
serviceLocation: {
zipCode: "43235",
},
},
payment: {
isInsurance: false,
},
},
}));
beforeEach(() => {
store.getters = {
lineItems: {
supportingItems: [
{
description: null,
kitPrice: 0,
laborAmount: 0,
partNumber: "SUPPLIES-REPAIR",
partType: "REPAIR FEE",
sellingPrice: 7.99,
},
],
},
order: {
serviceLocation: {
zipCode: "43235",
},
},
payment: {
isInsurance: false,
},
};
});
describe("service-location.vue", () => {
describe("arePagePrerequisitesValid", () => {
test("No prerequisites set: Should return false.", () => {
const { wrapper } = setupMocks({});
store.getters = {
lineItems: {
supportingItems: null,
},
order: {
serviceLocation: {
zipCode: null,
},
},
payment: {
isInsurance: null,
},
};
let arePagePrerequisitesValid = wrapper.vm.arePagePrerequisitesValid();
expect(arePagePrerequisitesValid).toBe(false);
});
test("All prerequisites set: Should return true.", () => {
const { wrapper } = setupMocks({});
let arePagePrerequisitesValid = wrapper.vm.arePagePrerequisitesValid();
expect(arePagePrerequisitesValid).toBe(true);
});
test("Some prerequisites set: Should return false.", () => {
const { wrapper } = setupMocks({});
store.getters.order.serviceLocation.zipCode = null;
let arePagePrerequisitesValid = wrapper.vm.arePagePrerequisitesValid();
expect(arePagePrerequisitesValid).toBe(false);
});
});
});
function setupMocks({ mountOptionsMockData = {} }) {
const apiResponses = {};
const apiPromise = Promise.resolve(apiResponses);
settleAllPromises.mockImplementation(() => apiPromise);
fetchCmsContentForPage.mockImplementation(() => Promise.resolve());
const mountOptions = getMountOptions(mountOptionsMockData);
const wrapper = shallowMount(serviceLocation, mountOptions);
wrapper.vm.setCmsContent = baseMixin.methods.setCmsContent;
return { wrapper, apiPromise };
}

View file

@ -6,16 +6,15 @@
<funnelSubHeader cmsWidgetName="FunnelSubHeaderWidget" />
<serviceZipModalQuestion
v-model="serviceZipCodeQuestion"
@service-zip-updated="resetMobileLocation"
ref="serviceZipCodeQuestion"
linkWidgetName="ServiceZipLinkWidget"
modalWidgetName="ServiceZipModalWidget" />
<mobileLocationModalQuestions
v-model="mobileLocationQuestions"
@updated-zip-code="resetServiceZipCode"
ref="mobileLocationModalQuestions"
linkWidgetName="MobileLocationLinkWidget"
modalWidgetName="MobileLocationModalWidget"
mobileFeeDisclaimerWidgetName="MobileFeeDisclaimerWidget"
workspaceRequirementsWidgetName="WorkspaceRequirementsWidget" />
modalWidgetName="MobileLocationModalWidget" />
<funnel-footer
cmsWidgetName="FunnelFooterWidget"
ref="funnelFooter"
@ -60,6 +59,7 @@ export default {
zipCode: this.getRegistrationZipFromStore(),
},
isVehicleProtected: null,
serviceZipCode: this.getServiceZipCodeFromStore(),
},
};
},
@ -86,7 +86,11 @@ export default {
methods: {
arePagePrerequisitesValid() {
return true;
return (
store.getters.lineItems.supportingItems !== null &&
store.getters.order.serviceLocation.zipCode !== null &&
store.getters.payment.isInsurance !== null
);
},
getRegistrationAddressFromStore() {
return this.$store.getters.vehicle.registration.address;
@ -109,7 +113,7 @@ export default {
getIsServiceableFromStore() {
return this.$store.getters.order.serviceLocation.isServiceable;
},
resetMobileLocation() {
resetMobileLocation(updatedServiceZipCode) {
this.mobileLocationQuestions = {
addressQuestions: {
streetAddress: "",
@ -119,10 +123,16 @@ export default {
zipCode: "",
},
isVehicleProtected: null,
serviceZipCode: updatedServiceZipCode,
};
this.$refs.mobileLocationModalQuestions.resetComponent();
},
resetServiceZipCode(newValue) {
this.serviceZipCodeQuestion.state = newValue.state;
this.serviceZipCodeQuestion.zipCode = newValue.zipCode;
this.serviceZipCodeQuestion.isServiceable = newValue.isServiceable;
},
backButtonAction() {
this.$router.navigateWithoutSaving(this.navigationScenarios.CLICKED_BACK, this.$route);
},
@ -130,6 +140,15 @@ export default {
navigateToHeritageFunnel({ loadingModal: this.$refs.loadingModal });
},
},
watch: {
serviceZipCodeQuestion: {
handler(newValue, oldValue) {
if (newValue !== oldValue) {
this.resetMobileLocation(newValue.zipCode);
}
},
},
},
components: {
serviceZipModalQuestion,
mobileLocationModalQuestions,

View file

@ -139,11 +139,6 @@ export default {
this.internalModel.state = zipCodeData.state;
this.internalModel.isServiceable = zipCodeData.isServiceable;
// Notify the page that the service zip has been updated to clear Mobile Location form
if (this.internalModel.zipCode !== this.modelValue.zipCode) {
this.$emit("service-zip-updated");
}
// Update the page level model
this.$emit("update:modelValue", this.internalModel);
@ -152,14 +147,14 @@ export default {
},
},
watch: {
modelValue(newValue) {
this.internalModel = this.copyModel(newValue);
},
"internalModel.zipCode": {
handler() {
this.resetsOnZipInput();
},
},
modelValue(newValue) {
this.internalModel = this.copyModel(newValue);
},
},
components: {
textLink,

View file

@ -1423,24 +1423,34 @@ export const actions = {
context.commit(storeMutations.UPDATE_VAPS, vaps);
},
// Price order actions
async priceOrderItems(context, availableLineItems) {
async priceOrderItemsAndSaveServerData(context, { availableLineItems, serviceZipCode, ctu }) {
const zipCodeToUse = serviceZipCode
? serviceZipCode
: context.getters.order.serviceLocation.zipCode;
const ctuToUse = ctu ? ctu : context.getters.order.serviceLocation.zipCodeCtu;
const availableLineItemsFormattedForRequest =
getLineItemQueryStringForPricing(availableLineItems);
const vehicle = context.getters.order.vehicle;
let queryString =
`ParentAccountNumber=${applicationConfig.CASH_ACCOUNT_NUMBER}` +
`&CTU=${context.getters.order.serviceLocation.zipCodeCtu}` +
`&CTU=${ctuToUse}` +
`&CarId=${vehicle.carId}` +
`&Make=${vehicle.make}` +
`&Model=${vehicle.model}` +
`&Year=${vehicle.year}` +
`&EON=${context.getters.order.eon}` +
`&ZipCode=${context.getters.order.serviceLocation.zipCode}` +
`&ZipCode=${zipCodeToUse}` +
`${availableLineItemsFormattedForRequest}`;
const lineItemServerData = context.getters.order.lineItems.serverData;
if (lineItemServerData) {
queryString += `&ServerData=${encodeURIComponent(lineItemServerData)}`;
}
const response = await globalMethods.callHttpClient({
method: endpoints.PriceOrderItems.method,
endpoint: `${endpoints.PriceOrderItems.url}?${queryString}`,

View file

@ -47,7 +47,6 @@ export default {
true
);
if (!this.isDisabled) {
console.log("I'm here");
this.isLoaderDisplayed = true;
this.$emit("click-event");
}