diff --git a/jest.config.js b/jest.config.js index d010bda25..7f7516934 100644 --- a/jest.config.js +++ b/jest.config.js @@ -26,8 +26,7 @@ module.exports = { "!src/layouts/address-lookup/customer-questions/customer-questions.vue", "!src/layouts/address-lookup/customer-questions/address-questions/address-questions.vue", "!src/common-components/dropdown-question/dropdown-question.vue", - "!src/common-components/textbox-question/textbox-question.vue", - "!src/helpers/validation-rules.js", + "!src/common-components/textbox-question/textbox-question.vue", // END ], //! means exclude from coverage. testMatch: ["**/*.spec.(js|jsx|ts|tsx)|**/__tests__/*.(js|jsx|ts|tsx)"], diff --git a/src/common-components/dropdown-question/dropdown-question.spec.js1 b/src/common-components/dropdown-question/dropdown-question.spec.js1 index 3eeb3662b..81669f714 100644 --- a/src/common-components/dropdown-question/dropdown-question.spec.js1 +++ b/src/common-components/dropdown-question/dropdown-question.spec.js1 @@ -1,6 +1,7 @@ import { shallowMount } from "@vue/test-utils"; import dropdownQuestion from "./dropdown-question"; import { nextTick } from "vue"; +import { maska } from 'maska'; describe("dropdownQuestion.vue", () => { @@ -19,7 +20,7 @@ describe("dropdownQuestion.vue", () => { expect(input.attributes()["aria-disabled"]).toEqual("true"); }); - it("Should render a text input", async () => { + it("Should render a select input", async () => { // Act const wrapper = shallowMount(dropdownQuestion, { propsData: { @@ -49,33 +50,82 @@ describe("dropdownQuestion.vue", () => { expect(input.attributes().id).toEqual("input ID"); }); -/* it("Should return label text", async () => { + 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, { - propsData: { - labelText: "label text", + global: { + directives: { + maska: maska, + } }, }); + await wrapper.setData({ + questionText: questionText, + }); + wrapper.vm.initializeComponent(cmsContent); // Assert - const label = wrapper.find("label"); + 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 "Question 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 = "Question Text"; + const cmsContent = { + QuestionText: originalQuestionText, + }; - expect(label.text()).toEqual("label text"); - }); */ - - it("Should return input id", async () => { // Act const wrapper = shallowMount(dropdownQuestion, { + global: { + directives: { + maska: maska, + } + }, propsData: { - inputId: "input ID", + disableAutoFill: true, }, }); + await wrapper.setData({ + questionText: originalQuestionText, + }); + wrapper.vm.initializeComponent(cmsContent); // Assert - const input = wrapper.find("select"); - - // Expect - expect(input.attributes().id).toEqual("input ID"); + 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/dropdown-question/dropdown-question.vue b/src/common-components/dropdown-question/dropdown-question.vue index 08ed368cf..7afdc0b38 100644 --- a/src/common-components/dropdown-question/dropdown-question.vue +++ b/src/common-components/dropdown-question/dropdown-question.vue @@ -37,6 +37,7 @@ export default { isRequired: Boolean, disableAutoFill: Boolean, validationRules: String, + cmsWidgetName: String, }, setup(props) { const fieldOptions = { @@ -57,18 +58,11 @@ export default { handleChange, meta, }; - }, - data() { - return { - questionText: "", - } - }, - methods: { - initializeComponent(cmsContent) { - this.questionText = cmsContent; - } - }, + }, computed: { + questionText(){ + return this.getCmsContent(this.cmsWidgetName, 'QuestionText'); + }, selectedOption: { get: function() { return this.modelValue; @@ -79,14 +73,22 @@ export default { }, labelText: { get: function () { - let labelText = this.questionText; + const noBreakChar = "⁠"; + var questionText = ""; if (this.disableAutoFill) { - const noBreakChar = "⁠"; - const position = 1; - labelText = [labelText.slice(0, position), noBreakChar, labelText.slice(position)].join(''); + var words = this.questionText.toString().split(/[ ]+/); + words.forEach(function (word) { + const position = 1; + word = [word.toString().slice(0, position), noBreakChar, word.toString().slice(position)].join(''); + questionText += `${word} `; + }); + + questionText = questionText.trimEnd(); + } else { + questionText = this.questionText.toString(); } - return labelText; + return questionText; } } }, diff --git a/src/common-components/textbox-question/textbox-question.spec.js1 b/src/common-components/textbox-question/textbox-question.spec.js1 index 721fba9d5..63214c126 100644 --- a/src/common-components/textbox-question/textbox-question.spec.js1 +++ b/src/common-components/textbox-question/textbox-question.spec.js1 @@ -1,12 +1,17 @@ import { shallowMount } from "@vue/test-utils"; import textboxQuestion from "./textbox-question"; -import { nextTick } from "vue"; +import { maska } from 'maska'; describe("textboxQuestion.vue", () => { it("Should return aria-disabled state", async () => { // Act const wrapper = shallowMount(textboxQuestion, { + global: { + directives: { + maska: maska, + } + }, propsData: { isDisabled: true, }, @@ -17,11 +22,17 @@ describe("textboxQuestion.vue", () => { // Expect expect(input.attributes()["aria-disabled"]).toEqual("true"); + }); it("Should render a text input", async () => { // Act const wrapper = shallowMount(textboxQuestion, { + global: { + directives: { + maska: maska, + } + }, propsData: { name: "test", label: "unit test label", @@ -32,11 +43,17 @@ describe("textboxQuestion.vue", () => { const input = wrapper.find("input"); expect(input.exists()).toBe(true); + }); it("Should return input id", async () => { // Act const wrapper = shallowMount(textboxQuestion, { + global: { + directives: { + maska: maska, + } + }, propsData: { inputId: "input ID", }, @@ -47,35 +64,115 @@ describe("textboxQuestion.vue", () => { // Expect expect(input.attributes().id).toEqual("input ID"); + }); - it("Should return label text", async () => { + 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, + } + }, + }); + 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 "Question 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 = "Question Text"; + const cmsContent = { + QuestionText: originalQuestionText, + }; + + // Act + const wrapper = shallowMount(textboxQuestion, { + global: { + directives: { + maska: maska, + } + }, propsData: { - labelText: "label text", + 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(textboxQuestion, { + global: { + directives: { + maska: maska, + } + }, + propsData: { + modelValue: "val", }, }); - // Assert - const label = wrapper.find("label"); - - expect(label.text()).toEqual("label text"); - }); - - it("Should return input id", async () => { - // Act - const wrapper = shallowMount(textboxQuestion, { - propsData: { - inputId: "input ID", - }, - }); + await wrapper.find("input").setValue("val2"); // Assert - const input = wrapper.find("input"); + expect(wrapper.emitted()).toHaveProperty('change') - // Expect - expect(input.attributes().id).toEqual("input ID"); }); }); diff --git a/src/common-components/textbox-question/textbox-question.vue b/src/common-components/textbox-question/textbox-question.vue index 5fba4c165..062ceb683 100644 --- a/src/common-components/textbox-question/textbox-question.vue +++ b/src/common-components/textbox-question/textbox-question.vue @@ -91,14 +91,22 @@ export default { }, labelText: { get: function () { - let labelText = this.questionText; + const noBreakChar = "⁠"; + var questionText = ""; if (this.disableAutoFill) { - var noBreakChar = "⁠"; - var position = 1; - labelText = [labelText.slice(0, position), noBreakChar, labelText.slice(position)].join(''); + var words = this.questionText.toString().split(/[ ]+/); + words.forEach(function (word) { + const position = 1; + word = [word.toString().slice(0, position), noBreakChar, word.toString().slice(position)].join(''); + questionText += `${word} `; + }); + + questionText = questionText.trimEnd(); + } else { + questionText = this.questionText.toString(); } - return labelText; + return questionText; } } }, diff --git a/src/constants/endpoints.js b/src/constants/endpoints.js index 47eb50ab8..ca1555e7b 100644 --- a/src/constants/endpoints.js +++ b/src/constants/endpoints.js @@ -47,6 +47,10 @@ const endpoints = { url: "/vehicle/api/v1/vehicle/lookup-vin-by-plate", method: "POST", }, + LookupVinByAddress: { + url: "/vehicle/api/v1/vehicle/lookup-vin-by-address", + method: "POST", + }, GetPartsOrQuestions: { url: "/parts/api/v1/parts/parts-or-questions", method: "POST", diff --git a/src/constants/error-messages.js b/src/constants/error-messages.js index c3976d0f9..17012ab68 100644 --- a/src/constants/error-messages.js +++ b/src/constants/error-messages.js @@ -11,11 +11,14 @@ const errorMessages = { CITY_REQUIRED: "Please enter your city", STATE_REQUIRED: "Please enter your state", ZIP_REQUIRED: "Please enter your ZIP", + ZIP_FORMAT: "Please enter a valid ZIP", LICENSE_PLATE_REQUIRED: "Please enter your license plate number", FIRST_NAME_REQUIRED: "Please enter your first name", LAST_NAME_REQUIRED: "Please enter your last name", EMAIL_ADDRESS_REQUIRED: "Please enter your email address", EMAIL_ADDRESS_FORMAT: "Please enter a valid email address", + SERVICE_ZIP_REQUIRED: "Please enter your Service ZIP", + SERVICE_ZIP_FORMAT: "Please enter a valid Service ZIP", VIN_REQUIRED: "Please enter your VIN", VIN_FORMAT: "Please enter a valid VIN", }; diff --git a/src/constants/store-actions.js b/src/constants/store-actions.js index 8de78f70c..74dac5043 100644 --- a/src/constants/store-actions.js +++ b/src/constants/store-actions.js @@ -12,6 +12,7 @@ const storeActions = { LOOKUP_VEHICLE_BY_YMMS: "lookupVehicleByYmms", LOOKUP_VEHICLE_BY_VIN: "lookupVehicleByVin", LOOKUP_VIN_BY_PLATE: "lookupVinByPlate", + LOOKUP_VIN_BY_ADDRESS: "lookupVinByAddress", GET_PARTS_OR_QUESTIONS: "getPartsOrQuestions", SAVE_ORDER: "saveOrder", LOAD_ORDER: "loadOrder", diff --git a/src/helpers/validation-rules.spec.js b/src/helpers/validation-rules.spec.js index 323e05ff1..7e0c6304e 100644 --- a/src/helpers/validation-rules.spec.js +++ b/src/helpers/validation-rules.spec.js @@ -1,4 +1,5 @@ import { required } from "@/helpers/validation-rules"; +import { regex } from "@/helpers/validation-rules"; describe("validation-rules.vue", () => { test("required rules should return error if value missing", () => { @@ -15,15 +16,57 @@ describe("validation-rules.vue", () => { }); describe("validation-rules.vue", () => { - test("required rules should return true if value present", () => { - - //Arrange - const testFn = required("an error"); - - //Act - const testResponse = testFn('some value'); - - //Assert - expect(testResponse).toBe(true); - }); - }); \ No newline at end of file + test("required rules should return true if value present", () => { + + //Arrange + const testFn = required("an error"); + + //Act + const testResponse = testFn('some value'); + + //Assert + expect(testResponse).toBe(true); + }); +}); + +describe("validation-rules.vue", () => { + test("regex rules should return true if value is not present", () => { + + //Arrange + const testFn = regex(/(^\d{5}$)|(^\d{5}-\d{4}$)/, "an error"); // Using Zip regex + + //Act + const testResponse = testFn(); + + //Assert + expect(testResponse).toBe(true); + }); +}); + +describe("validation-rules.vue", () => { + test("regex rules should return false if value is present but does not match regular expression", () => { + + //Arrange + const testFn = regex(/(^\d{5}$)|(^\d{5}-\d{4}$)/, "an error"); // Using Zip regex + + //Act + const testResponse = testFn('4321'); // needs to be 5 numbers + + //Assert + expect(testResponse).toBe("an error"); + }); +}); + +describe("validation-rules.vue", () => { + test("regex rules should return true if value is present and does match regular expression", () => { + + //Arrange + const testFn = regex(/(^\d{5}$)|(^\d{5}-\d{4}$)/, "an error"); // Using Zip regex + + //Act + const testResponse = testFn('43213'); // needs to be 5 numbers + + //Assert + expect(testResponse).toBe(true); + }); +}); \ No newline at end of file diff --git a/src/layouts/address-lookup/address-lookup.spec.js1 b/src/layouts/address-lookup/address-lookup.spec.js1 index e69de29bb..43f3649b0 100644 --- a/src/layouts/address-lookup/address-lookup.spec.js1 +++ b/src/layouts/address-lookup/address-lookup.spec.js1 @@ -0,0 +1,244 @@ +// 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.dispatchNonBlockingStoreAction = 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 7a90ee123..c18ee545e 100644 --- a/src/layouts/address-lookup/address-lookup.vue +++ b/src/layouts/address-lookup/address-lookup.vue @@ -5,15 +5,54 @@ ref="theForm" v-slot="{ meta }" autocomplete="off" > -