diff --git a/src/common-components/button-question/button-question.spec.js b/src/common-components/button-question/button-question.spec.js index 9cf75368e..9cd9307db 100644 --- a/src/common-components/button-question/button-question.spec.js +++ b/src/common-components/button-question/button-question.spec.js @@ -10,6 +10,7 @@ describe("buttonQuestion.vue", () => { const wrapper = shallowMount(buttonQuestion, { propsData: { isOverflowScrollable: true, + groupName: "group-name" } }); @@ -25,6 +26,7 @@ describe("buttonQuestion.vue", () => { const wrapper = shallowMount(buttonQuestion, { propsData: { buttonType: "listCard", + groupName: "group-name" } }); // Assert @@ -39,6 +41,7 @@ describe("buttonQuestion.vue", () => { const wrapper = shallowMount(buttonQuestion, { propsData: { buttonType: "listButtonHorizontal", + groupName: "group-name" } }); // Assert @@ -53,6 +56,7 @@ describe("buttonQuestion.vue", () => { const wrapper = shallowMount(buttonQuestion, { propsData: { buttonType: "radio", + groupName: "group-name" } }); // Assert @@ -77,7 +81,8 @@ describe("buttonQuestion.vue", () => { // Act const localThis = { isWide: false, - answers: ['a', 'b'] + answers: ['a', 'b'], + groupName: "group-name" } expect(buttonQuestion.computed.getColLength.call(localThis)).toBe(""); @@ -109,7 +114,7 @@ describe("buttonQuestion.vue", () => { describe("buttonQuestion.vue", () => { it("Should trigger event modelValue change to new value on when radio button selected", async () => { // Act - const wrapper = shallowMount(buttonQuestion, setupMocks({})); + const wrapper = shallowMount(buttonQuestion, setupMocks({propsData: {groupName: "group-name"}})); await wrapper.setProps({ answers: ["2022", "2021", "2020"], isMultiSelect: false, @@ -129,6 +134,7 @@ describe("buttonQuestion.vue", () => { propsData: { modelValue: ["2022", "2021", "2020"], isMultiSelect: true, + groupName: "group-name" } })); const val = { checkValue: true, value: "2019", } @@ -144,7 +150,8 @@ describe("buttonQuestion.vue", () => { const wrapper = shallowMount(buttonQuestion, setupMocks({ propsData: { isMultiSelect: true, - modelValue: ['a', 'b'] + modelValue: ['a', 'b'], + groupName: "group-name" } })); const val = { checkValue: true, value: "2021", } @@ -161,7 +168,8 @@ describe("buttonQuestion.vue", () => { const wrapper = shallowMount(buttonQuestion, setupMocks({ propsData: { isMultiSelect: true, - modelValue: ['a', 'b'] + modelValue: ['a', 'b'], + groupName: "group-name" } })); @@ -173,24 +181,6 @@ describe("buttonQuestion.vue", () => { }); }); - -describe("buttonQuestion.vue", () => { - it("Should do nothing to this.selectedValues if this.selectedValues is not an array", () => { - // Act - const wrapper = shallowMount(buttonQuestion, setupMocks({ - propsData: { - isMultiSelect: true, - modelValue: 'a', - } - })); - const val = { checkValue: true, value: "c", } - wrapper.vm.handleCheckedChanged(val); - - // Assert - expect(wrapper.vm.selectedValues).toEqual("a"); - }); -}); - function setupMocks(mountOptionsMockData = {}) { const defaultMountOptions = { route: { query: { fmgPage: 'page-name' } } }; const baseMountOptions = getMountOptions(Object.assign(defaultMountOptions, mountOptionsMockData)); diff --git a/src/common-components/button-question/button-question.vue b/src/common-components/button-question/button-question.vue index f1c2ad98a..0cc7d5e03 100644 --- a/src/common-components/button-question/button-question.vue +++ b/src/common-components/button-question/button-question.vue @@ -1,27 +1,26 @@ @@ -81,22 +87,31 @@ export default { isRequired: Boolean, isOverflowScrollable: Boolean, isWide: Boolean, - modelValue: Array, + modelValue: [Array, String], validationRules: String, suppressError: Boolean, useTextForValue: Boolean, clearOnUnmount: { type: Boolean, default: true - } + }, }, computed: { - getFieldSetClasses() { - return this.isOverflowScrollable - ? "container-fluid overflow-scroll position-absolute px-5 pt-1 py-0" - : ""; + formattedGroupName() { + return this.groupName.replace(" ", "-"); }, - getComponentWrapperClasses() { + getFieldSetClasses() { + if (this.isOverflowScrollable) { + return "container-fluid overflow-scroll position-absolute px-5 pt-1 py-0"; + } + else if (this.buttonType == "listCard") { + return "w-100"; + } + else { + return ""; + } + }, + getComponentLoopWrapperClasses() { let classes; switch (this.buttonType) { case "listButton": @@ -106,7 +121,7 @@ export default { classes = "d-flex flex-row p-0"; break; case 'listCard': - classes = 'row justify-content-center g-2' + classes = "row g-2 justify-content-center"; break; case 'radio': classes = 'ui-radio d-flex' @@ -114,11 +129,22 @@ export default { } return classes; }, + getComponentWrapperClasses() { + let classes = ""; + + classes += this.isWide ? "col-12" : "col"; + + if (this.buttonType == "radio") { + classes += " radio-button-container"; + } + + return classes; + }, getColLength(){ if(this.isWide) { return "12" } else { - return this.answers.length < 3 ? '' : '-4'; + return ""; } }, selectedValues: { @@ -135,6 +161,7 @@ export default { if (this.useTextForValue){ return answer.Text } + return answer.Name ? answer.Name : answer; }, handleCheckedChanged(val) { @@ -146,7 +173,12 @@ export default { val.checkValue ? newSelectedValues.push(val.value) : newSelectedValues.splice(newSelectedValues.indexOf(val.value), 1); this.selectedValues = newSelectedValues; } + else { + this.selectedValues = val.value; + } } + + this.$emit("isCheckedChanged", val); }, }, components: { @@ -172,6 +204,12 @@ export default { } .button-question { color: $black; + + .radio-button-container { + &:not(:last-child) { + padding-bottom: map-get($spacers, 2); + } + } } .question-text { margin-top: 1.5rem; diff --git a/src/common-components/textbox-question/textbox-question.spec.js b/src/common-components/textbox-question/textbox-question.spec.js index 48f9a3a8e..b830b9783 100644 --- a/src/common-components/textbox-question/textbox-question.spec.js +++ b/src/common-components/textbox-question/textbox-question.spec.js @@ -15,7 +15,7 @@ const maska = jest.fn(); describe("textboxQuestion.vue", () => { it("Should render a text input", async () => { - // Arrange + // Arrange const wrapper = shallowMount(textboxQuestion, { global: { directives: { @@ -53,35 +53,7 @@ describe("textboxQuestion.vue", () => { expect(label.text()).toContain(questionText); }); - - it("Should render the 'questionText' data value with '⁠' after the first character of each word in the label text when disableAutoFill is true.", async () => { - // Arrange - const wrapper = shallowMount(textboxQuestion, { - global: { - directives: { - maska: maska, - } - }, - propsData: { - disableAutoFill: true, - }, - mixins: [mockMixin] - }); - // Mock CMS content ... - // Trust me, the below instance of the string "Q⁠uestion Text" actually has the ⁠ in it. You just can't see it - // Don't believe me? Copy and paste it into Google. Then inspect the search field element in Dev Tools, - // you will see "Q⁠uestion T⁠ext" - const expectedQuestionText = "Q⁠uestion T⁠ext"; - - // Act - const label = wrapper.find("label"); - - // Assert - expect(label.text()).toContain(expectedQuestionText); - - }); - it("Should return input id as the id of the input field", async () => { // Arrange const wrapper = shallowMount(textboxQuestion, { @@ -103,7 +75,7 @@ describe("textboxQuestion.vue", () => { expect(input.attributes().id).toEqual("input ID"); }); - + it("Should render the 'questionText' data value as the aria-label attribute.", async () => { // Arrange @@ -193,6 +165,6 @@ describe("textboxQuestion.vue", () => { // Assert expect(wrapper.vm.handleChange).toHaveBeenCalled; - }); + }); }); diff --git a/src/common-components/textbox-question/textbox-question.vue b/src/common-components/textbox-question/textbox-question.vue index 19163110e..822548cc9 100644 --- a/src/common-components/textbox-question/textbox-question.vue +++ b/src/common-components/textbox-question/textbox-question.vue @@ -1,7 +1,6 @@ @@ -122,14 +122,14 @@ export default { firstName: this.getRegistrationFirstNameFromStore(), lastName: this.getRegistrationLastNameFromStore(), emailAddress: this.getEmailFromStore(), - }, + }, serviceZipCode: this.getServiceZipFromStore(), displayNonServiceableZipAlert: false, displayVinNotFoundAlert: false, displayMatchedDifferentVehicleAlert: false, displayVinLookupByHomeAddressNotAllowedAlert: false, previouslyEnteredCarId: "", - isSelectedGlassAvailableForVehicle: false, + isSelectedGlassAvailableForVehicle: false, customAlertData: {}, showServiceZipField: this.getServiceZipFromStore(), isZipServicable: false, @@ -150,7 +150,7 @@ export default { this.$route ); }, - attachCustomEvents() { + attachCustomEvents() { this.prependActionToMethod(this, this.forwardButtonAction, () => { this.pushEventToGA( this.$route.query[this.queryStrings.FMG_PAGE], @@ -177,7 +177,7 @@ export default { }, getRegistrationLastNameFromStore() { return store.getters.vehicle.registration.lastName; - }, + }, getEmailFromStore() { return store.getters.order.customer.emailAddress; }, @@ -189,9 +189,9 @@ export default { // Lookup VIN(s) with the provided address const vinLookupPromise = this.lookupVin( - this.customerQuestions.lastName, - this.customerQuestions.addressQuestions.streetAddress, - this.customerQuestions.addressQuestions.zipCode, + this.customerQuestions.lastName, + this.customerQuestions.addressQuestions.streetAddress, + this.customerQuestions.addressQuestions.zipCode, this.customerQuestions.addressQuestions.state ); @@ -200,8 +200,8 @@ export default { const vinLookupResponse = await vinLookupPromise; const serviceZipValidationResponse = await serviceZipValidationPromise; - - if (!vinLookupResponse.data.isStatePermissible) { + + if (!vinLookupResponse.data.isStatePermissible) { // State Restrictions forbid lookup by address this.displayVinLookupByHomeAddressNotAllowedAlert = true; this.$refs.funnelFooter.removeLoader(); @@ -210,29 +210,29 @@ export default { // if the neither the registration zip code or service zip code are not serviceable this.isZipServicable = serviceZipValidationResponse.data.isServiceable; - if (!this.isZipServicable) { + if (!this.isZipServicable) { this.displayNonServiceableZipAlert = true; this.showServiceZipField = true; - this.$refs.funnelFooter.removeLoader(); + this.$refs.funnelFooter.removeLoader(); } else if (!this.serviceZipCode) { // if the registration zip code is servicable and nothing was entered for the service zip code // then set the service zip code to the registration zip code this.serviceZipCode = this.customerQuestions.addressQuestions.zipCode; - } + } const carEntered = store.getters.vehicle; const carsFound = vinLookupResponse.data.vinVehicles; if (carsFound.length == 0) { - // No VINs found + // No VINs found this.displayVinNotFoundAlert = true; this.$refs.funnelFooter.removeLoader(); - return; - } else if (carsFound.length == 1) { + return; + } else if (carsFound.length == 1) { const carFound = carsFound[0].vehicle; this.isCarIdDifferent = carFound.carId !== carEntered.carId; - if (this.isCarIdDifferent && carFound.carId !== this.previouslyEnteredCarId) { + if (this.isCarIdDifferent && carFound.carId !== this.previouslyEnteredCarId) { // Display Alert this.previouslyEnteredCarId = carFound.carId; this.customAlertData.vehicleInfo = carFound; @@ -241,12 +241,12 @@ export default { this.isSelectedGlassAvailableForVehicle = await isGlassAvailableForCarId(carFound.carId); // Update button "Continue with..." - this.$refs.funnelFooter.updateButtonText(`Continue with ${carFound.year} ${carFound.make} ${carFound.model}`); + this.$refs.funnelFooter.updateButtonText(`Continue with ${carFound.year} ${carFound.make} ${carFound.model}`); this.$refs.funnelFooter.removeLoader(); - + return; - } - + } + if (!this.isZipServicable) { return; } @@ -254,21 +254,21 @@ export default { // update data if the zip or service zip is servicable this.updateVehicleInfo(carsFound[0].vin, carFound); this.updateCustomerInfo(serviceZipValidationResponse.data.state); - + } else if (carsFound.length > 1) { if (!this.isZipServicable) { return; } // if multiple cars were found - let matchingCars = carsFound.filter(car => car.vehicle.carId === carEntered.carId); + let matchingCars = carsFound.filter(car => car.vehicle.carId === carEntered.carId); if (matchingCars.length === 1) { // and one and only one of them matches the carId entered, save the vehicle info // so we can go to the Heritage Funnel directly const matchingCar = matchingCars[0]; this.updateVehicleInfo(matchingCar.vin, matchingCar.vehicle); } - + // update data if the zip or service zip is servicable this.updateCustomerInfo(serviceZipValidationResponse.data.state); } @@ -277,13 +277,13 @@ export default { }, resetWarningsAndErrors() { this.displayVinNotFoundAlert = false; - this.displayNonServiceableZipAlert = false; + this.displayNonServiceableZipAlert = false; this.displayMatchedDifferentVehicleAlert = false; this.displayVinLookupByHomeAddressNotAllowedAlert = false; }, navigateForward(carEntered, carsFound) { this.updateServiceLocationIfNecessary(); - + if (carsFound.length == 1) { // if a different vehicle is found than the one entered and the selected glass // is not available for that vehicle @@ -297,37 +297,37 @@ export default { }, {} ); } else { - // otherwise + // otherwise this.navigateForwardWithSingleCarMatch(); - } + } } else if (carsFound.length > 1) { // if multiple cars were found - let matchingCars = carsFound.filter(car => car.vehicle.carId === carEntered.carId); + let matchingCars = carsFound.filter(car => car.vehicle.carId === carEntered.carId); if (matchingCars.length === 1) { // and one and only of them matches the car id entered - const matchingCar = matchingCars[0]; + const matchingCar = matchingCars[0]; this.updateVehicleInfo(matchingCar.vin, matchingCar.vehicle); this.navigateForwardWithSingleCarMatch(); } else { // if there are no matches or there are multiple matches, navigate to "address-vehicles" page this.$router.navigateAfterSave(this.navigationScenarios.CONTINUING_WITH_MULTIPLE_VEHICLES, this.$route, {}, {}, carsFound); } - } + } }, validateZip(zip) { return this.dispatchStoreAction( - storeActions.VALIDATE_ZIP, + storeActions.VALIDATE_ZIP, { zip }); }, lookupVin(lastName, streetAddress, zip, state) { return this.dispatchStoreAction( storeActions.LOOKUP_VIN_BY_ADDRESS, - { - licenseLastName: lastName, - licenseStreetAddress: streetAddress, - licenseZip: zip, - licenseState: state + { + licenseLastName: lastName, + licenseStreetAddress: streetAddress, + licenseZip: zip, + licenseState: state }, false ); }, @@ -342,7 +342,7 @@ export default { store.commit(storeMutations.UPDATE_VEHICLE_IMAGE_URL, vehicleInfo.imageUrl); store.commit(storeMutations.UPDATE_VEHICLE_IMAGE_VIF_NUMBER, vehicleInfo.imageVifNumber); store.commit(storeMutations.UPDATE_VEHICLE_IMAGE_COLOR, vehicleInfo.imageColor); - }, + }, updateCustomerInfo(serviceState) { store.commit(storeMutations.UPDATE_REGISTRATION_ADDRESS, this.customerQuestions.addressQuestions.streetAddress); store.commit(storeMutations.UPDATE_REGISTRATION_CITY, this.customerQuestions.addressQuestions.city); @@ -350,10 +350,10 @@ export default { store.commit(storeMutations.UPDATE_REGISTRATION_ZIP_CODE, this.customerQuestions.addressQuestions.zipCode); store.commit(storeMutations.UPDATE_REGISTRATION_FIRST_NAME, this.customerQuestions.firstName); store.commit(storeMutations.UPDATE_REGISTRATION_LAST_NAME, this.customerQuestions.lastName); - store.commit(storeMutations.UPDATE_SERVICE_LOCATION_ZIP_CODE, this.serviceZipCode); - store.commit(storeMutations.UPDATE_SERVICE_LOCATION_STATE, serviceState); + store.commit(storeMutations.UPDATE_SERVICE_LOCATION_ZIP_CODE, this.serviceZipCode); + store.commit(storeMutations.UPDATE_SERVICE_LOCATION_STATE, serviceState); store.commit(storeMutations.UPDATE_CUSTOMER_EMAIL_ADDRESS, this.customerQuestions.emailAddress); - }, + }, updateServiceLocationIfNecessary() { const serviceLocation = store.getters.order.serviceLocation; @@ -373,7 +373,7 @@ export default { }, AlertNonServiceableZipBody() { return this.getCmsContent("AlertNonServiceableZipWidget", "BodyText"); - }, + }, AlertMatchedDifferentVehicleHeader() { const text = this.getCmsContent("AlertMatchedDifferentVehicleWidget", "HeadlineText").replaceAll("{custom:glassText}", getDamageString()); return text; @@ -386,13 +386,13 @@ export default { const content = this.getCmsContent("AlertMatchedDifferentVehicleWidget", "BodyText") .replaceAll("{custom:glassText}", getDamageString()) .replaceAll("{custom:vinYmmFound}", vinYmmFound) - .replaceAll("{custom:vinYmmExpected}", vinYmmExpected); + .replaceAll("{custom:vinYmmExpected}", vinYmmExpected); return content; - }, + }, }, watch: { - customerQuestions: { + customerQuestions: { handler(newValue) { // if they modify one of the lookup fields (address, city, state, zipCode, or lastName), then modify the button text back to “Get my personalized quote” this.$refs.funnelFooter.updateButtonText(this.getCmsContent("FunnelFooterWidget", "ForwardButtonText")); @@ -412,8 +412,8 @@ export default { // if the Service Zip Code field is ever hidden, clear out it's value if (!newValue) { this.serviceZipCode = null; - } - }, + } + }, } }, components: { diff --git a/src/layouts/address-vehicles/address-vehicles.vue b/src/layouts/address-vehicles/address-vehicles.vue index 8a36bb51f..988e12e38 100644 --- a/src/layouts/address-vehicles/address-vehicles.vue +++ b/src/layouts/address-vehicles/address-vehicles.vue @@ -223,7 +223,11 @@ export default { handler() { // does this vehicle match the previously selected carId? this.isCarIdDifferent = this.selectedVehicle.vehicle.carId !== store.getters.vehicle.carId; - this.$refs.funnelFooter.updateButtonText(`Continue with ${this.selectedVehicle.vehicle.year} ${this.selectedVehicle.vehicle.make} ${this.selectedVehicle.vehicle.model}`); + if (this.isCarIdDifferent) { + this.$refs.funnelFooter.updateButtonText(`Continue with ${this.selectedVehicle.vehicle.year} ${this.selectedVehicle.vehicle.make} ${this.selectedVehicle.vehicle.model}`); + } else { + this.$refs.funnelFooter.updateButtonText(this.getCmsContent("FunnelFooterWidget", "ForwardButtonText")); + } }, deep: true }, diff --git a/src/layouts/quote/quote.vue b/src/layouts/quote/quote.vue new file mode 100644 index 000000000..158150baf --- /dev/null +++ b/src/layouts/quote/quote.vue @@ -0,0 +1,10 @@ + \ No newline at end of file diff --git a/src/layouts/vehicle-make/make-question/make-question.vue b/src/layouts/vehicle-make/make-question/make-question.vue index 26a17955c..5319d96f2 100644 --- a/src/layouts/vehicle-make/make-question/make-question.vue +++ b/src/layouts/vehicle-make/make-question/make-question.vue @@ -7,7 +7,7 @@ :answers="makes" groupName="ChooseVehicleMake" textPosition="text-start" - v-model="selectedValueAsArray" + v-model="selectedValue" isRequired=true /> @@ -34,14 +34,13 @@ export default { questionText(){ return this.getCmsContent(this.cmsWidgetName, 'QuestionText'); }, - selectedValueAsArray: { + selectedValue: { get: function() { const modelValueAsArray = this.modelValue ? [this.modelValue] : []; return modelValueAsArray; }, set: function(newValue) { - const newValueAsScalar = newValue && newValue.length > 0 ? newValue[0] : null; - this.$emit("update:modelValue", newValueAsScalar); + this.$emit("update:modelValue", newValue); } } }, diff --git a/src/layouts/vehicle-model/model-question/model-question.vue b/src/layouts/vehicle-model/model-question/model-question.vue index d8cb21894..5d2b1ebd2 100644 --- a/src/layouts/vehicle-model/model-question/model-question.vue +++ b/src/layouts/vehicle-model/model-question/model-question.vue @@ -7,7 +7,7 @@ :answers="models" groupName="ChooseVehicleModel" textPosition="text-start" - v-model="selectedValueAsArray" + v-model="selectedValue" isRequired=true /> @@ -34,14 +34,13 @@ export default { questionText(){ return this.getCmsContent(this.cmsWidgetName, 'QuestionText'); }, - selectedValueAsArray: { + selectedValue: { get: function() { const modelValueAsArray = this.modelValue ? [this.modelValue] : []; return modelValueAsArray; }, set: function(newValue) { - const newValueAsScalar = newValue && newValue.length > 0 ? newValue[0] : null; - this.$emit("update:modelValue", newValueAsScalar); + this.$emit("update:modelValue", newValue); } } }, diff --git a/src/layouts/vehicle-parts/glass-part-question/glass-part-question.spec.js b/src/layouts/vehicle-parts/glass-part-question/glass-part-question.spec.js index a6ef4e586..52471bf34 100644 --- a/src/layouts/vehicle-parts/glass-part-question/glass-part-question.spec.js +++ b/src/layouts/vehicle-parts/glass-part-question/glass-part-question.spec.js @@ -69,24 +69,25 @@ describe("glass-part-question.vue", () => { }); test("Should emit updateModelValue, and have correct attributes", async () => { - //Arrange - const { wrapper } = setupMocks(featureListData); + store.getters.pageData.mockReset(); + store.getters.pageData.mockReturnValueOnce({ partsOrQuestions: [{glassName: "Stationary", glassLocation: "Rear", parts: [{ partNumber: "DB12209GTYN", color: "Green Tint"}]}] }); //Act await wrapper.vm.$nextTick(); const listCard = await wrapper.findComponent({ - name: "listCard", + name: "buttonQuestion", }); - wrapper.setValue({ selectedTint: 'Green Tint' }); + await wrapper.setData({ selectedTint: 'Green Tint' }); + // to trigger the computed setter + wrapper.vm.selectedPartNumber = "DB12209GTYN"; //Assert - expect(wrapper.emitted()["update:modelValue"][0]).toEqual([{ selectedTint: 'Green Tint' }]); - expect(listCard.attributes("buttonid")).toBe("Rear-Stationary-Green Tint"); + expect(wrapper.emitted()["update:modelValue"][0]).toEqual([{ partNumber: "DB12209GTYN", color: "Green Tint"}]); expect(listCard.attributes("groupname")).toBe("Rear-Stationary"); - expect(listCard.attributes("isradio")).toBe("true"); + expect(listCard.attributes("validationrules")).toBe("Rear-Stationary-tint-required"); }); test("ResetTintAndPartSelections, should reset data elements ", async () => { @@ -96,21 +97,101 @@ describe("glass-part-question.vue", () => { //Act await wrapper.vm.$nextTick(); - wrapper.setData({ selectedTint: { "Rear-Stationary": 'Green Tint' } }); - - expect(wrapper.vm.selectedTint).toStrictEqual({ "Rear-Stationary": 'Green Tint' }); + await wrapper.setData({ selectedTint: 'Green Tint', selectedPartNumber: "test" }); + + expect(wrapper.vm.selectedTint).toEqual('Green Tint'); + expect(wrapper.vm.selectedPartNumber).toEqual('test'); await wrapper.vm.ResetTintAndPartSelections(); - expect(wrapper.vm.selectedTint).toStrictEqual({}); + + expect(wrapper.vm.selectedTint).toEqual("Green Tint"); + expect(wrapper.vm.selectedPartNumber).toEqual(null); }); + test("default is selected if only one option", async () => { + // Arrange + store.getters.pageData.mockReset(); + store.getters.pageData.mockReturnValueOnce({ partsOrQuestions: [{glassName: "Stationary", glassLocation: "Rear", parts: [{ partNumber: "DB12209GTYN", color: "Green Tint"}]}] }); + const { wrapper } = setupMocks(featureListData); + + // Act + await wrapper.vm.$nextTick(); + await wrapper.setData({ selectedTint: "Green Tint" }); + // take emitted value, pass down as modelValue + // yes, yes, it's not ideal + await wrapper.setProps({ modelValue: wrapper.emitted()["update:modelValue"][0][0] }) + await wrapper.vm.$nextTick(); + + // Assert + expect(wrapper.vm.selectedPartNumber).toBe("DB12209GTYN"); + }); + + test("default is not selected if more than one option", async () => { + // Arrange + store.getters.pageData.mockReset(); + store.getters.pageData.mockReturnValueOnce({ partsOrQuestions: [{glassName: "Stationary", glassLocation: "Rear", parts: [{ partNumber: "DB12209GTYN", color: "Green Tint"}, { partNumber: "DB12209GTYNXXX", color: "Green Tint"}]}] }); + const { wrapper } = setupMocks(featureListData); + + // Act + await wrapper.vm.$nextTick(); + await wrapper.setData({ selectedTint: "Green Tint" }); + await wrapper.vm.$nextTick(); + + // Assert + expect(wrapper.emitted()["update:modelValue"]).toBeFalsy(); + expect(wrapper.vm.selectedPartNumber).toBeFalsy(); + }) + + const partsForSelectedTintTestCases = [ + ["Rear", "Stationary", "Green Tint", [{ partNumber: "Glass1", color: "Green Tint"}, { partNumber: "Glass3", color: "Green Tint"}, { partNumber: "Glass4", color: "Green Tint"}, { partNumber: "Glass6", color: "Green Tint"} ]], + ["Rear", "Stationary", "Blue Tint", [{ partNumber: "Glass2", color: "Blue Tint"}, { partNumber: "Glass5", color: "Blue Tint"}]], + ["Rear", "Stationary", "Red Tint", [{ partNumber: "Glass7", color: "Red Tint"}]], + ["Windshield", "Single", "Green Tint", [{ partNumber: "Windshield1", color: "Green Tint"}, { partNumber: "Windshield2", color: "Green Tint"}]], + ["Windshield", "Single", "Blue Tint", []], + ["Driver", "Quarter", "Green Tint", []] + ]; + test.each(partsForSelectedTintTestCases)("partsForSelectedTint returns correct parts", async (glassLocation, glassName, selectedTint, expectedResults) => { + // Arrange + store.getters.pageData.mockReset(); + store.getters.pageData.mockReturnValueOnce({ partsOrQuestions: [ + { + glassName: "Stationary", + glassLocation: "Rear", + parts: [ + { partNumber: "Glass1", color: "Green Tint"}, + { partNumber: "Glass2", color: "Blue Tint"}, + { partNumber: "Glass3", color: "Green Tint"}, + { partNumber: "Glass4", color: "Green Tint"}, + { partNumber: "Glass5", color: "Blue Tint"}, + { partNumber: "Glass6", color: "Green Tint"}, + { partNumber: "Glass7", color: "Red Tint"} + ] + }, + { + glassName: "Single", + glassLocation: "Windshield", + parts: [{ partNumber: "Windshield1", color: "Green Tint"}, { partNumber: "Windshield2", color: "Green Tint"}] + } + ]}); + const { wrapper } = setupMocks({ + glassLocationProp: glassLocation, + glassNameProp: glassName, + colorAnswersProp: [], + }); + + // Act + await wrapper.setData({ selectedTint: selectedTint }); + + // Assert + expect(expectedResults).toEqual(wrapper.vm.partsForSelectedTint); + }); }); -function setupMocks({ glassNameProp, glassLocationProp, colorAnswersProp, modelValueProp }) { +function setupMocks({ glassNameProp, glassLocationProp, colorAnswersProp, modelValueProp }) { //Mock store - store.getters.pageData.mockReturnValueOnce({ partsOrQuestions: {}}); + store.getters.pageData.mockReturnValueOnce({ partsOrQuestions: [{glassName: "Stationary", glassLocation: "Rear", parts: []}] }); store.getters.lineItems = { glassParts: {} } const mountOptions = getMountOptions({ diff --git a/src/layouts/vehicle-parts/glass-part-question/glass-part-question.vue b/src/layouts/vehicle-parts/glass-part-question/glass-part-question.vue index d0def364d..608cf55cb 100644 --- a/src/layouts/vehicle-parts/glass-part-question/glass-part-question.vue +++ b/src/layouts/vehicle-parts/glass-part-question/glass-part-question.vue @@ -1,245 +1,230 @@ diff --git a/src/layouts/vehicle-parts/vehicle-parts.spec.js b/src/layouts/vehicle-parts/vehicle-parts.spec.js index a663c5396..c43136079 100644 --- a/src/layouts/vehicle-parts/vehicle-parts.spec.js +++ b/src/layouts/vehicle-parts/vehicle-parts.spec.js @@ -65,7 +65,7 @@ describe("vehicle-parts.vue", () => { test("Set cms content called on load", async (done) => { //Arrange store.getters.pageData.mockReturnValue(basePartResponse); - store.getters.lineItems = { glassParts: {} } + store.getters.lineItems = { glassParts: null } const { wrapper, apiPromise } = setupMocks( { @@ -104,7 +104,7 @@ describe("vehicle-parts.vue", () => { //Arrange store.getters.pageData.mockReturnValue(basePartResponse); - store.getters.lineItems = { glassParts: {} } + store.getters.lineItems = { glassParts: null } const { wrapper } = setupMocks({ mountOptionsMockData: { @@ -141,7 +141,7 @@ describe("vehicle-parts.vue", () => { //Arrange store.getters.pageData.mockReturnValueOnce(basePartResponse); - store.getters.lineItems = { glassParts: { 0: { partNumber: 'DB12209YPYNOEM'} } } + store.getters.lineItems = { glassParts: [{ partNumber: 'DB12209YPYNOEM'} ] } const { wrapper } = setupMocks({ mountOptionsMockData: { @@ -170,14 +170,21 @@ describe("vehicle-parts.vue", () => { await nextTick(); //Assert - expect(wrapper.vm.glassParts).toEqual({ "Rear-Stationary": { "Rear": ['DB12209YPYNOEM'] } }); + expect(wrapper.vm.glassParts).toEqual({ "Rear-Stationary": { + partNumber: "DB12209YPYNOEM", + description: "heated glass, solar, 1 hole", + color: "Gray Tint Privacy", + requiresRecalibration: false, + requiresCapabilityQuestions: false, + childParts: null + }}); }); test("BackButtonAction triggers a router.navigate change", async () => { //Arrange store.getters.pageData.mockReturnValueOnce(basePartResponse); - store.getters.lineItems = { glassParts: {} } + store.getters.lineItems = { glassParts: null } const { wrapper } = setupMocks({ mountOptionsMockData: { @@ -215,7 +222,7 @@ describe("vehicle-parts.vue", () => { //Arrange store.getters.pageData.mockReturnValueOnce(basePartResponse); - store.getters.lineItems = { glassParts: {} } + store.getters.lineItems = { glassParts: null } store.commit = jest.fn(); @@ -238,7 +245,7 @@ describe("vehicle-parts.vue", () => { } }); - wrapper.setData({ glassParts: { "Rear-Stationary": { "Rear": ['DB12209YPYNOEM'] } } }); + wrapper.setData({ glassParts: {"Rear-Stationary": { partNumber: 'DB12209GTYN'}}}); //Act vehicleParts.beforeRouteEnter.call( @@ -285,13 +292,14 @@ function setupMocks({ pageHeaderWidgetHeaderText = {}, mountOptionsMockData = {} settleAllPromises.mockImplementation(() => apiPromise); fetchCmsContentForPage.mockImplementation(() => Promise.resolve()); - const mountOptions = getMountOptions(mountOptionsMockData,); + const mountOptions = getMountOptions(mountOptionsMockData); const wrapper = shallowMount(vehicleParts, mountOptions); const partQuestionRearWrapper = wrapper.findComponent({ name: "glassPartQuestion", }); partQuestionRearWrapper.vm.initializeComponent = glassPartQuestion.methods.initializeComponent; wrapper.vm.setCmsContent = baseMixin.methods.setCmsContent; + wrapper.vm.$refs.funnelFooter.removeLoader = jest.fn(); return { wrapper, apiPromise }; } diff --git a/src/layouts/vehicle-parts/vehicle-parts.vue b/src/layouts/vehicle-parts/vehicle-parts.vue index 9ab861f0f..e21df516b 100644 --- a/src/layouts/vehicle-parts/vehicle-parts.vue +++ b/src/layouts/vehicle-parts/vehicle-parts.vue @@ -3,14 +3,13 @@ @submit="onSubmit" @invalid-submit="onInvalidSubmit" ref="theForm" - v-slot="{ meta }" >
-
+
- -
+
-
-
-
- +
+ :ref="`${RefPrefix}-${item.glassLocation}-${item.glassName}`" + v-model="glassParts[item.glassLocation + '-' + item.glassName]" + :glassLocation="item.glassLocation" + :glassName="item.glassName" + :colorAnswers="item.colorAnswers" + :alreadyPopulatedPartsData="alreadyPopulatedPartsData" + />
@@ -64,12 +59,7 @@ import { settleAllPromises } from "@/helpers/layout-helper"; import { fmgPageValues } from "@/router/router-constants/fmgPage-values"; import { storeMutations } from "@/constants/store-mutations"; import store from "@/store"; -import { Form, defineRule } from "vee-validate"; -import { required } from "@/helpers/validation-rules"; -import { errorMessages } from "@/constants/error-messages"; - -// DEFINE VALIDATION RULES -defineRule("replace-options-required", required(errorMessages.OPTION_REQUIRED)); +import { Form } from "vee-validate"; export default { name: "vehicle-parts", @@ -88,12 +78,12 @@ export default { // Glass Part Question dynamic component Object.keys(vm.$refs) - .filter((r) => r.includes(vm.RefPrefix) && vm.$refs[r][0] !== undefined) - .forEach((c) => - vm.$refs[c][0].initializeComponent({ - ColorQuestionWidget: resultMap.cmsContent.ColorQuestionWidget, - FeatureQuestionWidget: resultMap.cmsContent.FeatureQuestionWidget, - }) + .filter((r) => r.includes(vm.RefPrefix) && vm.$refs[r][0] !== undefined) + .forEach((c) => + vm.$refs[c][0].initializeComponent({ + ColorQuestionWidget: resultMap.cmsContent.ColorQuestionWidget, + FeatureQuestionWidget: resultMap.cmsContent.FeatureQuestionWidget, + }) ); }); }, @@ -101,6 +91,7 @@ data() { return { glassParts: {}, alertWidgetData: Object, + alreadyPopulatedPartsData: {} }; }, components: { @@ -113,7 +104,24 @@ components: { alert, }, computed: { - PartsForQuestions() { + isForwardActionDisabled() { + return Object.keys(this.matchedParts).length !== this.PartsFromApi.partsOrQuestions.length; + }, + matchedParts() { + const matchedParts = []; + + // Match them to the parts from the API. + this.PartsFromApi.partsOrQuestions.forEach(part => { + const selectedPartForGlassLocationAndName = this.glassParts[`${part.glassLocation}-${part.glassName}`]; + const selectedPartData = part.parts.filter(part => selectedPartForGlassLocationAndName && part.partNumber == selectedPartForGlassLocationAndName?.partNumber)[0]; + + if (selectedPartData) + matchedParts.push(selectedPartData); + }) + + return matchedParts; + }, + PartsOrQuestions() { const partsData = this.PartsFromApi; // Map API result data, to vehicle-parts data structure @@ -165,40 +173,14 @@ methods: { this.$router.navigate(this.navigationScenarios.CLICKED_BACK, this.$route); }, forwardButtonAction() { - const selectedGlassPartNumbers = []; - const matchedParts = []; - - // Compile all selected parts from the page. - for (let [key, value] of Object.entries(this.glassParts)) { - for (let [glassKey, glassValue] of Object.entries(value)) { - selectedGlassPartNumbers.push(glassValue[0]); - } - } - - // Match them to the parts from the API. - for (let [key, value] of Object.entries( - this.PartsFromApi.partsOrQuestions - )) { - for (let [partKey, partValue] of Object.entries(value.parts)) { - const currentPart = - this.PartsFromApi.partsOrQuestions[key].parts[partKey]; - const isMatched = selectedGlassPartNumbers.some( - (p) => p === currentPart.partNumber - ); - - if (isMatched) { - matchedParts.push(currentPart); - } - } - } - // If no parts could be matched, throw an error. - if (matchedParts.length === 0) { + if (this.isForwardActionDisabled) { + this.$refs.funnelFooter.removeLoader(); throw new Error("Could not match any parts to the selected parts"); } // Save parts to the store. - store.commit(storeMutations.UPDATE_GLASS_PARTS, matchedParts); + store.commit(storeMutations.UPDATE_GLASS_PARTS, this.matchedParts); // Navigate to the next page. this.$router.navigateAfterSave( @@ -212,25 +194,14 @@ methods: { }, LoadInitialPartsData() { - const partsData = this.PartsFromApi; - const alreadyPopulatedPartsData = - this.$store.getters.lineItems.glassParts === null - ? {} - : this.$store.getters.lineItems.glassParts; - - partsData.partsOrQuestions.map((g) => { - // If the part is already populated, use the value from the store and populate the v-model. - Object.keys(alreadyPopulatedPartsData).forEach((key) => { - const partNumber = alreadyPopulatedPartsData[key].partNumber; - g.parts.forEach((p) => { - if (p.partNumber === partNumber) { - this.glassParts[g.glassLocation + "-" + g.glassName] = { - [g.glassLocation]: [partNumber], - }; - } - }); - }); - }); + this.alreadyPopulatedPartsData = this.$store.getters.lineItems.glassParts ?? []; + const savedPartNumbers = this.alreadyPopulatedPartsData.map(part => part.partNumber) + this.PartsFromApi.partsOrQuestions.forEach(glass => { + const savedPart = glass.parts.filter(part => savedPartNumbers.includes(part.partNumber))[0]; + if (savedPart) { + this.glassParts[`${glass.glassLocation}-${glass.glassName}`.replace(" ", "-")] = savedPart; + } + }); }, }, mounted() { diff --git a/src/layouts/vehicle-style/style-question/style-question.vue b/src/layouts/vehicle-style/style-question/style-question.vue index 45c0f7dea..e578bc192 100644 --- a/src/layouts/vehicle-style/style-question/style-question.vue +++ b/src/layouts/vehicle-style/style-question/style-question.vue @@ -7,7 +7,7 @@ :answers="styles" groupName="ChooseVehicleStyle" textPosition="text-start" - v-model="selectedValueAsArray" + v-model="selectedValue" isRequired=true /> @@ -34,14 +34,13 @@ export default { questionText(){ return this.getCmsContent(this.cmsWidgetName, 'QuestionText'); }, - selectedValueAsArray: { + selectedValue: { get: function() { const modelValueAsArray = this.modelValue ? [this.modelValue] : []; return modelValueAsArray; }, set: function(newValue) { - const newValueAsScalar = newValue && newValue.length > 0 ? newValue[0] : null; - this.$emit("update:modelValue", newValueAsScalar); + this.$emit("update:modelValue", newValue); } } }, diff --git a/src/layouts/vehicle-year/year-question/year-question.vue b/src/layouts/vehicle-year/year-question/year-question.vue index a5b263fe8..ff2e2902a 100644 --- a/src/layouts/vehicle-year/year-question/year-question.vue +++ b/src/layouts/vehicle-year/year-question/year-question.vue @@ -7,7 +7,7 @@ :answers="years" groupName="ChooseVehicleYear" textPosition="text-start" - v-model="selectedValueAsArray" + v-model="selectedValue" isRequired=true /> @@ -35,14 +35,13 @@ export default { questionText(){ return this.getCmsContent(this.cmsWidgetName, 'QuestionText'); }, - selectedValueAsArray: { + selectedValue: { get: function() { const modelValueAsArray = this.modelValue ? [this.modelValue] : []; return modelValueAsArray; }, set: function(newValue) { - const newValueAsScalar = newValue && newValue.length > 0 ? newValue[0] : null; - this.$emit("update:modelValue", newValueAsScalar); + this.$emit("update:modelValue", newValue); } } }, diff --git a/src/layouts/vin-lookup/vin-lookup.vue b/src/layouts/vin-lookup/vin-lookup.vue index b3ca243f0..214899d31 100644 --- a/src/layouts/vin-lookup/vin-lookup.vue +++ b/src/layouts/vin-lookup/vin-lookup.vue @@ -183,15 +183,10 @@ export default { previouslyEnteredCarId: '', invalidZip: '', vinPopulatedOnPageLoad: this.getVinFromStore()?.length > 0, - isInsuranceVerified: false, }; }, mounted() { this.attachCustomEvents(); - if (this.vinPopulatedOnPageLoad) { - this.setupVinMask(); - this.isInsuranceVerified = store.getters.payment.insuranceCoverage.isVerified || getFunnelCookie().HasDelayedClaimRegistration; - } }, watch: { vin() { @@ -239,12 +234,20 @@ export default { return this.getCmsContent("PerfectMatchInsuranceVerifiedAlert", "BodyText").replaceAll("{custom:damage}", getIsWindshieldOnly()) }, + isInsuranceVerified() { + return store.getters.payment.insuranceCoverage.isVerified || getFunnelCookie().HasDelayedClaimRegistration; + }, + vinMask() { + if (this.vinPopulatedOnPageLoad) { + const lastSixChars = this.vin.substring(11, this.vin.length); + return `!X!X!X!X!X!X!X!X!X!X!X${lastSixChars}`; + } + else { + return 'XXXXXXXXXXXXXXXXX'; + } + }, }, methods: { - setupVinMask() { - const lastSixChars = this.vin.substring(11, this.vin.length); - this.vinMask = `!X!X!X!X!X!X!X!X!X!X!X${lastSixChars}`; - }, arePagePrerequisitesValid() { return store.getters.vehicle.carId !== null; }, diff --git a/src/router/router-constants/fmgPage-values.js b/src/router/router-constants/fmgPage-values.js index 9abb45cd2..d067895ee 100644 --- a/src/router/router-constants/fmgPage-values.js +++ b/src/router/router-constants/fmgPage-values.js @@ -12,6 +12,7 @@ const fmgPageValues = { REVEAL: "reveal", ESTIMATE: "estimate", ADDRESS_VEHICLES: "address-vehicles", + QUOTE: "quote" }; export { fmgPageValues }; diff --git a/src/styles/common-error-styles.scss b/src/styles/common-error-styles.scss index 95985f7c4..25a651d52 100644 --- a/src/styles/common-error-styles.scss +++ b/src/styles/common-error-styles.scss @@ -46,6 +46,7 @@ html { input[type=radio]+label:before, input[type=checkbox]+label:before { border: 1px solid $red; + background-color: initial; } input[type=checkbox]:checked + label:before { border: 1px solid $blue; diff --git a/src/ux-components/list-button-horizontal/list-button-horizontal.vue b/src/ux-components/list-button-horizontal/list-button-horizontal.vue index dfd322cb3..e60d0d22c 100644 --- a/src/ux-components/list-button-horizontal/list-button-horizontal.vue +++ b/src/ux-components/list-button-horizontal/list-button-horizontal.vue @@ -223,18 +223,26 @@ export default { span { font-size: .875rem; } - } + } +} + +.col { &:first-of-type { - label { - border-bottom-left-radius: 0.5rem; - border-top-left-radius: 0.5rem; - z-index: 2; + .list-button-horizontal { + label { + border-bottom-left-radius: 0.5rem; + border-top-left-radius: 0.5rem; + z-index: 2; + } } } + &:last-of-type { - label { - border-bottom-right-radius: 0.5rem; - border-top-right-radius: 0.5rem; + .list-button-horizontal { + label { + border-bottom-right-radius: 0.5rem; + border-top-right-radius: 0.5rem; + } } } } diff --git a/src/ux-components/list-card/list-card.spec.js b/src/ux-components/list-card/list-card.spec.js index 2fbdaade4..7db735ced 100644 --- a/src/ux-components/list-card/list-card.spec.js +++ b/src/ux-components/list-card/list-card.spec.js @@ -205,7 +205,7 @@ describe("list-card.vue", () => { }); wrapper.vm.handleCheckChange(); // Assert - expect(wrapper.emitted()["isCheckedChanged"][0]).toEqual([{value: "List Card Checkbox", checkValue: [Boolean, String], buttonId: 'list-card-id'}]); + expect(wrapper.emitted()["isCheckedChanged"][0]).toEqual([{value: "List Card Checkbox", checkValue: "List Card Checkbox", buttonId: 'list-card-id'}]); }); it("Should set checkValue data if selectedButtonIDs has value(s)", async () => { diff --git a/src/ux-components/list-card/list-card.vue b/src/ux-components/list-card/list-card.vue index 88aad0b6a..84762fc91 100644 --- a/src/ux-components/list-card/list-card.vue +++ b/src/ux-components/list-card/list-card.vue @@ -1,8 +1,9 @@