diff --git a/jest.config.js b/jest.config.js index a6be0c11a..c5309b45d 100644 --- a/jest.config.js +++ b/jest.config.js @@ -18,8 +18,7 @@ module.exports = { "!src/layouts/vehicle-damage/windshield-options/windshield-options.vue", "!src/layouts/part-questions/**/*.vue", "!src/layouts/reveal/**/*.vue", - "!src/layouts/estimate/**/*.vue", - "!src/layouts/address-lookup/customer-questions/address-questions/address-questions.vue", // there are limitations of async script loading and third party scripts that prevent full coverage from being met for this component. + "!src/layouts/estimate/**/*.vue", // TODO REMOVE THESE AFTER WRITING UNIT TESTS "!src/layouts/address-vehicles/address-vehicles.vue", "!src/layouts/address-vehicles/address-vehicles-question/address-vehicles-question.vue", diff --git a/src/helpers/unit-test-helper.js b/src/helpers/unit-test-helper.js index 07632f5e5..f9b4d551b 100644 --- a/src/helpers/unit-test-helper.js +++ b/src/helpers/unit-test-helper.js @@ -55,6 +55,7 @@ export function getMountOptions(mockData) { mocks.$store = mockData.store; mocks.$router = mockData.router; mocks.$route = mockData.route; + mocks.$loadScript = mockData.loadScript; const global = { mocks: mocks, diff --git a/src/layouts/address-lookup/customer-questions/address-questions/address-questions.spec.js b/src/layouts/address-lookup/customer-questions/address-questions/address-questions.spec.js index 11a0aa4ed..fb1154fe6 100644 --- a/src/layouts/address-lookup/customer-questions/address-questions/address-questions.spec.js +++ b/src/layouts/address-lookup/customer-questions/address-questions/address-questions.spec.js @@ -1,130 +1,630 @@ -import { shallowMount } from "@vue/test-utils"; +// Components import addressQuestions from "@/layouts/address-lookup/customer-questions/address-questions/address-questions"; +import alert from "@/ux-components/alert/alert"; -const addressModel = { - streetAddress: "", - city: "", - state: "", - zipCode: "", -} +// Supporting Files +import { mount, shallowMount } from "@vue/test-utils"; +import { getMountOptions } from "@/helpers/unit-test-helper.js"; +import { storeActions } from "@/constants/store-actions"; +import { storeMutations } from "@/constants/store-mutations"; +import store from "@/store"; -describe("addressQuestions.vue", () => { +let autocompleteElement; +describe("address-questions.vue", () => { + beforeEach(() => { + // Create the `addressField1` element (autocomplete's input) + autocompleteElement = document.createElement("input") + autocompleteElement.getPlace = jest.fn(); + document.getElementById = jest.fn().mockReturnValue(autocompleteElement); + }) - it("Should render addressQuestions sub-components (textbox-questions and dropdown-questions)", async () => { - // Arrange - const wrapper = shallowMount(addressQuestions); + describe("initial state", () => { + test("only street address field is shown", () => { + // Arrange + const { wrapper } = setupMocks({}); - // Act - const streetAddress = wrapper.findComponent({ ref: 'autocomplete' }); - const city = wrapper.findComponent({ ref: 'city' }); - const state = wrapper.findComponent({ ref: 'state' }); - const zipCode = wrapper.findComponent({ ref: 'zipCode' }); + // Assert + const streetAddressField = wrapper.findComponent({ ref: "autocomplete" }); + const cityField = wrapper.findComponent({ ref: "city" }); + const stateField = wrapper.findComponent({ ref: "state" }); + const zipCodeField = wrapper.findComponent({ ref: "zipCode" }); - // Assert - expect(streetAddress.exists()).toBe(true); - expect(city.exists()).toBe(true); - expect(state.exists()).toBe(true); - expect(zipCode.exists()).toBe(true); - + expect(streetAddressField.exists()).toBe(true); + expect(streetAddressField.isVisible()).toBe(true); + expect(cityField.exists()).toBe(true); + expect(cityField.isVisible()).toBe(false); + expect(stateField.exists()).toBe(true); + expect(stateField.isVisible()).toBe(false); + expect(zipCodeField.exists()).toBe(true); + expect(zipCodeField.isVisible()).toBe(false); + + const alerts = wrapper.findAllComponents(alert); + expect(alerts.length).toEqual(0); + }) + + test("Should render addressQuestions sub-components (textbox-questions and dropdown-questions)", async () => { + // Arrange + const { wrapper } = setupMocks({}); + + // Act + const streetAddress = wrapper.findComponent({ ref: 'autocomplete' }); + const city = wrapper.findComponent({ ref: 'city' }); + const state = wrapper.findComponent({ ref: 'state' }); + const zipCode = wrapper.findComponent({ ref: 'zipCode' }); + + // Assert + expect(streetAddress.exists()).toBe(true); + expect(city.exists()).toBe(true); + expect(state.exists()).toBe(true); + expect(zipCode.exists()).toBe(true); }); - it("Should call the watch handler for the address model when the addressModel is changed", async () => { - // Arrange - const wrapper = shallowMount(addressQuestions, { - propsData: { - modelValue: addressModel, + test("Should set this.displayNoMatchWarning to false when it is set to true, if the model if prepopulated", async () => { + // Arrange + const newAddressModel = { + streetAddress: "foo", + city: "foo", + state: "foo", + zipCode: "55555", + }; + const wrapper = shallowMount(addressQuestions, { + propsData: { + modelValue: newAddressModel, + }, + }); + + await wrapper.setData({ + displayNoMatchWarning: true + }) + expect(wrapper.vm.displayNoMatchWarning).toBeTruthy(); + + // Act + wrapper.vm.$options.watch.addressModel.handler.call(wrapper.vm, wrapper.vm.addressModel); + + // Assert + expect(wrapper.vm.displayNoMatchWarning).toBeFalsy(); + }); + + test("Should it set this.showAddressFields to true when the model is prepopulated", async () => { + // Arrange + // Act + const newAddressModel = { + streetAddress: "foo", + city: "foo", + state: "foo", + zipCode: "55555", + }; + const wrapper = shallowMount(addressQuestions, { + propsData: { + modelValue: newAddressModel, + }, + }); + + // Act + wrapper.vm.setupAddressLookup(); + + // Assert + expect(wrapper.vm.showAddressFields).toBe(true); + + }); + }); + + describe("happy paths", () => { + test("full street address is passed in => address fields are displayed", async () => { + // Arrange/Act + const { wrapper } = setupMocks({ + props: { + modelValue: { + streetAddress: "12345 Test Road", + city: "Tests", + state: "OH", + zipCode: "12312" + } + } + }); + + await wrapper.vm.$nextTick(); + + // Assert + const cityField = wrapper.findComponent({ ref: "city" }); + const stateField = wrapper.findComponent({ ref: "state" }); + const zipField = wrapper.findComponent({ ref: "zipCode" }); + expect(cityField.exists()).toBeTruthy(); + expect(cityField.isVisible()).toBeTruthy(); + expect(stateField.exists()).toBeTruthy(); + expect(cityField.isVisible()).toBeTruthy(); + expect(zipField.exists()).toBeTruthy(); + expect(cityField.isVisible()).toBeTruthy(); + }); + + test("full street address is passed in => don't load Google Autocomplete script", async () => { + // Arrange/Act + const { wrapper } = setupMocks({ + props: { + modelValue: { + streetAddress: "12345 Test Road", + city: "Tests", + state: "OH", + zipCode: "12312" + } + } + }); + + await wrapper.vm.$nextTick(); + + // Assert + expect(wrapper.vm.$loadScript).not.toHaveBeenCalled(); + }); + + test("address field is focused => disable autocomplete", async () => { + // Arrange + let focusEventCallbackFunction; + autocompleteElement.addEventListener = jest.fn().mockImplementation((eventName, callbackFunction) => { + if (eventName == "focus") { + focusEventCallbackFunction = callbackFunction; + } + }); + const { wrapper } = setupMocks({}); + await wrapper.vm.$nextTick(); + + // Act + focusEventCallbackFunction(); + await wrapper.vm.$nextTick(); + + // Assert + expect(autocompleteElement.getAttribute("autocomplete")).toEqual("do-not-autofill"); + }); + + test("street address is entered, user chooses good result from autocomplete results => other fields are filled in", async () => { + // Arrange + const { wrapper } = setupMocks({}); + await wrapper.setData({ + addressModel: { + streetAddress: "123 Test Street" + } + }) + + const selectedPlace = { + address_components: [ + { + long_name: "1234", + short_name: "1234", + types: ["street_number"] + }, + { + long_name: "Test Road", + short_name: "Test Road", + types: ["route"] + }, + { + long_name: "East Columbus", + short_name: "Columbus", + types: ["neighborhood", "political"] + }, + { + long_name: "Columbus", + short_name: "Columbus", + types: ["locality", "political"] + }, + { + long_name: "Franklin County", + short_name: "Franklin County", + types: ["administrative_area_level_2", "political"] + }, + { + long_name: "Ohio", + short_name: "OH", + types: ["administrative_area_level_1", "political"] + }, + { + long_name: "United States", + short_name: "US", + types: ["country", "political"] + }, + { + long_name: "43215", + short_name: "43215", + types: ["postal_code"] + }, + ] + } + + // Act + autocompleteElement.dispatchEvent(new CustomEvent("place_changed", { detail: selectedPlace })); + + // Assert + const addressModel = wrapper.vm.addressModel; + expect(addressModel.streetAddress).toEqual("1234 Test Road"); + expect(addressModel.city).toEqual("Columbus"); + expect(addressModel.state).toEqual("OH"); + expect(addressModel.zipCode).toEqual("43215"); + }) + + test("street address is entered, user chooses good result from autocomplete results => alerts are cleared", async () => { + // Arrange + const { wrapper } = setupMocks({}); + await wrapper.setData({ + addressModel: { + streetAddress: "123 Test Street" + }, + displayVerificationWarning: true, + displayNoMatchWarning: true + }) + + let alerts = wrapper.findAllComponents(alert); + alerts.forEach(alert => expect(alert.isVisible()).toBeTruthy()); + + const selectedPlace = { + address_components: [ + { + long_name: "1234", + short_name: "1234", + types: ["street_number"] + }, + { + long_name: "Test Road", + short_name: "Test Road", + types: ["route"] + }, + { + long_name: "East Columbus", + short_name: "Columbus", + types: ["neighborhood", "political"] + }, + { + long_name: "Columbus", + short_name: "Columbus", + types: ["locality", "political"] + }, + { + long_name: "Franklin County", + short_name: "Franklin County", + types: ["administrative_area_level_2", "political"] + }, + { + long_name: "Ohio", + short_name: "OH", + types: ["administrative_area_level_1", "political"] + }, + { + long_name: "United States", + short_name: "US", + types: ["country", "political"] + }, + { + long_name: "43215", + short_name: "43215", + types: ["postal_code"] + }, + ] + } + + // Act + autocompleteElement.dispatchEvent(new CustomEvent("place_changed", { detail: selectedPlace })); + await wrapper.vm.$nextTick(); + + // Assert + alerts = wrapper.findAllComponents(alert); + alerts.forEach(alert => expect(alert.exists()).toBeFalsy()); + }); + + test("street address is entered, but user clicks away => first result is selected and other fields are filled in", async () => { + // Arrange + let changeEventCallbackFunction; + autocompleteElement.addEventListener = jest.fn().mockImplementation((eventName, callbackFunction) => { + if (eventName == "change") { + changeEventCallbackFunction = callbackFunction; + } + }); + + const { wrapper } = setupMocks({ + querySelectorFunction: function (query) { + if (query == ".pac-container .pac-item") { + let element = document.createElement("div"); + element.textContent = "123 Test Street" + return element; + } + }, + geocoderResult: { + address_components: [ + { + long_name: "1234", + short_name: "1234", + types: ["street_number"] }, + { + long_name: "Test Road", + short_name: "Test Road", + types: ["route"] + }, + { + long_name: "East Columbus", + short_name: "Columbus", + types: ["neighborhood", "political"] + }, + { + long_name: "Columbus", + short_name: "Columbus", + types: ["locality", "political"] + }, + { + long_name: "Franklin County", + short_name: "Franklin County", + types: ["administrative_area_level_2", "political"] + }, + { + long_name: "Ohio", + short_name: "OH", + types: ["administrative_area_level_1", "political"] + }, + { + long_name: "United States", + short_name: "US", + types: ["country", "political"] + }, + { + long_name: "43215", + short_name: "43215", + types: ["postal_code"] + }, + ] + } + }); + + let noMatchAlert = wrapper.findComponent({ ref: "alertNoMatchWarning" }); + let verificationAlert = wrapper.findComponent({ ref: "alertVerificationWarning" }); + expect(noMatchAlert.exists()).toBeFalsy(); + expect(verificationAlert.exists()).toBeFalsy(); + + await wrapper.vm.$nextTick(); + + // Act + changeEventCallbackFunction(); + + await wrapper.vm.$nextTick(); + + // Assert + const addressModel = wrapper.vm.addressModel; + expect(addressModel.streetAddress).toEqual("1234 Test Road"); + expect(addressModel.city).toEqual("Columbus"); + expect(addressModel.state).toEqual("OH"); + expect(addressModel.zipCode).toEqual("43215"); + }); + }); + + describe("alerts", () => { + const places = [null, { address_components: null }, undefined, {}]; + test.each(places)("selected place/place properties is null => display verification alert", async (place) => { + // Arrange + const { wrapper } = setupMocks({}); + await wrapper.setData({ + addressModel: { + streetAddress: "123 Test Street" + }, + displayVerificationWarning: true, + displayNoMatchWarning: true + }) + + let alerts = wrapper.findAllComponents(alert); + alerts.forEach(alert => expect(alert.isVisible()).toBeTruthy()); + + const selectedPlace = place; + + // Act + autocompleteElement.dispatchEvent(new CustomEvent("place_changed", { detail: selectedPlace })); + await wrapper.vm.$nextTick(); + + // Assert + const verificationAlert = wrapper.findComponent({ ref: "alertVerificationWarning" }); + expect(verificationAlert.exists()).toBe(true); + expect(verificationAlert.isVisible()).toBe(true); + const noMatchAlert = wrapper.findComponent({ ref: "alertNoMatchWarning" }); + expect(noMatchAlert.exists()).toBe(false); + }); + + test("user enters address that yields no autocomplete results => show noMatch alert", async () => { + // Arrange + let changeEventCallbackFunction; + autocompleteElement.addEventListener = jest.fn().mockImplementation((eventName, callbackFunction) => { + if (eventName == "change") { + changeEventCallbackFunction = callbackFunction; + } + }); + + const { wrapper } = setupMocks({}); + + let noMatchAlert = wrapper.findComponent({ ref: "alertNoMatchWarning" }); + expect(noMatchAlert.exists()).toBeFalsy(); + + await wrapper.vm.$nextTick(); + + // Act + changeEventCallbackFunction(); + + await wrapper.vm.$nextTick(); + + // Assert + expect(wrapper.vm.displayNoMatchWarning).toBeTruthy(); + noMatchAlert = wrapper.findComponent({ ref: "alertNoMatchWarning" }); + expect(noMatchAlert.exists()).toBeTruthy(); + expect(noMatchAlert.isVisible()).toBeTruthy(); + }); + + test("user enters address that yields autocomplete results, but doesn't select => show verification alert", async () => { + // Arrange + let changeEventCallbackFunction; + autocompleteElement.addEventListener = jest.fn().mockImplementation((eventName, callbackFunction) => { + if (eventName == "change") { + changeEventCallbackFunction = callbackFunction; + } + }); + + const { wrapper } = setupMocks({ + querySelectorFunction: function (query) { + if (query == ".pac-container .pac-item") { + let element = document.createElement("div"); + element.textContent = "123 Test Street" + return element; + } + } + }); + + let noMatchAlert = wrapper.findComponent({ ref: "alertNoMatchWarning" }); + let verificationAlert = wrapper.findComponent({ ref: "alertVerificationWarning" }); + expect(noMatchAlert.exists()).toBeFalsy(); + expect(verificationAlert.exists()).toBeFalsy(); + + await wrapper.vm.$nextTick(); + + // Act + changeEventCallbackFunction(); + + await wrapper.vm.$nextTick(); + + // Assert + verificationAlert = wrapper.findComponent({ ref: "alertVerificationWarning" }); + expect(wrapper.vm.displayVerificationWarning).toBeTruthy(); + expect(verificationAlert.exists()).toBeTruthy(); + expect(verificationAlert.isVisible()).toBeTruthy(); + noMatchAlert = wrapper.findComponent({ ref: "alertNoMatchWarning" }); + expect(wrapper.vm.displayNoMatchWarning).toBeFalsy(); + expect(noMatchAlert.exists()).toBeFalsy(); + }); + + describe("noMatch alert is cleared on address change", () => { + test("user sees noMatch warning and enters city => noMatch warning is removed", async () => { + // Arrange + const { wrapper } = setupMocks({}); + + wrapper.setData({ + displayNoMatchWarning: true }); - wrapper.vm.displayNoMatchWarning = true; + await wrapper.vm.$nextTick(); + + let noMatchAlert = wrapper.findComponent({ ref: "alertNoMatchWarning" }); + expect(noMatchAlert.exists()).toBeTruthy(); + expect(noMatchAlert.isVisible()).toBeTruthy(); // Act - const newAddressModel = { - streetAddress: "foo", - city: "foo", - state: "foo", - zipCode: "55555", - }; - - wrapper.vm.$options.watch.addressModel.handler.call(wrapper.vm, newAddressModel); + wrapper.vm.$options.watch.addressModel.handler.call(wrapper.vm, { + city: "Somewhere" + }) + await wrapper.vm.$nextTick(); // Assert - expect(wrapper.vm.addressModel.handler).toHaveBeenCalled; + expect(wrapper.vm.displayNoMatchWarning).toBeFalsy(); + noMatchAlert = wrapper.findComponent({ ref: "alertNoMatchWarning" }); + expect(noMatchAlert.exists()).toBeFalsy(); + }); - }); + test("user sees noMatch warning and enters state => noMatch warning is removed", async () => { + // Arrange + const { wrapper } = setupMocks({}); - it("Should set this.displayNoMatchWarning to false, if the model changes when it is set to true", async () => { - // Arrange - const wrapper = shallowMount(addressQuestions, { - propsData: { - modelValue: addressModel, - }, + wrapper.setData({ + displayNoMatchWarning: true }); - wrapper.vm.displayNoMatchWarning = true; - - // Act - const newAddressModel = { - streetAddress: "foo", - city: "", - state: "", - zipCode: "", - }; + await wrapper.vm.$nextTick(); + + let noMatchAlert = wrapper.findComponent({ ref: "alertNoMatchWarning" }); + expect(noMatchAlert.exists()).toBeTruthy(); + expect(noMatchAlert.isVisible()).toBeTruthy(); // Act - wrapper.vm.$options.watch.addressModel.handler.call(wrapper.vm, newAddressModel); + wrapper.vm.$options.watch.addressModel.handler.call(wrapper.vm, { + state: "KO" + }) + await wrapper.vm.$nextTick(); // Assert - expect(wrapper.vm.displayNoMatchWarning === false); + expect(wrapper.vm.displayNoMatchWarning).toBeFalsy(); + noMatchAlert = wrapper.findComponent({ ref: "alertNoMatchWarning" }); + expect(noMatchAlert.exists()).toBeFalsy(); + }); - }); - - it("Should it not should set this.displayNoMatchWarning to false when it is set to true, if the model if prepopulated", async () => { - // Arrange - const wrapper = shallowMount(addressQuestions, { - propsData: { - modelValue: addressModel, - }, + test("user sees noMatch warning and enters zip code => noMatch warning is removed", async () => { + // Arrange + const { wrapper } = setupMocks({}); + + wrapper.setData({ + displayNoMatchWarning: true }); - wrapper.vm.displayNoMatchWarning = true; - - // Act - const newAddressModel = { - streetAddress: "foo", - city: "foo", - state: "foo", - zipCode: "55555", - }; + await wrapper.vm.$nextTick(); + + let noMatchAlert = wrapper.findComponent({ ref: "alertNoMatchWarning" }); + expect(noMatchAlert.exists()).toBeTruthy(); + expect(noMatchAlert.isVisible()).toBeTruthy(); // Act - wrapper.vm.$options.watch.addressModel.handler.call(wrapper.vm, newAddressModel); + wrapper.vm.$options.watch.addressModel.handler.call(wrapper.vm, { + zipCode: "12345" + }) + await wrapper.vm.$nextTick(); // Assert - expect(wrapper.vm.displayNoMatchWarning === true); + expect(wrapper.vm.displayNoMatchWarning).toBeFalsy(); + noMatchAlert = wrapper.findComponent({ ref: "alertNoMatchWarning" }); + expect(noMatchAlert.exists()).toBeFalsy(); + }); + }); + }); +}); - }); +function setupMocks({ mountOptions, props, isShallowMount = true, querySelectorFunction, geocoderResult = ["1234 Test Street"] }) { + store.commit(storeMutations.RESET_STATE); - it("Should it set this.showAddressFields to true when the model is prepopulated", async () => { - // Arrange - const wrapper = shallowMount(addressQuestions, { - propsData: { - modelValue: addressModel, - }, - }); - wrapper.vm.displayNoMatchWarning = true; - - // Act - const newAddressModel = { - streetAddress: "foo", - city: "foo", - state: "foo", - zipCode: "55555", - }; + const resultingMountOptions = getMountOptions({ + ...mountOptions, + router: { + navigate: jest.fn(), + navigateAfterSave: jest.fn() + }, + loadScript: jest.fn().mockResolvedValue() + }); - // Act - wrapper.vm.setupAddressLookup(); + window.google = { + maps: { + event: { + addListener: jest.fn().mockImplementation((element, eventName, callbackFunction) => { + function interceptedCallbackFunction(e) { + callbackFunction(e.detail); + } + // selectedPlace = "Woogly"; + element.addEventListener(eventName, interceptedCallbackFunction); + }), + removeListener: jest.fn(), + clearInstanceListeners: jest.fn() + }, + places: { + Autocomplete: jest.fn().mockImplementation((el) => el) + }, + Geocoder: class Geocoder { + // constructor(); - // Assert - expect(wrapper.vm.showAddressFields).toBe(true); + geocode(request, callback) { + callback([geocoderResult], true) + } + }, + GeocoderStatus: { + OK: true + } + } + }; - }); + if (props) + resultingMountOptions.propsData = props; -}) \ No newline at end of file + const wrapper = isShallowMount ? shallowMount(addressQuestions, resultingMountOptions) : mount(addressQuestions, resultingMountOptions); + document.querySelector = jest.fn().mockImplementation(query => { + let result = null; + if (query == ".pac-container") + result = document.createElement("div"); + else if (querySelectorFunction) { + result = querySelectorFunction(query); + } + + return result ?? null; + }); + + return { wrapper }; +} \ No newline at end of file diff --git a/src/layouts/address-lookup/customer-questions/address-questions/address-questions.vue b/src/layouts/address-lookup/customer-questions/address-questions/address-questions.vue index 61062ba47..d93c2559b 100644 --- a/src/layouts/address-lookup/customer-questions/address-questions/address-questions.vue +++ b/src/layouts/address-lookup/customer-questions/address-questions/address-questions.vue @@ -185,11 +185,10 @@ export default ({ }, methods: { setupAddressLookup() { - - if (this.addressModel.streetAddress !== null & - this.addressModel.city !== null & - this.addressModel.state !== null & - this.addressModel.zipCode !== null) { + if (this.addressModel.streetAddress && + this.addressModel.city && + this.addressModel.state && + this.addressModel.zipCode) { this.showAddressFields = true; return; @@ -228,10 +227,9 @@ export default ({ if (autocompleteResultsContainer) { streetAddressField.appendChild(autocompleteResultsContainer); } - }) - addressField1.onchange = function() { + addressField1.addEventListener("change", () => { const hover = document.querySelector(".pac-container .pac-item:hover"); // if an item has been clicked, do nothing, otherwise get first solution and use Geocoder to get the place if (hover === null) { @@ -258,7 +256,7 @@ export default ({ self.displayNoMatchWarning = true; } } - }; + }); function fillInAddress(place) { if (!place) { @@ -310,7 +308,6 @@ export default ({ addressField1.onchange = null; const pacContainer = document.querySelector(".pac-container"); pacContainer.remove(); - } }) .catch(() => { @@ -325,16 +322,10 @@ export default ({ watch: { addressModel: { handler(newValue) { - // The first time the address model changes is when the page first loads - if (!newValue.city && - !newValue.state && - !newValue.zipCode) { - return; - } - - if (this.displayNoMatchWarning === true) { + // Clear no match warning on address change + if (newValue.city || newValue.state || newValue.zipCode) { this.displayNoMatchWarning = false; - } + } }, deep: true }