diff --git a/azure-pipelines.yml b/azure-pipelines.yml index c8184f93f..207c013be 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -26,7 +26,7 @@ resources: type: github name: Safelite/AzureDevOps endpoint: Safelite - ref: refs/tags/t5.4.0 + ref: refs/tags/t5.5.19 variables: - group: Digital-Infrastructure @@ -67,9 +67,10 @@ stages: - template: templates/digital/step-build-vue.yml@AzureDevOps parameters: buildOutputDir: dist + environment: Dev - template: templates/digital/step-deploy-vue.yml@AzureDevOps parameters: - artifactName: vueDist + artifactName: vueDistDev awsProfile: $(devDeploymentProfile) outputPath: /fmg/ deployBuckets: @@ -109,9 +110,10 @@ stages: - template: templates/digital/step-build-vue.yml@AzureDevOps parameters: buildOutputDir: dist + environment: Qa - template: templates/digital/step-deploy-vue.yml@AzureDevOps parameters: - artifactName: vueDist + artifactName: vueDistQa awsProfile: $(qaDeploymentProfile) outputPath: /fmg/ deployBuckets: @@ -131,7 +133,7 @@ stages: # Prod Build/Deploy - stage: Prod - condition: eq(variables['Build.SourceBranch'], variables['prod-branch'] ) + condition: succeeded('Qa') variables: - group: FixMyGlassProd jobs: @@ -150,9 +152,10 @@ stages: - template: templates/digital/step-build-vue.yml@AzureDevOps parameters: buildOutputDir: dist + environment: Prod - template: templates/digital/step-deploy-vue.yml@AzureDevOps parameters: - artifactName: vueDist + artifactName: vueDistProd awsProfile: $(prodDeploymentProfile) outputPath: /fmg/ deployBuckets: @@ -168,4 +171,8 @@ stages: indexDeployVariables: __VUE_APP_GOOGLE_TAG_MANAGER_SCRIPT_BODY__: $(__VUE_APP_GOOGLE_TAG_MANAGER_SCRIPT_BODY__) __VUE_APP_GOOGLE_TAG_MANAGER_NOSCRIPT_FRAME_SRC__: $(__VUE_APP_GOOGLE_TAG_MANAGER_NOSCRIPT_FRAME_SRC__) - cfDistributionId: $(cfDistributionId) \ No newline at end of file + cfDistributionId: $(cfDistributionId) + - template: templates/digital/auto-tag.yml@AzureDevOps + parameters: + userName: SafeliteAzureDevops + userEmail: githubazuredevops@safelite.com \ No newline at end of file diff --git a/jest.config.js b/jest.config.js index 839db4378..c5309b45d 100644 --- a/jest.config.js +++ b/jest.config.js @@ -18,15 +18,10 @@ module.exports = { "!src/layouts/vehicle-damage/windshield-options/windshield-options.vue", "!src/layouts/part-questions/**/*.vue", "!src/layouts/reveal/**/*.vue", - "!src/layouts/estimate/**/*.vue", - // TODO REMOVE THESE AFTER WRITING UNIT TESTS - "!src/layouts/address-lookup/address-lookup.vue", - "!src/layouts/address-lookup/customer-questions/customer-questions.vue", - "!src/layouts/address-lookup/customer-questions/address-questions/address-questions.vue", + "!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", - "!src/common-components/dropdown-question/dropdown-question.vue", - "!src/common-components/textbox-question/textbox-question.vue", "!src/ux-components/alert\alert.vue", "!src/helpers/validation-rules.js", // END diff --git a/package.json b/package.json index ec3d65d79..304fc2afd 100644 --- a/package.json +++ b/package.json @@ -6,6 +6,7 @@ "serve": "vue-cli-service serve", "build": "vue-cli-service build", "test:unit": "vue-cli-service test:unit --coverage --ci", + "test:unit:lite": "vue-cli-service test:unit --ci", "lint": "vue-cli-service lint" }, "dependencies": { diff --git a/src/common-components/dropdown-question/dropdown-question.spec.js b/src/common-components/dropdown-question/dropdown-question.spec.js new file mode 100644 index 000000000..381f34890 --- /dev/null +++ b/src/common-components/dropdown-question/dropdown-question.spec.js @@ -0,0 +1,170 @@ +import { shallowMount } from "@vue/test-utils"; +import dropdownQuestion from "./dropdown-question"; + +// Mock CMS content +const questionText = "Question Text"; +const mockMixin = { + methods: { + getCmsContent: jest.fn().mockImplementation(()=> { + return questionText; + }) + } +} + +// TODO: Remove the following from dropdown-question.vue -> :class="(errors && errors.length) || hasError ? 'has-error' : ''" +// It is not being used. +describe("dropdownQuestion.vue", () => { + + it("Should render a select input", async () => { + + // Arrange + const wrapper = shallowMount(dropdownQuestion, { + propsData: { + options: {}, + }, + mixins: [mockMixin] + }); + + wrapper.getCmsContent = jest.fn(); + + // Act + const select = wrapper.find("select"); + + // Assert + expect(select.exists()).toBe(true); + + }); + + it("Should render the 'questionText' data value as the label text.", async () => { + // Arrange + const wrapper = shallowMount(dropdownQuestion, { + propsData: { + options: {}, + }, + mixins: [mockMixin] + }); + + // Act + const label = wrapper.find("label"); + + // Assert + 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(dropdownQuestion, { + propsData: { + options: {}, + 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 select field", async () => { + // Arrange + const wrapper = shallowMount(dropdownQuestion, { + propsData: { + inputId: "input ID", + options: {}, + }, + mixins: [mockMixin] + }); + + // Act + const select = wrapper.find("select"); + + // Assert + expect(select.attributes().id).toEqual("input ID"); + + }); + + it("Should render the 'questionText' data value as the aria-label attribute.", async () => { + // Arrange + const wrapper = shallowMount(dropdownQuestion, { + propsData: { + options: {}, + }, + mixins: [mockMixin] + }); + + // Act + const label = wrapper.find("label"); + + // Assert + expect(label.attributes("aria-label")).toContain(questionText); + + }); + + it("Should return aria-disabled state as disabled", async () => { + // Arrange + const wrapper = shallowMount(dropdownQuestion, { + propsData: { + options: {}, + isDisabled: true, + }, + mixins: [mockMixin] + }); + + // Act + const select = wrapper.find("select"); + + // Assert + expect(select.attributes("aria-disabled")).toEqual("true"); + + }); + + it("Should emit new value when modelValue is changed", async () => { + // Arrange + const wrapper = shallowMount(dropdownQuestion, { + propsData: { + options: {}, + modelValue: "val", + }, + mixins: [mockMixin] + }); + + // Act + await wrapper.find("select").setValue("val2"); + + // Assert + expect(wrapper.emitted()).toHaveProperty('change') + + }); + + it("Should call this.handleChange with new value when selectedOption is changed", async () => { + // Arrange + const wrapper = shallowMount(dropdownQuestion, { + propsData: { + options: {}, + modelValue: 0, + }, + mixins: [mockMixin] + }); + + wrapper.vm.handleChange = jest.fn().mockImplementation(() => {}); + + // Act + wrapper.vm.$options.watch.selectedOption.call(wrapper.vm, 1); + + // Assert + expect(wrapper.vm.handleChange).toHaveBeenCalled; + + }); + +}); diff --git a/src/common-components/dropdown-question/dropdown-question.spec.js1 b/src/common-components/dropdown-question/dropdown-question.spec.js1 deleted file mode 100644 index 81669f714..000000000 --- a/src/common-components/dropdown-question/dropdown-question.spec.js1 +++ /dev/null @@ -1,131 +0,0 @@ -import { shallowMount } from "@vue/test-utils"; -import dropdownQuestion from "./dropdown-question"; -import { nextTick } from "vue"; -import { maska } from 'maska'; - -describe("dropdownQuestion.vue", () => { - - it("Should return aria-disabled state", async () => { - // Act - const wrapper = shallowMount(dropdownQuestion, { - propsData: { - isDisabled: true, - }, - }); - - // Assert - const input = wrapper.find("select"); - - // Expect - expect(input.attributes()["aria-disabled"]).toEqual("true"); - }); - - it("Should render a select input", async () => { - // Act - const wrapper = shallowMount(dropdownQuestion, { - propsData: { - name: "test", - label: "unit test label", - }, - }); - - // Assert - const input = wrapper.find("select"); - - expect(input.exists()).toBe(true); - }); - - it("Should return input id", async () => { - // Act - const wrapper = shallowMount(dropdownQuestion, { - propsData: { - inputId: "input ID", - }, - }); - - // Assert - const input = wrapper.find("select"); - - // Expect - expect(input.attributes().id).toEqual("input ID"); - }); - - it("Should render the 'questionText' data value as the label text when disableAutoFill is false.", async () => { - // Arrange - //Mock CMS content - const questionText = "Question Text"; - const cmsContent = { - QuestionText: questionText, - }; - - // Act - const wrapper = shallowMount(dropdownQuestion, { - global: { - directives: { - maska: maska, - } - }, - }); - await wrapper.setData({ - questionText: questionText, - }); - wrapper.vm.initializeComponent(cmsContent); - - // Assert - expect(wrapper.find("label").text()).toContain(questionText); - wrapper.unmount(); - }); - - it("Should render the 'questionText' data value with '⁠' after the first character as the label text when disableAutoFill is true.", async () => { - // Arrange - //Mock CMS content - const originalQuestionText = "Question Text"; - // 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 it and paste it into Google. Then inspect the search field element in Dev Tools, - // you will see "Q&uestion Text" - const expectedQuestionText = "Q⁠uestion Text"; - const cmsContent = { - QuestionText: originalQuestionText, - }; - - // Act - const wrapper = shallowMount(dropdownQuestion, { - global: { - directives: { - maska: maska, - } - }, - propsData: { - disableAutoFill: true, - }, - }); - await wrapper.setData({ - questionText: originalQuestionText, - }); - wrapper.vm.initializeComponent(cmsContent); - - // Assert - expect(wrapper.vm.$el.children[0].innerHTML).toBe(expectedQuestionText); - wrapper.unmount(); - }); - - it("Should emit new value when modelValue is changed", async () => { - // Act - const wrapper = shallowMount(dropdownQuestion, { - global: { - directives: { - maska: maska, - } - }, - propsData: { - modelValue: "val", - }, - }); - - await wrapper.find("select").setValue("val2"); - - // Assert - expect(wrapper.emitted()).toHaveProperty('change') - - }); -}); diff --git a/src/common-components/funnel-footer/funnel-footer.spec.js b/src/common-components/funnel-footer/funnel-footer.spec.js index 198fa5657..d246b602b 100644 --- a/src/common-components/funnel-footer/funnel-footer.spec.js +++ b/src/common-components/funnel-footer/funnel-footer.spec.js @@ -3,30 +3,6 @@ import funnelFooter from "./funnel-footer"; describe("funnel-footer.vue", () => { - it("Should return footer-link class", async () => { - // Act - const r = { - test:"testing", - clientHeight: 10, - offsetHeight: 24, - }; - - global.document.querySelector = jest.fn().mockImplementation(()=> { - return r; - }); - - const wrapper = mount(funnelFooter, { - mixins: [mockMixin] - }); - - // Assert - const link = wrapper.find("a"); - - // Expect - expect(link.attributes('class')).toContain("footer"); - - }); - it("Should emit ForwardClicked on button click", async () => { // Act const wrapper = mount(funnelFooter, { @@ -53,7 +29,7 @@ describe("funnel-footer.vue", () => { mixins: [mockMixin] }); wrapper.vm.updateButtonText('newText'); - + // Assert expect(wrapper.componentVM.customButtontext).toBe('newText'); }); @@ -65,4 +41,4 @@ const mockMixin = { getCmsContent: jest.fn(), getFooterInfoBoxHeight: jest.fn(()=>80) } -} \ No newline at end of file +} diff --git a/src/common-components/funnel-footer/funnel-footer.vue b/src/common-components/funnel-footer/funnel-footer.vue index 27ee88cb3..2c4152cfa 100644 --- a/src/common-components/funnel-footer/funnel-footer.vue +++ b/src/common-components/funnel-footer/funnel-footer.vue @@ -1,21 +1,7 @@ + + diff --git a/src/common-components/loading-modal/loading-modal.vue b/src/common-components/loading-modal/loading-modal.vue index a3cbfa280..2bcdc9c31 100644 --- a/src/common-components/loading-modal/loading-modal.vue +++ b/src/common-components/loading-modal/loading-modal.vue @@ -3,7 +3,7 @@
-
+
Loading...
@@ -12,17 +12,7 @@
-

