diff --git a/src/constants/store-actions.js b/src/constants/store-actions.js index c44a6fe85..e1cb33e14 100644 --- a/src/constants/store-actions.js +++ b/src/constants/store-actions.js @@ -70,6 +70,7 @@ const storeActions = { SAVE_VIN: "saveVin", SAVE_REGISTRATION_ADDRESS_LOOKUP: "saveRegistrationAddressLookup", SAVE_GLASS_PARTS: "saveGlassParts", + SAVE_GLASS_PART_PRICES: "saveGlassPartPrices", SAVE_PART_QUESTION_ANSWERS: "savePartQuestionAnswers", RESET_MOLDING_AND_CAPABILITY_QUESTIONS_IF_NEEDED: "resetMoldingAndCapabilityQuestionAnswersIfNeeded", @@ -78,6 +79,7 @@ const storeActions = { SAVE_PAYMENT_TYPE: "savePaymentType", SAVE_PARENT_ACCOUNT_NUMBER: "saveParentAccountNumber", SAVE_SUPPORTING_ITEMS: "saveSupportingItems", + SAVE_SUPPORTING_ITEMS_AND_RESET_SERVICE_LOCATION: "saveSupportingItemsAndResetServiceLocation", SAVE_VAPS: "saveVaps", }; diff --git a/src/constants/store-mutations.js b/src/constants/store-mutations.js index 82cc2082d..939caf922 100644 --- a/src/constants/store-mutations.js +++ b/src/constants/store-mutations.js @@ -58,6 +58,9 @@ const storeMutations = { RESET_GLASS_PARTS_STATE: "resetGlassPartsState", RESET_STATE: "resetState", RESET_SAVE_SESSION_PROMISE: "resetSaveSessionPromise", + RESET_SERVICE_LOCATION_APPOINTMENT_TYPE: "resetServiceLocationAppointmentType", + RESET_SERVICE_LOCATION_PROVIDER: "resetServiceLocationProvider", + RESET_SERVICE_LOCATION_MOBILE_ADDRESS: "resetServiceLocationMobileAddress", RESET_SCHEDULE: "resetSchedule", // OTHER MUTATIONS diff --git a/src/helpers/object-helper.js b/src/helpers/object-helper.js new file mode 100644 index 000000000..b42230be1 --- /dev/null +++ b/src/helpers/object-helper.js @@ -0,0 +1,81 @@ +// For nested objects, spread operator only creates new references to the top level fields, +// the remaining nested fields actually reference the original object which can introduce problems. + +// The purpose of this method is to deep clone the data in an object recursively, this is useful +// for cloning modelValues to internal models when regular two-way binding is not an option. +// See: mobile-location-modal-questions.vue + +// Creates a deep clone of an object. Clones primitives, arrays and objects, excluding class instances. +// https://www.30secondsofcode.org/js/s/deep-clone +export function deepClone(object) { + if (object === null) { + return null; + } + + let clone = Object.assign({}, object); + Object.keys(clone).forEach( + (key) => + (clone[key] = typeof object[key] === "object" ? deepClone(object[key]) : object[key]) + ); + + if (Array.isArray(object)) { + clone.length = object.length; + return Array.from(clone); + } + + return clone; +} + +// The purpose of this method is to check for array or object equality recursively to determine if two complex objects are equal. +// This is only a comparison of data, not functions. +export function deepEqual(obj1, obj2) { + if (typeof obj1 !== typeof obj2) { + return false; + } + + if (obj1 === null || obj2 === null) { + return obj1 === obj2; + } + + if (Array.isArray(obj1) && Array.isArray(obj2)) { + if (obj1.length !== obj2.length) { + return false; + } + + const sorted1 = obj1.slice().sort(); + const sorted2 = obj2.slice().sort(); + + for (let i = 0; i < sorted1.length; i++) { + if (!deepEqual(sorted1[i], sorted2[i])) { + return false; + } + } + + return true; + } + + if (typeof obj1 === "object" && typeof obj2 === "object") { + const keys1 = Object.keys(obj1); + const keys2 = Object.keys(obj2); + + if (keys1.length !== keys2.length) { + return false; + } + + const sortedKeys1 = keys1.sort(); + const sortedKeys2 = keys2.sort(); + + for (let i = 0; i < sortedKeys1.length; i++) { + const key1 = sortedKeys1[i]; + const key2 = sortedKeys2[i]; + + if (key1 !== key2 || !deepEqual(obj1[key1], obj2[key2])) { + return false; + } + } + + return true; + } + + return obj1 === obj2; +} diff --git a/src/helpers/object-helper.spec.js b/src/helpers/object-helper.spec.js new file mode 100644 index 000000000..730dea1f8 --- /dev/null +++ b/src/helpers/object-helper.spec.js @@ -0,0 +1,275 @@ +import { deepClone, deepEqual } from "./object-helper"; + +describe("object-cloning-helper.js", () => { + describe("deepClone", () => { + it("Should return null if no object is passed in", async () => { + // Arrange + const expected = null; + + // Act + const result = deepClone(null); + + // Assert + expect(result).toEqual(expected); + }); + + it("Should return a deep copy of the object", async () => { + // Arrange + + const object = { + addressQuestions: { + streetAddress: "555 Some St", + apartmentNumberOrBusinessName: "Apt 1", + city: "Funkytown", + state: "OH", + zipCode: "55555", + }, + isVehicleProtected: true, + serviceZipCode: "55555", + }; + + const expected = { + addressQuestions: { + streetAddress: "555 Some St", + apartmentNumberOrBusinessName: "Apt 1", + city: "Funkytown", + state: "OH", + zipCode: "55555", + }, + isVehicleProtected: true, + serviceZipCode: "55555", + }; + + // Act + const result = deepClone(object); + + // Assert + expect(result).toStrictEqual(expected); + }); + + it("Should return a copy of the array", async () => { + // Arrange + + const array = [9, 8, 7, 6, 5, 4, 3, 2, 1]; + + const expected = [9, 8, 7, 6, 5, 4, 3, 2, 1]; + + // Act + const result = deepClone(array); + + // Assert + expect(result).toStrictEqual(expected); + }); + }); + + describe("deepEqual", () => { + it("Should return false if the items being compared are not the same type", async () => { + // Arrange + const obj1 = ""; // String + const obj2 = 3; // Number + const expected = false; + + // Act + const result = deepEqual(obj1, obj2); + + // Assert + expect(result).toEqual(expected); + }); + + it("Should return false if one the items being compared is null", async () => { + // Arrange + const obj1 = {}; // Object + const obj2 = null; // Array + const expected = false; + + // Act + const result = deepEqual(obj1, obj2); + + // Assert + expect(result).toEqual(expected); + }); + + describe("Both items being compared are arrays", () => { + it("Should return true if the two arrays have the same elements in the same order", async () => { + // Arrange + const obj1 = [1, 2, 3]; // Array + const obj2 = [1, 2, 3]; // Array + const expected = true; + + // Acts + const result = deepEqual(obj1, obj2); + + // Assert + expect(result).toEqual(expected); + }); + + it("Should return true if the two arrays have the same elements in a different order", async () => { + // Arrange + const obj1 = [1, 2, 3]; // Array + const obj2 = [3, 1, 2]; // Array + const expected = true; + + // Acts + const result = deepEqual(obj1, obj2); + + // Assert + expect(result).toEqual(expected); + }); + + it("Should return false if the two arrays are not the same length", async () => { + // Arrange + const obj1 = [1, 2, 3]; // Array + const obj2 = [1, 2]; // Array + const expected = false; + + // Acts + const result = deepEqual(obj1, obj2); + + // Assert + expect(result).toEqual(expected); + }); + + it("Should return false if they have the same elements but their the array elements are different", async () => { + // Arrange + const obj1 = [1, 2, 3]; // Array + const obj2 = [4, 5, 6]; // Array + const expected = false; + + // Acts + const result = deepEqual(obj1, obj2); + + // Assert + expect(result).toEqual(expected); + }); + }); + + describe("Both items being compared are objects", () => { + it("Should return false if the two objects *do not* have the same number of keys", async () => { + // Arrange + const obj1 = { + prop1: {}, + }; // Object + + const obj2 = { + prop1: {}, + prop2: {}, + }; // Object + const expected = false; + + // Acts + const result = deepEqual(obj1, obj2); + + // Assert + expect(result).toEqual(expected); + }); + + it("Should return true if the two objects have the same keys in the same order", async () => { + // Arrange + const obj1 = { + prop1: {}, + prop2: {}, + }; // Object + + const obj2 = { + prop1: {}, + prop2: {}, + }; // Object + + const expected = true; + + // Acts + const result = deepEqual(obj1, obj2); + + // Assert + expect(result).toEqual(expected); + }); + + it("Should return true if the two objects have the same keys in a different order", async () => { + // Arrange + const obj1 = { + prop1: {}, + prop2: {}, + }; // Object + + const obj2 = { + prop2: {}, + prop1: {}, + }; // Object + + const expected = true; + + // Acts + const result = deepEqual(obj1, obj2); + + // Assert + expect(result).toEqual(expected); + }); + + it("Should return false if the two objects have the same keys but in a nested object comparison one of the two values is null", async () => { + // Arrange + const obj1 = { + prop1: { + subProp1: { + foo: "", + bar: null, + }, + }, + prop2: { + subProp1: [1, 2, 3], + }, + }; // Object + + const obj2 = { + prop1: {}, + prop2: {}, + }; // Object + + const expected = false; + + // Acts + const result = deepEqual(obj1, obj2); + + // Assert + expect(result).toEqual(expected); + }); + + it("Should return false if the two objects have the same elements but a nested object comparison is of two different types", async () => { + // Arrange + const obj1 = { + prop1: { + subProp1: { + foo: "", + bar: "", + }, + }, + prop2: { + subProp1: [1, 2, 3], + }, + }; // Object + + const obj2 = { + prop1: { + subProp1: { + foo: "", + bar: "", + }, + }, + prop2: { + subProp1: { + foo1: "", + bar1: "", + }, + }, + }; // Object + + const expected = false; + + // Acts + const result = deepEqual(obj1, obj2); + + // Assert + expect(result).toEqual(expected); + }); + }); + }); +}); diff --git a/src/layouts/estimate/estimate.vue b/src/layouts/estimate/estimate.vue index 1ca0c9d48..404caff1d 100644 --- a/src/layouts/estimate/estimate.vue +++ b/src/layouts/estimate/estimate.vue @@ -272,7 +272,7 @@ export default { const resultMap = await settleAllPromises(promiseResultMap); this.dispatchStoreAction( - this.storeActions.SAVE_SUPPORTING_ITEMS, + this.storeActions.SAVE_SUPPORTING_ITEMS_AND_RESET_SERVICE_LOCATION, resultMap.supportingItems, false ); diff --git a/src/layouts/molding-questions/molding-questions.spec.js b/src/layouts/molding-questions/molding-questions.spec.js index 13961df5a..9c003e748 100644 --- a/src/layouts/molding-questions/molding-questions.spec.js +++ b/src/layouts/molding-questions/molding-questions.spec.js @@ -109,7 +109,7 @@ store.getters = { damage: baseStoreGettersDamage, order: {}, }; -store.commit = jest.fn(); +store.dispatch = jest.fn(); afterEach(() => { // reset store after each test diff --git a/src/layouts/quote/quote.vue b/src/layouts/quote/quote.vue index 06b8ecb9a..4a28151f3 100644 --- a/src/layouts/quote/quote.vue +++ b/src/layouts/quote/quote.vue @@ -182,6 +182,7 @@ export default { this.isInsuranceSelected, false ); + if (!this.isInsuranceSelected) { this.dispatchStoreAction( this.storeActions.SAVE_PARENT_ACCOUNT_NUMBER, @@ -196,18 +197,21 @@ export default { ) { this.supportingItems = this.filterOutFees(this.supportingItems); } + if (this.pricedGlassParts.length > 0) { this.dispatchStoreAction( - this.storeActions.SAVE_GLASS_PARTS, + this.storeActions.SAVE_GLASS_PART_PRICES, this.pricedGlassParts, false ); } + this.dispatchStoreAction( - this.storeActions.SAVE_SUPPORTING_ITEMS, + this.storeActions.SAVE_SUPPORTING_ITEMS_AND_RESET_SERVICE_LOCATION, this.supportingItems, false ); + this.dispatchStoreAction(this.storeActions.SAVE_VAPS, this.selectedVaps, false); const payment = this.$store.getters.payment; diff --git a/src/layouts/service-location/helpers/object-cloning-helper/object-cloning-helper.js b/src/layouts/service-location/helpers/object-cloning-helper/object-cloning-helper.js deleted file mode 100644 index 45899973f..000000000 --- a/src/layouts/service-location/helpers/object-cloning-helper/object-cloning-helper.js +++ /dev/null @@ -1,27 +0,0 @@ -// For nested objects, spread operator only creates new references to the top level fields, -// the remaining nested fields actually reference the original object which can introduce problems. - -// The purpose of this method is to deep clone the data in an object recursively, this is useful -// for cloning modelValues to internal models when regular two-way binding is not an option. -// See: mobile-location-modal-questions.vue - -// Creates a deep clone of an object. Clones primitives, arrays and objects, excluding class instances. -// https://www.30secondsofcode.org/js/s/deep-clone -export function deepClone(object) { - if (object === null) { - return null; - } - - let clone = Object.assign({}, object); - Object.keys(clone).forEach( - (key) => - (clone[key] = typeof object[key] === "object" ? deepClone(object[key]) : object[key]) - ); - - if (Array.isArray(object)) { - clone.length = object.length; - return Array.from(clone); - } - - return clone; -} diff --git a/src/layouts/service-location/helpers/object-cloning-helper/object-cloning-helper.spec.js b/src/layouts/service-location/helpers/object-cloning-helper/object-cloning-helper.spec.js deleted file mode 100644 index 4246f9167..000000000 --- a/src/layouts/service-location/helpers/object-cloning-helper/object-cloning-helper.spec.js +++ /dev/null @@ -1,62 +0,0 @@ -import { deepClone } from "./object-cloning-helper"; - -describe("object-cloning-helper.js", () => { - it("Should return null if no object is passed in", async () => { - // Arrange - const expected = null; - - // Act - const result = deepClone(null); - - // Assert - expect(result).toEqual(expected); - }); - - it("Should return a deep copy of the object", async () => { - // Arrange - - const object = { - addressQuestions: { - streetAddress: "555 Some St", - apartmentNumberOrBusinessName: "Apt 1", - city: "Funkytown", - state: "OH", - zipCode: "55555", - }, - isVehicleProtected: true, - serviceZipCode: "55555", - }; - - const expected = { - addressQuestions: { - streetAddress: "555 Some St", - apartmentNumberOrBusinessName: "Apt 1", - city: "Funkytown", - state: "OH", - zipCode: "55555", - }, - isVehicleProtected: true, - serviceZipCode: "55555", - }; - - // Act - const result = deepClone(object); - - // Assert - expect(result).toStrictEqual(expected); - }); - - it("Should return a copy of the array", async () => { - // Arrange - - const array = [9, 8, 7, 6, 5, 4, 3, 2, 1]; - - const expected = [9, 8, 7, 6, 5, 4, 3, 2, 1]; - - // Act - const result = deepClone(array); - - // Assert - expect(result).toStrictEqual(expected); - }); -}); diff --git a/src/layouts/service-location/mobile-location-modal-questions/mobile-location-modal-questions.vue b/src/layouts/service-location/mobile-location-modal-questions/mobile-location-modal-questions.vue index 78094e76b..0052c1368 100644 --- a/src/layouts/service-location/mobile-location-modal-questions/mobile-location-modal-questions.vue +++ b/src/layouts/service-location/mobile-location-modal-questions/mobile-location-modal-questions.vue @@ -66,7 +66,7 @@ import addressQuestions from "@/layouts/address-lookup/customer-questions/addres import vehicleProtectedQuestion from "@/layouts/service-location/mobile-location-modal-questions/vehicle-protected-question/vehicle-protected-question"; // Helpers -import { deepClone } from "@/layouts/service-location/helpers/object-cloning-helper/object-cloning-helper"; +import { deepClone } from "@/helpers/object-helper"; import { getPricedMobileFeePart, getServiceabilityDetails, diff --git a/src/layouts/vehicle-damage/vehicle-damage.vue b/src/layouts/vehicle-damage/vehicle-damage.vue index ae89a61e6..e3dbb7875 100644 --- a/src/layouts/vehicle-damage/vehicle-damage.vue +++ b/src/layouts/vehicle-damage/vehicle-damage.vue @@ -329,7 +329,7 @@ export default { storeActions.GET_SUPPORTING_ITEMS ); this.dispatchStoreAction( - this.storeActions.SAVE_SUPPORTING_ITEMS, + this.storeActions.SAVE_SUPPORTING_ITEMS_AND_RESET_SERVICE_LOCATION, supportingItems.data, false ); diff --git a/src/layouts/vehicle-parts/vehicle-parts.spec.js b/src/layouts/vehicle-parts/vehicle-parts.spec.js index 23ce3b88a..4d4287430 100644 --- a/src/layouts/vehicle-parts/vehicle-parts.spec.js +++ b/src/layouts/vehicle-parts/vehicle-parts.spec.js @@ -481,7 +481,7 @@ describe("vehicle-parts.vue", () => { }, }); - wrapper.vm.$store.commit = jest.fn(); + wrapper.vm.dispatchStoreAction = jest.fn(); wrapper.setData({ selectedGlassParts: { "Rear-Stationary": { partNumber: "DB12209GTYN" } }, @@ -498,7 +498,7 @@ describe("vehicle-parts.vue", () => { await wrapper.vm.forwardButtonAction(); //Assert - expect(wrapper.vm.$store.commit).toHaveBeenCalled(); + expect(wrapper.vm.dispatchStoreAction).toHaveBeenCalled(); // TODO KO UNCOMMENT FOR QUOTE PAGES RELEASE // expect(wrapper.vm.$router.navigateWithSaving).toHaveBeenCalledWith( diff --git a/src/mixins/vehicle-questions-mixin.js b/src/mixins/vehicle-questions-mixin.js index 718f8fc50..4b05067ea 100644 --- a/src/mixins/vehicle-questions-mixin.js +++ b/src/mixins/vehicle-questions-mixin.js @@ -442,7 +442,7 @@ export default { const collectedGlassParts = this.reducedGlassPartsArray(partsOrQuestions); // save to store lineItems.glassParts - self.$store.commit(storeMutations.UPDATE_GLASS_PARTS, collectedGlassParts); + self.dispatchStoreAction(storeActions.SAVE_GLASS_PARTS, collectedGlassParts, false); const payment = store.getters.payment; diff --git a/src/mixins/vehicle-questions-mixin.spec.js b/src/mixins/vehicle-questions-mixin.spec.js index a9bf6f313..f87d7efdc 100644 --- a/src/mixins/vehicle-questions-mixin.spec.js +++ b/src/mixins/vehicle-questions-mixin.spec.js @@ -2240,15 +2240,17 @@ describe("vehicle-questions-mixin", () => { // Assert expect(wrapper.vm.$store.commit).toHaveBeenCalledTimes(1); - expect(wrapper.vm.$store.commit).toHaveBeenCalledWith( - storeMutations.UPDATE_GLASS_PARTS, + expect(wrapper.vm.$store.dispatch).toHaveBeenCalledWith( + storeActions.SAVE_GLASS_PARTS, collectedGlassParts ); + expect(wrapper.vm.$router.navigateWithSaving).toHaveBeenCalledTimes(1); expect(wrapper.vm.$router.navigateWithSaving).toHaveBeenCalledWith( navigationScenarios.CLICKED_FORWARD_WITH_NO_MORE_QUESTIONS, { query: { fmgPage: "vin-lookup" } } ); + expect(wrapper.vm.$router.navigateWithoutSaving).not.toHaveBeenCalled(); }); }); diff --git a/src/store/index.js b/src/store/index.js index b70faf5ed..ee6e40450 100644 --- a/src/store/index.js +++ b/src/store/index.js @@ -1,4 +1,4 @@ -import { createStore, Store } from "vuex"; +import { createStore } from "vuex"; import { endpoints } from "@/constants/endpoints.js"; import { storeMutations } from "@/constants/store-mutations"; import { getDateForSavedSessionTimeout } from "@/helpers/heritage-integration/session-helper"; @@ -9,8 +9,8 @@ import { applicationConfig } from "@/constants/application-config"; import { experimentTriggers } from "@/constants/experiments"; import { damageLocationsSelected } from "@/constants/damage-locations-selected"; import { fmgPageValues } from "@/router/router-constants/fmgPage-values"; -import router from "@/router"; import { deleteFunnelCookie } from "@/helpers/heritage-integration/cookie-helper.js"; +import { deepEqual } from "@/helpers/object-helper"; import { AppointmentTypeStrings, PREMIUM_FEE_PART_TYPE } from "@/constants/schedule-constants"; // Export State @@ -254,18 +254,7 @@ export const mutations = { state.order.serviceLocation.isVehicleProtected = serviceLocationInfo.isVehicleProtected; if (serviceLocationInfo.provider) { - state.order.serviceLocation.provider.providerNumber = - serviceLocationInfo.provider?.providerNumber; - state.order.serviceLocation.provider.address.streetAddress = - serviceLocationInfo.provider?.address?.streetAddress; - state.order.serviceLocation.provider.address.city = - serviceLocationInfo.provider?.address?.city; - state.order.serviceLocation.provider.address.state = - serviceLocationInfo.provider?.address?.state; - state.order.serviceLocation.provider.address.zip = - serviceLocationInfo.provider?.address?.zip; - state.order.serviceLocation.provider.address.zipCtu = - serviceLocationInfo.provider?.address?.zipCtu; + state.order.serviceLocation.provider = serviceLocationInfo.provider; } }, updateSchedule(state, scheduleInfo) { @@ -369,6 +358,20 @@ export const mutations = { resetSaveSessionPromise(state) { state.applicationUser.saveSessionPromise = null; }, + resetServiceLocationAppointmentType(state) { + state.order.serviceLocation.appointmentType = null; + }, + resetServiceLocationProvider(state) { + state.order.serviceLocation.provider = null; + }, + resetServiceLocationMobileAddress(state) { + state.order.serviceLocation.address = null; + state.order.serviceLocation.address2 = null; + state.order.serviceLocation.city = null; + state.order.serviceLocation.state = null; + state.order.serviceLocation.zipCode = null; + state.order.serviceLocation.isVehicleProtected = null; + }, // Misc Mutations updateStateWithOrderInformation(state, sessionInformation) { state.order.referralNumber = sessionInformation.order.referralNumber; @@ -1776,6 +1779,15 @@ export const actions = { context.commit(storeMutations.UPDATE_SUPPORTING_ITEMS, supportingItems); }, + saveSupportingItemsAndResetServiceLocation(context, supportingItems) { + if (!deepEqual(supportingItems, context.state.order.lineItems.supportingItems)) { + context.commit(storeMutations.RESET_SERVICE_LOCATION_APPOINTMENT_TYPE); + context.commit(storeMutations.RESET_SERVICE_LOCATION_PROVIDER); + } + + context.commit(storeMutations.UPDATE_SUPPORTING_ITEMS, supportingItems); + }, + saveVaps(context, vaps) { context.commit(storeMutations.UPDATE_VAPS, vaps); }, @@ -1838,6 +1850,13 @@ export const actions = { }, saveServiceLocation(context, serviceLocationInfo) { + if ( + context.state.order.serviceLocation && + serviceLocationInfo.zipCode !== context.state.order.serviceLocation.zipCode + ) { + context.commit(storeMutations.RESET_SERVICE_LOCATION_MOBILE_ADDRESS); + } + context.commit(storeMutations.UPDATE_SERVICE_LOCATION, serviceLocationInfo); }, @@ -1859,6 +1878,15 @@ export const actions = { }, saveGlassParts(context, parts) { + if (!deepEqual(parts, context.state.order.lineItems.glassParts)) { + context.commit(storeMutations.RESET_SERVICE_LOCATION_APPOINTMENT_TYPE); + context.commit(storeMutations.RESET_SERVICE_LOCATION_PROVIDER); + } + + context.commit(storeMutations.UPDATE_GLASS_PARTS, parts); + }, + + saveGlassPartPrices(context, parts) { context.commit(storeMutations.UPDATE_GLASS_PARTS, parts); }, diff --git a/src/store/store.spec.js b/src/store/store.spec.js index 6913c0527..262cb263a 100644 --- a/src/store/store.spec.js +++ b/src/store/store.spec.js @@ -961,7 +961,10 @@ describe("Actions", () => { it("saveGlassParts, should call mutation", () => { // Arrange - const context = state; + const context = { + state: state, + }; + const commit = jest.fn(); context.commit = commit;