Finishing up - . - . - . -

-

Nearly there - . - . - . -

-

Generating your quote +

Assessing your damage . . . @@ -32,7 +22,17 @@ . .

-

Assessing your damage +

Generating your quote + . + . + . +

+

Nearly there + . + . + . +

+

Finishing up . . . @@ -77,7 +77,7 @@ export default { left: 0; right: 0; bottom: 0; - z-index: 1055; + z-index: 1057; width: 100%; height: 100%; overflow-x: hidden; @@ -130,39 +130,39 @@ export default { p { margin: 0 175px; - transform: translateX(-2346px); + transform: translateX(180px); } @keyframes slide-in { 0% { - transform: translateX(-2346px) + transform: translateX(180px) } 11% { - transform: translateX(-2010px) + transform: translateX(-80px) } 22% { - transform: translateX(-2010px) + transform: translateX(-80px) } 33% { - transform: translateX(-1490px) + transform: translateX(-600px) } 44% { - transform: translateX(-1490px) + transform: translateX(-600px) } 55% { - transform: translateX(-985px) + transform: translateX(-1110px) } 66% { - transform: translateX(-985px) + transform: translateX(-1110px) } 77% { - transform: translateX(-490px) + transform: translateX(-1600px) } 88% { - transform: translateX(-490px) + transform: translateX(-1600px) } 100% { - transform: translateX(-40px) + transform: translateX(-2050px) } } diff --git a/src/common-components/menu-button/menu-button.vue b/src/common-components/menu-button/menu-button.vue deleted file mode 100644 index c7b798f9a..000000000 --- a/src/common-components/menu-button/menu-button.vue +++ /dev/null @@ -1,53 +0,0 @@ - - - - - diff --git a/src/common-components/textbox-question/textbox-question.spec.js1 b/src/common-components/textbox-question/textbox-question.spec.js similarity index 55% rename from src/common-components/textbox-question/textbox-question.spec.js1 rename to src/common-components/textbox-question/textbox-question.spec.js index 63214c126..09f929490 100644 --- a/src/common-components/textbox-question/textbox-question.spec.js1 +++ b/src/common-components/textbox-question/textbox-question.spec.js @@ -1,139 +1,61 @@ import { shallowMount } from "@vue/test-utils"; import textboxQuestion from "./textbox-question"; -import { maska } from 'maska'; + +// Mock CMS content +const questionText = "Question Text"; +const mockMixin = { + methods: { + getCmsContent: jest.fn().mockImplementation(()=> { + return questionText; + }) + } +} +const maska = jest.fn(); describe("textboxQuestion.vue", () => { - it("Should return aria-disabled state", async () => { - // Act - const wrapper = shallowMount(textboxQuestion, { - global: { - directives: { - maska: maska, - } - }, - propsData: { - isDisabled: true, - }, - }); - - // Assert - const input = wrapper.find("input"); - - // Expect - expect(input.attributes()["aria-disabled"]).toEqual("true"); - - }); - it("Should render a text input", async () => { - // Act + // Arrange const wrapper = shallowMount(textboxQuestion, { global: { directives: { maska: maska, } }, - propsData: { - name: "test", - label: "unit test label", - }, + mixins: [mockMixin] }); - // Assert + wrapper.getCmsContent = jest.fn(); + + // Act const input = wrapper.find("input"); + // Assert expect(input.exists()).toBe(true); }); - it("Should return input id", async () => { - // Act - const wrapper = shallowMount(textboxQuestion, { - global: { - directives: { - maska: maska, - } - }, - propsData: { - inputId: "input ID", - }, - }); - - // Assert - const input = wrapper.find("input"); - - // Expect - expect(input.attributes().id).toEqual("input ID"); - - }); - - it("Should render the 'questionText' data value as the aria-label attribute.", async () => { - // Arrange - //Mock CMS content - const questionText = "Question Text"; - const cmsContent = { - QuestionText: questionText, - }; - - // Act - const wrapper = shallowMount(textboxQuestion, { - global: { - directives: { - maska: maska, - } - }, - }); - await wrapper.setData({ - questionText: questionText, - }); - wrapper.vm.initializeComponent(cmsContent); - - // Assert - expect(wrapper.find("label").attributes('aria-label')).toBe(questionText); - wrapper.unmount(); - - }); - it("Should render the 'questionText' data value as the label text when disableAutoFill is false.", async () => { // Arrange - //Mock CMS content - const questionText = "Question Text"; - const cmsContent = { - QuestionText: questionText, - }; - - // Act const wrapper = shallowMount(textboxQuestion, { global: { directives: { maska: maska, } }, + mixins: [mockMixin] }); - await wrapper.setData({ - questionText: questionText, - }); - wrapper.vm.initializeComponent(cmsContent); - - // Assert - expect(wrapper.find("label").text()).toContain(questionText); - wrapper.unmount(); - - }); - - it("Should render the 'questionText' data value with '⁠' after the first character as the label text when disableAutoFill is true.", async () => { - // Arrange - //Mock CMS content - const originalQuestionText = "Question Text"; - // 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 it and paste it into Google. Then inspect the search field element in Dev Tools, - // you will see "Q&uestion Text" - const expectedQuestionText = "Q⁠uestion Text"; - const cmsContent = { - QuestionText: originalQuestionText, - }; // Act + const label = wrapper.find("label"); + + // Assert + 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: { @@ -143,15 +65,84 @@ describe("textboxQuestion.vue", () => { propsData: { disableAutoFill: true, }, + mixins: [mockMixin] }); - await wrapper.setData({ - questionText: originalQuestionText, - }); - wrapper.vm.initializeComponent(cmsContent); + + // 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(wrapper.vm.$el.children[0].innerHTML).toBe(expectedQuestionText); - wrapper.unmount(); + expect(label.text()).toContain(expectedQuestionText); + + }); + + it("Should return input id as the id of the input field", async () => { + // Arrange + const wrapper = shallowMount(textboxQuestion, { + global: { + directives: { + maska: maska, + } + }, + propsData: { + inputId: "input ID", + }, + mixins: [mockMixin] + }); + + // Act + const input = wrapper.find("input"); + + // Assert + expect(input.attributes().id).toEqual("input ID"); + + }); + + it("Should render the 'questionText' data value as the aria-label attribute.", async () => { + + // Arrange + const wrapper = shallowMount(textboxQuestion, { + global: { + directives: { + maska: maska, + } + }, + mixins: [mockMixin] + }); + + // Act + const label = wrapper.find("label"); + + // Assert + expect(label.attributes("aria-label")).toContain(questionText); + + }); + + it("Should return aria-disabled state as disabled", async () => { + // Arrange + const wrapper = shallowMount(textboxQuestion, { + global: { + directives: { + maska: maska, + } + }, + propsData: { + isDisabled: true, + }, + mixins: [mockMixin] + }); + + // Assert + const input = wrapper.find("input"); + + // Expect + expect(input.attributes("aria-disabled")).toEqual("true"); }); @@ -166,6 +157,7 @@ describe("textboxQuestion.vue", () => { propsData: { modelValue: "val", }, + mixins: [mockMixin] }); await wrapper.find("input").setValue("val2"); @@ -175,4 +167,33 @@ describe("textboxQuestion.vue", () => { }); + it("Should call this.handleChange with new value when this.semiAggressiveValidation = true, the value is changed, and the new value is valid", async () => { + // Arrange + const wrapper = shallowMount(textboxQuestion, { + global: { + directives: { + maska: maska, + } + }, + propsData: { + options: {}, + modelValue: "foo", + semiAggressiveValidation: true, + }, + mixins: [mockMixin] + }); + + wrapper.vm.handleChange = jest.fn().mockImplementation(() => {}); + wrapper.vm.validate = jest.fn().mockImplementation(() => { + return true; + }); + + // Act + wrapper.vm.$options.watch.value.call(wrapper.vm, "bar"); + + // Assert + expect(wrapper.vm.handleChange).toHaveBeenCalled; + + }); + }); diff --git a/src/constants/dynamic-strings.js b/src/constants/dynamic-strings.js index 1c3b8ade7..c526e45a9 100644 --- a/src/constants/dynamic-strings.js +++ b/src/constants/dynamic-strings.js @@ -1,7 +1,7 @@ const dynamicStrings = { GLOBAL_STATE: "globalState", CUSTOM: "custom", - ROUTER_LINK: "routerLink" + ROUTER_LINK: "routerLink:" }; export { dynamicStrings }; \ No newline at end of file diff --git a/src/constants/endpoints.js b/src/constants/endpoints.js index 059693773..7ff9e3cec 100644 --- a/src/constants/endpoints.js +++ b/src/constants/endpoints.js @@ -79,6 +79,10 @@ const endpoints = { url: "/analytics/api/v1/analytics/log-custom-event", method: "POST", }, + InitializeSession:{ + url: "/analytics/api/v1/analytics/initialize", + method: "POST", + }, GetExperimentsByUser: { url: "/analytics/api/v1/analytics/get-experiments", method: "GET", diff --git a/src/constants/store-actions.js b/src/constants/store-actions.js index fe658fbe1..0ac79ac28 100644 --- a/src/constants/store-actions.js +++ b/src/constants/store-actions.js @@ -21,6 +21,7 @@ const storeActions = { LOG_EXPERIMENT_EXPOSURE: "logExperimentExposure", LOG_PAGE_VIEW: "logPageView", LOG_CUSTOM_EVENT: "logCustomEvent", + INITIALIZE_SESSION: "initializeSession", GET_EXPERIMENTS_BY_USER: "GetExperimentsByUser", UPDATE_SERVICE_LOCATION_WITH_VEHICLE_REGISTRATION: "updateServiceLocationWithVehicleRegistration", diff --git a/src/helpers/cms-content-helper.js b/src/helpers/cms-content-helper.js index 642f0d014..73912835a 100644 --- a/src/helpers/cms-content-helper.js +++ b/src/helpers/cms-content-helper.js @@ -120,4 +120,37 @@ function processWidgetItemForReplacement(widgetModel, key) { // If we have something else like a number, boolean, etc. just return it return widgetModel[key]; -} \ No newline at end of file +} + + +export function doesCopyContainRouterLink(copy) { + return copy.includes(this.dynamicStrings.ROUTER_LINK); +} + +export function splitCopyOnCMSPlaceHolder(copy){ + // splits copy on { ... } such as {routerlink: ...} + return copy.split(/{(.*?)}/g); +} + +export function getRouterLinkRouteFromCopy(copy){ + // sample input: {routerLink:estimate,provide your VIN} + // first split would return 'estimate,provide your VIN' + // second split would return 'estimate' + return copy.split(':')[1].split(',')[0]; +} + +export function getRouterLinkDisplayTextFromCopy(copy){ + // sample input: {routerLink:estimate,provide your VIN} + // first split would return 'estimate,provide your VIN' + // second split would return 'provide your VIN' + return copy.split(':')[1].split(',')[1]; +} + +// Copy returned from the CMS that has newlines will return blocks wrapped in +//

...

+// This function returns an array of each paragraph, works with or without html +// attributes present +export function splitCMSCopyOnParagraphTag(copy) { + // filter removes empty strings that are a result of string.split with regex + return copy.split(/(?:)|(?:<\/p>)/g).filter(paragraph => paragraph !== ""); +} diff --git a/src/helpers/heritage-integration/cookie-helper.js b/src/helpers/heritage-integration/cookie-helper.js index f3b730f90..5d681f17f 100644 --- a/src/helpers/heritage-integration/cookie-helper.js +++ b/src/helpers/heritage-integration/cookie-helper.js @@ -104,6 +104,14 @@ export function getSessionIdValue(){ return '00000000-0000-0000-0000-000000000000'; } +export function setCookieProperties(properties) { + if (typeof properties == "object") { + Object.keys(properties).forEach(key => { + document.cookie = `${key}=${properties[key]}`; + }); + } +} + /* =========================== = PRIVATE FUNCTIONS = diff --git a/src/helpers/heritage-integration/order-helper.js b/src/helpers/heritage-integration/order-helper.js index d64ab865d..962550592 100644 --- a/src/helpers/heritage-integration/order-helper.js +++ b/src/helpers/heritage-integration/order-helper.js @@ -11,8 +11,8 @@ import baseMixin from "@/mixins/base-mixin"; export async function loadOrderIfPresent() { const funnelCookie = getFunnelCookie(); - // Do nothing if there is no cookie or no correlation id. - if (funnelCookie == null || funnelCookie.ReferralCorrelationId == null) { + // Do nothing if there is no cookie, correlation id, or referral number. + if (funnelCookie == null || funnelCookie.ReferralCorrelationId == null || !funnelCookie.ReferralNumber) { return null; } diff --git a/src/helpers/heritage-integration/order-helper.spec.js b/src/helpers/heritage-integration/order-helper.spec.js index b66286d50..7d769a8d8 100644 --- a/src/helpers/heritage-integration/order-helper.spec.js +++ b/src/helpers/heritage-integration/order-helper.spec.js @@ -18,7 +18,8 @@ describe("loadOrderIfPresent", () => { const testCookieValue = { ShouldResetState: testShouldResetState, - ReferralCorrelationId: "xxx" + ReferralCorrelationId: "xxx", + ReferralNumber: "12345" } document.cookie = `${cookieNames.FUNNEL_SESSION_INFO}=${JSON.stringify(testCookieValue)}; path=/; ${cookieHelper.getCookieDomainValue()}`; @@ -33,7 +34,11 @@ describe("loadOrderIfPresent", () => { test("ShouldResetState == true => reset store", () => { // Arrange - cookieHelper.getFunnelCookie = jest.spyOn(cookieHelper, "getFunnelCookie").mockReturnValueOnce({ ShouldResetState: true, ReferralCorrelationId: "xxx-xxx-xxx" }); + cookieHelper.getFunnelCookie = jest.spyOn(cookieHelper, "getFunnelCookie").mockReturnValueOnce({ + ShouldResetState: true, + ReferralCorrelationId: "xxx-xxx-xxx", + ReferralNumber: "12345" + }); const mockData = { actionList: [{ diff --git a/src/helpers/unit-test-helper.js b/src/helpers/unit-test-helper.js index df697a022..592af9a5c 100644 --- a/src/helpers/unit-test-helper.js +++ b/src/helpers/unit-test-helper.js @@ -24,14 +24,13 @@ export function getMountOptions(mockData) { mocks.logEvent = jest.fn(); mocks.pushExperimentsToDataLayer = jest.fn(); mocks.prependActionToMethod = jest.fn(); - mocks.dispatchStoreAction = jest.fn(); mocks.dispatchStoreAction.mockImplementation((actionName) => { let actionFilterResult = mockData.actionList.filter( (x) => x.actionName == actionName ); - if (actionFilterResult.length > 0 && actionFilterResult.length === 1) { + if (actionFilterResult.length === 1) { return Promise.resolve({ data: actionFilterResult[0].data, }); @@ -56,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/address-lookup.spec.js b/src/layouts/address-lookup/address-lookup.spec.js new file mode 100644 index 000000000..d1f261f9a --- /dev/null +++ b/src/layouts/address-lookup/address-lookup.spec.js @@ -0,0 +1,777 @@ +// Components +import addressLookup from "@/layouts/address-lookup/address-lookup.vue"; + +// Supporting Files +import baseMixin from "@/mixins/base-mixin"; +import { 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 { navigationScenarios } from "@/router/router-constants/navigation-scenarios"; +import store from "@/store"; +import { navigateAfterSaveToHeritageFunnel } from "@/helpers/heritage-integration/navigation-helper"; + + +jest.mock("@/helpers/damage-helper", () => ({ + isGlassAvailableForCarId: jest.fn().mockImplementation(() => true), + getDamageString: jest.fn() +})); + +jest.mock("@/helpers/heritage-integration/navigation-helper", () => ({ + navigateAfterSaveToHeritageFunnel: jest.fn() +})); + +describe("address-lookup.vue", () => { + describe("page level alerts", () => { + test("if the address is not serviceable display the Non-Serviceable Zip Alert", async () => { + // Arrange + const mockRegistrationAddress = { + streetAddress: "1234 Main St", + city: "Columbus", + state: "OH", + zipCode: "43215" + } + + const { wrapper } = setupMocks(addressLookup, { + isZipServiceable: false + }); + + store.commit(storeMutations.UPDATE_CAR_ID, "CARID"); + + await wrapper.setData({ + customerQuestions: { + addressQuestions: mockRegistrationAddress + }, + }) + + // Act + await wrapper.vm.forwardButtonAction(); + + // Assert + expect(wrapper.findComponent({ ref: "alertNonServiceableZip" }).isVisible()).toBe(true); + }); + + test("if the address matches a different vehicle display the Matched Different VehicleAlert", async () => { + // Arrange + const mockRegistrationAddress = { + streetAddress: "1234 Main St", + city: "Columbus", + state: "OH", + zipCode: "43215" + } + + const { wrapper } = setupMocks(addressLookup, { + isZipServiceable: true + }); + + store.commit(storeMutations.UPDATE_CAR_ID, "CARID2"); + + await wrapper.setData({ + customerQuestions: { + addressQuestions: mockRegistrationAddress + }, + }) + + // Act + await wrapper.vm.forwardButtonAction(); + + // Assert + expect(wrapper.findComponent({ ref: "alertMatchedDifferentVehicle" }).isVisible()).toBe(true); + }); + + test("if the looking up VIN by address is not allowed in the state selected display the Vin Lookup By HomeAddress Not Allowed Alert", async () => { + // Arrange + const mockRegistrationAddress = { + streetAddress: "1234 Main St", + city: "Columbus", + state: "OH", + zipCode: "43215" + } + + const { wrapper } = setupMocks(addressLookup, { + isZipServiceable: true + }); + + store.commit(storeMutations.UPDATE_CAR_ID, "CARID"); + + baseMixin.methods.dispatchStoreAction = jest.fn(); + baseMixin.methods.dispatchStoreAction.mockImplementation((actionName, value) => { + let data = {}; + if (actionName == storeActions.VALIDATE_ZIP) { + data = { + isServiceable: true + } + } + else if (actionName == storeActions.LOOKUP_VIN_BY_ADDRESS) { + data = { + isStatePermissible: false, + vinVehicles: [{ + vin: "TEST_VIN", + vehicle: { + carId: "CARID" + } + }, + { + vin: "TEST_VIN2", + vehicle: { + carId: "CARID2" + } + }] + } + } + + return Promise.resolve({ data }); + }) + + await wrapper.setData({ + customerQuestions: { + addressQuestions: mockRegistrationAddress + }, + }) + + // Act + await wrapper.vm.forwardButtonAction(); + + // Assert + expect(wrapper.findComponent({ ref: "alertVinLookupsByHomeAddressNotAllowed" }).isVisible()).toBe(true); + + }); + + test("if no vehicles found, display Vin Not Found alert", async () => { + // Arrange + const mockRegistrationAddress = { + streetAddress: "1234 Main St", + city: "Columbus", + state: "OH", + zipCode: "43215" + } + + const { wrapper } = setupMocks(addressLookup, { + isZipServiceable: true + }); + + store.commit(storeMutations.UPDATE_CAR_ID, "CARID"); + + baseMixin.methods.dispatchStoreAction = jest.fn(); + baseMixin.methods.dispatchStoreAction.mockImplementation((actionName, value) => { + let data = {}; + if (actionName == storeActions.VALIDATE_ZIP) { + data = { + isServiceable: true + } + } + else if (actionName == storeActions.LOOKUP_VIN_BY_ADDRESS) { + data = { + isStatePermissible: true, + vinVehicles: [] // Return no vehicles + } + } + + return Promise.resolve({ data }); + + }) + + await wrapper.setData({ + customerQuestions: { + addressQuestions: mockRegistrationAddress + }, + }) + + wrapper.vm.navigateForward = jest.fn(); + + // Act + await wrapper.vm.forwardButtonAction(); + + // Assert + expect(wrapper.findComponent({ ref: "alertVinNotFound" }).isVisible()).toBe(true); + + }); + }); + + describe("navigation", () => { + + test("if the back button is clicked, navigate back", async () => { + // Arrange + const { wrapper } = setupMocks(addressLookup, { + isZipServiceable: true + }); + + // Act + await wrapper.vm.backButtonAction(); + + // Assert + expect(wrapper.vm.$router.navigate).toHaveBeenCalled(); + + }); + + test("if the car entered matches one of the vehicles found and the zip is serviceable, navigate forward", async () => { + // Arrange + const mockRegistrationAddress = { + streetAddress: "1234 Main St", + city: "Columbus", + state: "OH", + zipCode: "43215" + } + + const { wrapper } = setupMocks(addressLookup, { + isZipServiceable: true + }); + + store.commit(storeMutations.UPDATE_CAR_ID, "CARID"); + + baseMixin.methods.dispatchStoreAction = jest.fn(); + baseMixin.methods.dispatchStoreAction.mockImplementation((actionName, value) => { + let data = {}; + if (actionName == storeActions.VALIDATE_ZIP) { + data = { + isServiceable: true + } + } + else if (actionName == storeActions.LOOKUP_VIN_BY_ADDRESS) { + data = { + isStatePermissible: true, + vinVehicles: [{ + vin: "TEST_VIN", + vehicle: { + carId: "CARID" + } + }, + { + vin: "TEST_VIN2", + vehicle: { + carId: "CARID2" + } + }] + } + } + + return Promise.resolve({ data }); + }) + + await wrapper.setData({ + customerQuestions: { + addressQuestions: mockRegistrationAddress + }, + }) + + wrapper.vm.navigateForward = jest.fn(); + + // Act + await wrapper.vm.forwardButtonAction(); + + // Assert + expect(wrapper.vm.navigateForward).toHaveBeenCalled(); + }); + + test("if the car entered matches one of multiple vehicles found, update vehicle info and navigate to the heritage funnel", async () => { + // Arrange + const mockRegistrationAddress = { + streetAddress: "1234 Main St", + city: "Columbus", + state: "OH", + zipCode: "43215" + } + + const { wrapper } = setupMocks(addressLookup, { + isZipServiceable: true + }); + + store.commit(storeMutations.UPDATE_CAR_ID, "CARID"); + + baseMixin.methods.dispatchStoreAction = jest.fn(); + baseMixin.methods.dispatchStoreAction.mockImplementation((actionName, value) => { + let data = {}; + if (actionName == storeActions.VALIDATE_ZIP) { + data = { + isServiceable: true + } + } + else if (actionName == storeActions.LOOKUP_VIN_BY_ADDRESS) { + data = { + isStatePermissible: true, + vinVehicles: [{ + vin: "TEST_VIN", + vehicle: { + carId: "CARID" + } + }, + { + vin: "TEST_VIN2", + vehicle: { + carId: "CARID2" + } + }] + } + } + + return Promise.resolve({ data }); + + }) + + await wrapper.setData({ + customerQuestions: { + addressQuestions: mockRegistrationAddress + }, + }) + + wrapper.vm.updateVehicleInfo = jest.fn(); + + // Act + await wrapper.vm.forwardButtonAction(); + + // Assert + expect(wrapper.vm.updateVehicleInfo).toHaveBeenCalled(); + expect(navigateAfterSaveToHeritageFunnel).toHaveBeenCalled(); + + }); + + test("if the car entered does not match any of the multiple vehicles found, navigate to address-vehicles page", async () => { + // Arrange + const mockRegistrationAddress = { + streetAddress: "1234 Main St", + city: "Columbus", + state: "OH", + zipCode: "43215" + } + + const { wrapper } = setupMocks(addressLookup, { + isZipServiceable: true + }); + + store.commit(storeMutations.UPDATE_CAR_ID, "CARID_A"); + + baseMixin.methods.dispatchStoreAction = jest.fn(); + baseMixin.methods.dispatchStoreAction.mockImplementation((actionName, value) => { + let data = {}; + if (actionName == storeActions.VALIDATE_ZIP) { + data = { + isServiceable: true + } + } + else if (actionName == storeActions.LOOKUP_VIN_BY_ADDRESS) { + data = { + isStatePermissible: true, + vinVehicles: [{ + vin: "TEST_VIN", + vehicle: { + carId: "CARID" + } + }, + { + vin: "TEST_VIN2", + vehicle: { + carId: "CARID2" + } + }] + } + } + + return Promise.resolve({ data }); + + }) + + const carsFound = [{ + vin: "TEST_VIN", + vehicle: { + carId: "CARID" + } + }, + { + vin: "TEST_VIN2", + vehicle: { + carId: "CARID2" + } + }] + + await wrapper.setData({ + customerQuestions: { + addressQuestions: mockRegistrationAddress + }, + }) + + wrapper.vm.updateVehicleInfo = jest.fn(); + + // Act + await wrapper.vm.forwardButtonAction(); + + // Assert + expect(wrapper.vm.$router.navigateAfterSave).toHaveBeenCalledWith(navigationScenarios.CONTINUING_WITH_MULTIPLE_VEHICLES, undefined, {}, {}, carsFound); + + }); + + test("if the car entered matches one of the vehicles found but the zip is NOT serviceable, do not navigate forward", async () => { + // Arrange + const mockRegistrationAddress = { + streetAddress: "1234 Main St", + city: "Columbus", + state: "OH", + zipCode: "43215" + } + + const { wrapper } = setupMocks(addressLookup, { + isZipServiceable: true + }); + + store.commit(storeMutations.UPDATE_CAR_ID, "CARID"); + + baseMixin.methods.dispatchStoreAction = jest.fn(); + baseMixin.methods.dispatchStoreAction.mockImplementation((actionName, value) => { + let data = {}; + if (actionName == storeActions.VALIDATE_ZIP) { + data = { + isServiceable: false + } + } + else if (actionName == storeActions.LOOKUP_VIN_BY_ADDRESS) { + data = { + isStatePermissible: true, + vinVehicles: [{ + vin: "TEST_VIN", + vehicle: { + carId: "CARID" + } + }, + { + vin: "TEST_VIN2", + vehicle: { + carId: "CARID2" + } + }] + } + } + + return Promise.resolve({ data }); + + }) + + await wrapper.setData({ + customerQuestions: { + addressQuestions: mockRegistrationAddress + }, + }) + + wrapper.vm.navigateForward = jest.fn(); + + // Act + await wrapper.vm.forwardButtonAction(); + + // Assert + expect(wrapper.vm.navigateForward).toHaveBeenCalledTimes(0); + + }); + + test("if a different vehicle is found than the one entered and the selected glass is not available for that vehicle, navigate back to vehicle-damage page", async () => { + // Arrange + const mockRegistrationAddress = { + streetAddress: "1234 Main St", + city: "Columbus", + state: "OH", + zipCode: "43215" + } + + const { wrapper } = setupMocks(addressLookup, { + isZipServiceable: true + }); + + store.commit(storeMutations.UPDATE_CAR_ID, "CARID"); + + baseMixin.methods.dispatchStoreAction = jest.fn(); + baseMixin.methods.dispatchStoreAction.mockImplementation((actionName, value) => { + let data = {}; + if (actionName == storeActions.LOOKUP_VIN_BY_ADDRESS) { + data = { + isStatePermissible: true, + vinVehicles: [{ + vin: "TEST_VIN", + vehicle: { + carId: "CARID2" + } + }] + } + } + + return Promise.resolve({ data }); + + }) + + await wrapper.setData({ + customerQuestions: { + addressQuestions: mockRegistrationAddress + }, + isCarIdDifferent: true, + isGlassAvailableForCarId: false, + }) + + let carEntered = [{ + vin: "TEST_VIN", + vehicle: { + carId: "CARID" + } + }]; + + let carsFound = [{ + vin: "TEST_VIN2", + vehicle: { + carId: "CARID2" + } + }]; + + // Act + await wrapper.vm.navigateForward(carEntered, carsFound); + + // Assert + expect(wrapper.vm.$router.navigateAfterSave).toHaveBeenCalledWith(navigationScenarios.CONTINUING_WITH_DIFFERENT_GLASS, undefined, {}, {"displayVehicleChangeAlert": true}, {}); + + }); + + }); + + describe("reseting dependent state", () => { + test("when reseting dependent state, license plate is set to null and parts state and dependencies are reset", async () => { + // Arrange + const commitSpy = jest.spyOn(store, "commit"); + const dispatchSpy = jest.spyOn(store, "dispatch"); + const { wrapper } = setupMocks(addressLookup, { + isZipServiceable: true + }); + + // Act + wrapper.vm.resetDependentState(); + + // Assert + expect(commitSpy).toBeCalledWith(storeMutations.UPDATE_REGISTRATION_LICENSE_PLATE, null); + expect(dispatchSpy).toBeCalledWith(storeActions.RESET_PARTS_STATE_AND_DEPENDENCIES); + + }); + }); + + describe("registration and service zips", () => { + describe("if registration zip is serviceable", () => { + test("if registration address is provided => update service address on successful continue", async () => { + // Arrange + const mockRegistrationAddress = { + streetAddress: "1234 Main St", + city: "Columbus", + state: "OH", + zipCode: "43215" + } + + const { wrapper } = setupMocks(addressLookup,{ + isZipServiceable: true + }); + + store.commit(storeMutations.UPDATE_CAR_ID, "CARID"); + + await wrapper.setData({ + customerQuestions: { + addressQuestions: mockRegistrationAddress + } + }) + + // Act + await wrapper.vm.forwardButtonAction(); + + // Assert + expect(baseMixin.methods.dispatchStoreAction).toHaveBeenCalledWith(storeActions.UPDATE_SERVICE_LOCATION_WITH_VEHICLE_REGISTRATION); + }); + }); + + describe("if registration zip is not serviceable", () => { + test("if registration address is provided and user clicks continue => show non-serviceable zip alert", async () => { + // Arrange + const mockRegistrationAddress = { + streetAddress: "1234 Main St", + city: "Columbus", + state: "OH", + zipCode: "43215" + } + + const { wrapper } = setupMocks(addressLookup, { + isZipServiceable: false + }); + + store.commit(storeMutations.UPDATE_CAR_ID, "CARID"); + + await wrapper.setData({ + customerQuestions: { + addressQuestions: mockRegistrationAddress + } + }) + + expect(wrapper.findComponent({ ref: "alertNonServiceableZip" }).exists()).toBe(false); + + // Act + await wrapper.vm.forwardButtonAction(); + + // Assert + expect(wrapper.vm.displayNonServiceableZipAlert).toBe(true); + expect(wrapper.findComponent({ ref: "alertNonServiceableZip" }).exists()).toBe(true); + expect(wrapper.findComponent({ ref: "alertNonServiceableZip" }).isVisible()).toBe(true); + }); + + test("if registration address is provided user clicks continue => show service zip field on continue click", async () => { + // Arrange + const mockRegistrationAddress = { + streetAddress: "1234 Main St", + city: "Columbus", + state: "OH", + zipCode: "43215" + } + + const { wrapper } = setupMocks(addressLookup, { + isZipServiceable: false + } + ); + + expect(wrapper.vm.showServiceZipField).toBeFalsy(); + expect(wrapper.findComponent({ ref: "serviceZip" }).exists()).toBe(false); + store.commit(storeMutations.UPDATE_CAR_ID, "CARID"); + + await wrapper.setData({ + customerQuestions: { + addressQuestions: mockRegistrationAddress + } + }) + + // Act + await wrapper.vm.forwardButtonAction(); + + // Assert + expect(wrapper.vm.showServiceZipField).toBe(true); + expect(wrapper.findComponent({ ref: "serviceZip" }).isVisible()).toBe(true); + }); + + test("if registration address, service zip are provided, and user clicks continue => don't update service address", async () => { + // Arrange + const mockRegistrationAddress = { + streetAddress: "1234 Main St", + city: "Columbus", + state: "OH", + zipCode: "43215" + } + + const { wrapper } = setupMocks(addressLookup, { + isZipServiceable: false + } + ); + + store.commit(storeMutations.UPDATE_CAR_ID, "CARID"); + + await wrapper.setData({ + customerQuestions: { + addressQuestions: mockRegistrationAddress + } + }) + + // Act + await wrapper.vm.forwardButtonAction(); + + // Assert + expect(baseMixin.methods.dispatchStoreAction).not.toHaveBeenCalledWith(storeActions.UPDATE_SERVICE_LOCATION_WITH_VEHICLE_REGISTRATION); + }); + + test("if registration address, service zip are provided, and user clicks continue => both zips are saved and are different", async () => { + // Arrange + const mockRegistrationAddress = { + streetAddress: "1234 Main St", + city: "Columbus", + state: "OH", + zipCode: "43215" + } + + const { wrapper } = setupMocks(addressLookup, {}); + + store.commit(storeMutations.UPDATE_CAR_ID, "CARID"); + + baseMixin.methods.dispatchStoreAction = jest.fn(); + baseMixin.methods.dispatchStoreAction.mockImplementation((actionName, value) => { + let data = {}; + if (actionName == storeActions.VALIDATE_ZIP) { + if (value == "43215") { + data = { + isServiceable: false + }; + } + else { + data = { + isServiceable: true + } + } + } + else if (actionName == storeActions.LOOKUP_VIN_BY_ADDRESS) { + data = { + isStatePermissible: true, + vinVehicles: [{ + vin: "TEST_VIN", + vehicle: { + carId: "CARID" + } + }] + } + } + + return Promise.resolve({ data }); + }) + + await wrapper.setData({ + customerQuestions: { + addressQuestions: mockRegistrationAddress + } + }) + await wrapper.vm.forwardButtonAction(); + await wrapper.setData({ + serviceZipCode: "12345" + }) + + // Act + await wrapper.vm.forwardButtonAction(); + + // Assert + expect(store.getters.order.serviceLocation.zipCode).not.toEqual(store.getters.vehicle.registration.zipCode); + expect(store.getters.vehicle.registration.zipCode).toEqual("43215"); + expect(store.getters.order.serviceLocation.zipCode).toEqual("12345"); + }); + }); + }); +}); + +function setupMocks(mountOptions, { isZipServiceable = true, lookupVinbyAddressResponse }) { + store.commit(storeMutations.RESET_STATE); + const wrapper = shallowMount(addressLookup, getMountOptions({ + ...mountOptions, + actionList: [ + { + actionName: storeActions.VALIDATE_ZIP, + data: { + isServiceable: isZipServiceable + } + }, + { + actionName: storeActions.LOOKUP_VIN_BY_ADDRESS, + data: lookupVinbyAddressResponse ? lookupVinbyAddressResponse : { + isStatePermissible: true, + vinVehicles: [{ + vin: "TEST_VIN", + vehicle: { + carId: "CARID" + } + }] + } + } + ], + router: { + navigate: jest.fn(), + navigateAfterSave: jest.fn() + }, + })); + + wrapper.vm.getCmsContent = jest.fn().mockImplementation(() => ""); + wrapper.vm.setCmsContent = baseMixin.methods.setCmsContent; + wrapper.vm.$refs.funnelFooter.updateButtonText = jest.fn(); + wrapper.vm.$refs.funnelFooter.removeLoader = jest.fn(); + wrapper.vm.$refs.loadingModal.showModal = jest.fn(); + + return { wrapper }; + +} + diff --git a/src/layouts/address-lookup/address-lookup.spec.js1 b/src/layouts/address-lookup/address-lookup.spec.js1 deleted file mode 100644 index 4443100c3..000000000 --- a/src/layouts/address-lookup/address-lookup.spec.js1 +++ /dev/null @@ -1,244 +0,0 @@ -// Components -import addressLookup from "@/layouts/address-lookup/address-lookup.vue"; -import funnelHeader from "@/common-components/funnel-header/funnel-header"; -import funnelFooter from "@/common-components/funnel-footer/funnel-footer"; -import vehicleBanner from "@/common-components/vehicle-banner/vehicle-banner"; -import funnelSubHeader from "@/common-components/funnel-sub-header/funnel-sub-header"; -import customerQuestions from "@/layouts/address-lookup/customer-questions/customer-questions"; -import addressQuestions from "@/layouts/address-lookup/customer-questions/address-questions/address-questions"; - -// Supporting Files -import { settleAllPromises } from "@/helpers/layout-helper.js"; -import baseMixin from "@/mixins/base-mixin"; -import { fetchCmsContentForPage } from "@/helpers/cms-content-helper"; -import { mount, flushPromises } from "@vue/test-utils"; -import { getMountOptions } from "@/helpers/unit-test-helper.js"; -import { maska } from 'maska'; -import { nextTick } from "vue"; -import { storeActions } from "@/constants/store-actions"; -import { storeMutations } from "@/constants/store-mutations"; -import store from "@/store"; -import { validate } from "vee-validate"; - -// 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(), -})); - -describe("address-lookup.vue", () => { - test("Page header is initialized with api data", async (done) => { - //Arrange - const pageHeaderWidgetHeaderText = "Select Damage"; - const { wrapper, apiPromise } = setupMocks({ - pageHeaderWidgetHeaderText: pageHeaderWidgetHeaderText, - }); - - //Act - addressLookup.beforeRouteEnter.call( - wrapper.vm, - { query: { fmgPage: "address-lookup" } }, - undefined, - (c) => c(wrapper.vm) - ); - - //Assert - apiPromise.finally(() => { - expect(funnelSubHeader.methods.initializeComponent).toHaveBeenCalledWith( - pageHeaderWidgetHeaderText - ); - done(); - }); - }); - - test("Customer Questions component is initialized with api data", async (done) => { - //Arrange - const StreetAddressQuestionWidget = { QuestionText: "test" }; - const CityQuestionWidget = { QuestionText: "test" }; - const StateQuestionWidget = { QuestionText: "test" }; - const ZipQuestionWidget = { QuestionText: "test" }; - const FirstNameQuestionWidget = { QuestionText: "test" }; - const LastNameQuestionWidget = { QuestionText: "test" }; - const EmailAddressQuestionWidget = { QuestionText: "test" }; - const AlertVerificationWarningWidget = { HeaderText: "test", BodyText: "test" }; - const AlertNoMatchWarningWidget = { HeaderText: "test", BodyText: "test" }; - - const widgets = [ - StreetAddressQuestionWidget, - CityQuestionWidget, - StateQuestionWidget, - ZipQuestionWidget, - AlertVerificationWarningWidget, - AlertNoMatchWarningWidget, - FirstNameQuestionWidget, - LastNameQuestionWidget, - EmailAddressQuestionWidget, - ]; - - const { wrapper, apiPromise } = setupMocks({ - cmsContent: widgets, - }); - - //Act - addressLookup.beforeRouteEnter.call( - wrapper.vm, - { query: { fmgPage: "address-lookup" } }, - undefined, - (c) => c(wrapper.vm) - ); - - //Assert - apiPromise.finally(() => { - expect(customerQuestions.methods.initializeComponent).toHaveBeenCalledWith( - widgets - ); - done(); - }); - }); - - }); - - - - function setupMocks({ - pageHeaderWidgetHeaderText = {}, - mountOptionsMockData = { - router: { - navigate: jest.fn(), - }, - store: { - getters: { - vehicle: {}, - }, - }, - }, - }) { - //Mock api responses - baseMixin.methods.dispatchStoreAction = jest.fn(); - const apiResponses = { - cmsContent: { - FunnelSubHeaderWidget: pageHeaderWidgetHeaderText, - VehicleBannerWidget: { - GenericVehicleImage: - "https://digitalconsumercms-dev.safelite.com/images/default-source/default-album/blurred-image.jpg?sfvrsn=a6ce3034_3", - }, - FunnelHeaderWidget: { - LogoImage: - "https://digitalconsumercms-dev.safelite.com/images/default-source/default-album/safelite-logo.svg?sfvrsn=45e7ed06_3", - }, - StreetAddressQuestionWidget: { - QuestionText: - "test" - }, - CityQuestionWidget: { - QuestionText: - "test" - }, - StateQuestionWidget: { - QuestionText: - "test" - }, - ZipQuestionWidget: { - QuestionText: - "test" - }, - FirstNameQuestionWidget: { - QuestionText: - "test" - }, - LastNameQuestionWidget: { - QuestionText: - "test" - }, - EmailAddressQuestionWidget: { - QuestionText: - "test" - }, - AlertVerificationWarningWidget: { - HeaderText: - "test", - BodyText: - "test", - }, - AlertNoMatchWarningWidget: { - HeaderText: - "test", - BodyText: - "test", - }, - - }, - }; - - const apiPromise = Promise.resolve(apiResponses); - - settleAllPromises.mockImplementation(() => apiPromise); - fetchCmsContentForPage.mockImplementation(() => Promise.resolve()); - - //Mock damage initialize methods - funnelHeader.methods = { - initializeComponent: jest.fn(), - }; - - vehicleBanner.methods = { - initializeComponent: jest.fn(), - }; - - funnelSubHeader.methods = { - initializeComponent: jest.fn(), - }; - - funnelFooter.methods = { - initializeComponent: jest.fn(), - }; - - customerQuestions.methods = { - initializeComponent: jest.fn(), - }; - - addressQuestions.methods = { - initializeComponent: jest.fn(), - setupAddressLookup: jest.fn(), - }; - - const mountOptions = getMountOptions(mountOptionsMockData); - mountOptions['attachTo'] = document.body; // append wrapper to document.body to test DOM methods - - mountOptions.global.directives = { - maska: maska - }; - - const wrapper = mount(addressLookup, mountOptions); - - const funnelHeaderWrapper = wrapper.findComponent({ name: "funnelHeader" }); - funnelHeaderWrapper.vm.initializeComponent = - funnelHeader.methods.initializeComponent; - - const vehicleBannerWrapper = wrapper.findComponent({ name: "vehicleBanner" }); - vehicleBannerWrapper.vm.initializeComponent = - vehicleBanner.methods.initializeComponent; - - const funnelSubHeaderWrapper = wrapper.findComponent({ name: "funnelSubHeader" }); - funnelSubHeaderWrapper.vm.initializeComponent = - funnelSubHeader.methods.initializeComponent; - - const funnelFooterWrapper = wrapper.findComponent({ name: "funnelFooter" }); - funnelFooterWrapper.vm.initializeComponent = - funnelFooter.methods.initializeComponent; - - const customerQuestionsWrapper = wrapper.findComponent({ name: "customerQuestions" }); - customerQuestionsWrapper.vm.initializeComponent = - customerQuestions.methods.initializeComponent; - - const addressQuestionsWrapper = wrapper.findComponent({ name: "addressQuestions" }); - addressQuestionsWrapper.vm.initializeComponent = - addressQuestions.methods.initializeComponent; - addressQuestionsWrapper.vm.setupAddressLookup = - addressQuestions.methods.setupAddressLookup; - - return { wrapper, apiPromise }; - } \ No newline at end of file diff --git a/src/layouts/address-lookup/address-lookup.vue b/src/layouts/address-lookup/address-lookup.vue index f29e3d21a..2e13299de 100644 --- a/src/layouts/address-lookup/address-lookup.vue +++ b/src/layouts/address-lookup/address-lookup.vue @@ -151,15 +151,15 @@ export default { ); }, attachCustomEvents() { - this.prependActionToMethod(this, this.forwardButtonAction, () => { - this.pushEventToGA( - this.$route.query[this.queryStrings.FMG_PAGE], - this.GaActions.SUBMITTED, - this.GaLabels.ADDRESS_LOOKUP, - true - ); - }); - }, + this.prependActionToMethod(this, this.forwardButtonAction, () => { + this.pushEventToGA( + this.$route.query[this.queryStrings.FMG_PAGE], + this.GaActions.SUBMITTED, + this.GaLabels.ADDRESS_LOOKUP, + true + ); + }); + }, getRegistrationAddressFromStore() { return store.getters.vehicle.registration.address; }, @@ -207,7 +207,7 @@ export default { this.$refs.funnelFooter.removeLoader(); return; } - + // if the neither the registration zip code or service zip code are not serviceable this.isZipServicable = serviceZipValidationResponse.data.isServiceable; if (!this.isZipServicable) { @@ -329,8 +329,8 @@ export default { { licenseLastName: lastName, licenseStreetAddress: streetAddress, - licenseZip: zip, - licenseState: state + licenseZip: zip, + licenseState: state, }, false ); }, @@ -368,22 +368,22 @@ export default { } }, mounted() { - this.attachCustomEvents(); - }, + this.attachCustomEvents(); + }, computed: { - AlertNonServiceableZipHeader(){ + AlertNonServiceableZipHeader() { const zipCode = this.serviceZipCode ? this.serviceZipCode : this.customerQuestions.addressQuestions.zipCode; const text = this.getCmsContent("AlertNonServiceableZipWidget", "HeadlineText").replaceAll("{custom:serviceZip}", zipCode); return text; }, - AlertNonServiceableZipBody(){ + AlertNonServiceableZipBody() { return this.getCmsContent("AlertNonServiceableZipWidget", "BodyText"); }, - AlertMatchedDifferentVehicleHeader(){ + AlertMatchedDifferentVehicleHeader() { const text = this.getCmsContent("AlertMatchedDifferentVehicleWidget", "HeadlineText").replaceAll("{custom:glassText}", getDamageString()); return text; }, - AlertMatchedDifferentVehicleBody(){ + AlertMatchedDifferentVehicleBody() { const vinYmmFound = `${this.customAlertData?.vehicleInfo?.year} ${this.customAlertData?.vehicleInfo?.make} ${this.customAlertData?.vehicleInfo?.model}`; const vinYmmExpected = `${store.getters.vehicle.year} ${store.getters.vehicle.make} ${store.getters.vehicle.model}`; 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 new file mode 100644 index 000000000..fb1154fe6 --- /dev/null +++ b/src/layouts/address-lookup/customer-questions/address-questions/address-questions.spec.js @@ -0,0 +1,630 @@ +// Components +import addressQuestions from "@/layouts/address-lookup/customer-questions/address-questions/address-questions"; +import alert from "@/ux-components/alert/alert"; + +// 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"; + +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); + }) + + describe("initial state", () => { + test("only street address field is shown", () => { + // Arrange + const { wrapper } = setupMocks({}); + + // 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" }); + + 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); + }); + + 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 + }); + 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, { + city: "Somewhere" + }) + await wrapper.vm.$nextTick(); + + // Assert + 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({}); + + wrapper.setData({ + displayNoMatchWarning: true + }); + 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, { + state: "KO" + }) + await wrapper.vm.$nextTick(); + + // Assert + expect(wrapper.vm.displayNoMatchWarning).toBeFalsy(); + noMatchAlert = wrapper.findComponent({ ref: "alertNoMatchWarning" }); + expect(noMatchAlert.exists()).toBeFalsy(); + }); + + test("user sees noMatch warning and enters zip code => noMatch warning is removed", async () => { + // Arrange + const { wrapper } = setupMocks({}); + + wrapper.setData({ + displayNoMatchWarning: true + }); + 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, { + zipCode: "12345" + }) + await wrapper.vm.$nextTick(); + + // Assert + 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); + + const resultingMountOptions = getMountOptions({ + ...mountOptions, + router: { + navigate: jest.fn(), + navigateAfterSave: jest.fn() + }, + loadScript: jest.fn().mockResolvedValue() + }); + + 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(); + + geocode(request, callback) { + callback([geocoderResult], true) + } + }, + GeocoderStatus: { + OK: true + } + } + }; + + if (props) + resultingMountOptions.propsData = props; + + 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.spec.js1 b/src/layouts/address-lookup/customer-questions/address-questions/address-questions.spec.js1 deleted file mode 100644 index e69de29bb..000000000 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 f188cf214..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; @@ -215,12 +214,12 @@ export default ({ // Standard place_changed event handling const autocompleteListener = window.google.maps.event.addListener(autocomplete, 'place_changed', fillInAddress); - // Wrapping the addressField1 element in the Google Address Autocomplete object - // will cause "autocomplete='off'" which Chrome completely ignores. This event - // handler will set the value to something arbitrary so autofill doesn't work. - // https://stackoverflow.com/a/30976223 - addressField1.addEventListener("focus", () => { - addressField1.setAttribute("autocomplete", "do-not-autofill"); + // Wrapping the addressField1 element in the Google Address Autocomplete object + // will cause "autocomplete='off'" which Chrome completely ignores. This event + // handler will set the value to something arbitrary so autofill doesn't work. + // https://stackoverflow.com/a/30976223 + addressField1.addEventListener("focus", () => { + addressField1.setAttribute("autocomplete", "do-not-autofill"); // Make place results box stick to the input on scroll const streetAddressField = document.getElementById("streetAddressField"); @@ -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 w - 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 } diff --git a/src/layouts/address-lookup/customer-questions/customer-questions.spec.js b/src/layouts/address-lookup/customer-questions/customer-questions.spec.js new file mode 100644 index 000000000..949d3dde0 --- /dev/null +++ b/src/layouts/address-lookup/customer-questions/customer-questions.spec.js @@ -0,0 +1,36 @@ +import { shallowMount } from "@vue/test-utils"; +import customerQuestions from "@/layouts/address-lookup/customer-questions/customer-questions"; + +const customerModel = { + addressQuestions: { + streetAddress: "", + city: "", + state: "", + zipCode: "", + }, + firstName: "", + lastName: "", + emailAddress: "", +} + +describe("customerQuestions.vue", () => { + + it("Should render customerQuestions sub-components (addressQuestions, first name, last name, and email textbox-questions)", async () => { + // Arrange + const wrapper = shallowMount(customerQuestions); + + // Act + const addressQuestions = wrapper.findComponent({ ref: 'addressQuestions' }); + const firstName = wrapper.findComponent({ ref: 'firstName' }); + const lastName = wrapper.findComponent({ ref: 'lastName' }); + const emailAddress = wrapper.findComponent({ ref: 'emailAddress' }); + + // Assert + expect(addressQuestions.exists()).toBe(true); + expect(firstName.exists()).toBe(true); + expect(lastName.exists()).toBe(true); + expect(emailAddress.exists()).toBe(true); + + }); + +}) \ No newline at end of file diff --git a/src/layouts/address-lookup/customer-questions/customer-questions.spec.js1 b/src/layouts/address-lookup/customer-questions/customer-questions.spec.js1 deleted file mode 100644 index e69de29bb..000000000 diff --git a/src/layouts/address-lookup/customer-questions/customer-questions.vue b/src/layouts/address-lookup/customer-questions/customer-questions.vue index 51bb2b770..9b89cf826 100644 --- a/src/layouts/address-lookup/customer-questions/customer-questions.vue +++ b/src/layouts/address-lookup/customer-questions/customer-questions.vue @@ -1,5 +1,5 @@