diff --git a/jest.config.js b/jest.config.js index e36f1a70d..720237d10 100644 --- a/jest.config.js +++ b/jest.config.js @@ -16,7 +16,7 @@ module.exports = { testMatch: ["**/*.spec.(js|jsx|ts|tsx)|**/__tests__/*.(js|jsx|ts|tsx)"], coverageThreshold: { global: { - statements: 70, + statements: 85, }, }, }; diff --git a/src/common-components/button-question/button-question.vue b/src/common-components/button-question/button-question.vue index 97ebf1310..d86dcd2ce 100644 --- a/src/common-components/button-question/button-question.vue +++ b/src/common-components/button-question/button-question.vue @@ -10,14 +10,15 @@ {{groupName}} @@ -46,3 +47,15 @@ export default { }, }; + + \ No newline at end of file diff --git a/src/common-components/site-header/site-header.spec.js b/src/common-components/funnel-header/funnel-header.spec.js similarity index 68% rename from src/common-components/site-header/site-header.spec.js rename to src/common-components/funnel-header/funnel-header.spec.js index 2a471fe26..cae21a179 100644 --- a/src/common-components/site-header/site-header.spec.js +++ b/src/common-components/funnel-header/funnel-header.spec.js @@ -1,12 +1,12 @@ import { shallowMount } from "@vue/test-utils"; -import siteHeader from "./site-header"; +import funnelHeader from "./funnel-header"; -describe("siteHeader", () => { +describe("funnelHeader", () => { test("renders the logo image", () => { // Arrange // Act - const wrapper = shallowMount(siteHeader, { + const wrapper = shallowMount(funnelHeader, { propsData: { imageSrc: "image_url", }, diff --git a/src/common-components/site-header/site-header.vue b/src/common-components/funnel-header/funnel-header.vue similarity index 90% rename from src/common-components/site-header/site-header.vue rename to src/common-components/funnel-header/funnel-header.vue index 441b188c4..7b6c6efc6 100644 --- a/src/common-components/site-header/site-header.vue +++ b/src/common-components/funnel-header/funnel-header.vue @@ -9,11 +9,12 @@ + + diff --git a/src/common-components/vehicle-banner/vehicle-banner.vue b/src/common-components/vehicle-banner/vehicle-banner.vue index 51e8d8aa3..984657a47 100644 --- a/src/common-components/vehicle-banner/vehicle-banner.vue +++ b/src/common-components/vehicle-banner/vehicle-banner.vue @@ -1,5 +1,5 @@ - + { - if (Object.values(widgetNames).includes(widget.Type)) { + + let widgetWithReplacements = findAndReplaceGlobalStateValues(widget.Model, widget.Type); + + if (Object.values(widgetNames).includes(widgetWithReplacements.Type)) { // If we already have this widget, push it on the collection - if (widget.Type in pageDataFromCms) { - pageDataFromCms[widget.Type].push(widget.Model); + if (widgetWithReplacements.Type in pageDataFromCms) { + pageDataFromCms[widgetWithReplacements.Type].push(widgetWithReplacements.Model); return; } - pageDataFromCms[widget.Type] = [widget.Model]; + pageDataFromCms[widgetWithReplacements.Type] = [widgetWithReplacements.Model]; } }); return pageDataFromCms; }); } + +// Function to convert a string, into a matching global state item. +function mapStringToState(str) { + + // Pull all matches out of the string. + const regexExp = new RegExp('{(.*?):(.*?)}', 'g'); + const matches = [...str.matchAll(regexExp)]; + + // Our final string value that will be built from the matches. + let stringBuilder = ''; + + for (const match of matches) { + + // Reset store state for each match. + let storeState = store.state; + + for (const s of match[2].split('.')) { + if (storeState[s] != undefined) { + storeState = storeState[s]; + } else { + return false; + } + } + + // Concatenate the string. + stringBuilder = `${stringBuilder} ${storeState}`; + } + + return stringBuilder.trimStart(); +} + +// Parent function for processWidgetItemForReplacement. This will loop through the parent +// object and pass any objects that need additional processing to the processWidgetItemForReplacement function. +function findAndReplaceGlobalStateValues(widgetModel, widgetType) { + + const objWithReplacements = { + Type: widgetType, + Model: {} + }; + + Object.keys(widgetModel).forEach(key => { + + let modelWithReplacements = processWidgetItemForReplacement(widgetModel, key); + + objWithReplacements.Model[key] = modelWithReplacements; + }); + + return objWithReplacements; +} + +// This function will process the widget item and replace any global state variables with their values. +// This is a recursive function, it will call itself until it runs out of items to iterate on given the object. +function processWidgetItemForReplacement(widgetModel, key) { + // If we have a string, and it needs to be replaced. + if (typeof widgetModel[key] === 'string') { + if (widgetModel[key].includes('{globalState:')) { + widgetModel[key] = mapStringToState(widgetModel[key]); + } + return widgetModel[key]; + } + + // If we have an object. array, etc + if (typeof widgetModel[key] === 'object' && Object.keys(widgetModel[key]).length) { + Object.keys(widgetModel[key]).forEach(item => { + processWidgetItemForReplacement(widgetModel[key], item); + }); + + return widgetModel[key]; + } + + return widgetModel[key]; +} \ No newline at end of file diff --git a/src/helpers/cms-helper.spec.js b/src/helpers/cms-helper.spec.js index e9bd66653..0f03ab604 100644 --- a/src/helpers/cms-helper.spec.js +++ b/src/helpers/cms-helper.spec.js @@ -3,50 +3,138 @@ import { dispatch } from "@/store"; jest.mock("@/store", () => ({ dispatch: jest.fn(), + state: { + order: { vehicle: { year: "2019", make: "Acura" } } + } })); -it("cms-content-helper: Should return data from CMS", () => { - // Arrange - const cmsMockData = { - Result: [ - { - Type: "VehicleBannerWidget", - Model: { - ImageId: "28452dcb-7762-4cc9-ab09-7643d0b89203", - GenericVehicleImage: - "https://digitalconsumercms-dev.safelite.com/images/default-source/default-album/blurred-image.jpg?sfvrsn=a6ce3034_3", - GenericVehicleImageFilePath: - "images/default-source/default-album/blurred-image.jpg", - }, - }, - { - Type: "PageHeaderWidget", - Model: { - HeaderText: "Select a year to get started", - }, - }, - { - Type: "PageHeaderWidget", - Model: { - HeaderText: "Select a model", - }, - }, - { - Type: "RadioQuestionWidget", - Model: { - QuestionText: "What year is your vehicle?", - }, - }, - ], - }; - dispatch.mockImplementation(() => Promise.resolve({ data: cmsMockData })); +describe("cms-content-helper.js", () => { + it("Should return data from CMS", () => { + // Arrange + const cmsMockData = { + Result: [ + { + Type: "PageHeaderWidget", + Model: { + HeaderText: "Select a year to get started", + }, + }, + { + Type: "PageHeaderWidget", + Model: { + HeaderText: "Select a model", + }, + }, + { + Type: "RadioQuestionWidget", + Model: { + QuestionText: "What year is your vehicle?", + }, + }, + ], + }; + + dispatch.mockImplementation(() => Promise.resolve({ data: cmsMockData })); + + // Act + fetchCmsContentForPage("testPage").then((response) => { + // Assert + expect(response.PageHeaderWidget[0].HeaderText).toEqual( + "Select a year to get started" + ); + }); + }); - // Act - fetchCmsContentForPage("testPage").then((response) => { - // Assert - expect(response.PageHeaderWidget[0].HeaderText).toEqual( - "Select a year to get started" - ); +}); + +describe("cms-content-helper.js", () => { + it("Should replace strings for global state", () => { + const cmsMockData = { + Result: [ + { + Type: "PageHeaderWidget", + Model: { + HeaderText: "{globalState:order.vehicle.year}", + }, + }, + ], + }; + + dispatch.mockImplementation(() => Promise.resolve({ data: cmsMockData })); + + fetchCmsContentForPage("testPage").then((response) => { + // Assert + expect(response.PageHeaderWidget[0].HeaderText).toEqual( + "2019" + ); + }); }); }); + +describe("cms-content-helper.js", () => { + it("Should replace strings for global state, and leave others the same", () => { + + const cmsMockData = { + Result: [ + { + Type: "PageHeaderWidget", + Model: { + HeaderText: "{globalState:order.vehicle.year}", + }, + }, + { + Type: "RadioQuestionWidget", + Model: { + ExampleText: "My widget value!", + }, + }, + ], + }; + + dispatch.mockImplementation(() => Promise.resolve({ data: cmsMockData })); + + fetchCmsContentForPage("testPage").then((response) => { + // Assert + expect(response.PageHeaderWidget[0].HeaderText).toEqual("2019"); + expect(response.RadioQuestionWidget[0].ExampleText).toEqual("My widget value!"); + + }); + }); +}); + +describe("cms-content-helper.js", () => { + it("Should replace strings for global state in nested objects", () => { + + const cmsMockData = { + Result: [ + { + Type: "PageHeaderWidget", + Model: { + HeaderText: "{globalState:order.vehicle.year}", + }, + }, + { + Type: "RadioQuestionWidget", + Model: { + OtherObjectInside: { + ExampleText: "{globalState:order.vehicle.make}", + } + }, + }, + ], + }; + + dispatch.mockImplementation(() => Promise.resolve({ data: cmsMockData })); + + fetchCmsContentForPage("testPage").then((response) => { + // Assert + + expect(response.PageHeaderWidget[0].HeaderText).toEqual("2019"); + expect(response.RadioQuestionWidget[0].OtherObjectInside.ExampleText).toEqual("Acura"); + + }); + }); +}); + +test.todo("String cannot be mapped to global state"); \ No newline at end of file diff --git a/src/helpers/unit-test-helper.js b/src/helpers/unit-test-helper.js index 5b5deb521..268a8e524 100644 --- a/src/helpers/unit-test-helper.js +++ b/src/helpers/unit-test-helper.js @@ -1,5 +1,6 @@ import { storeActions } from "@/constants/store-actions"; import { storeMutations } from "@/constants/store-mutations.js"; +import { navigationScenarios } from "@/router/router-constants/navigation-scenarios.js"; export function getMountOptions(mockData) { // Define our mocks to attached to the 'global' object for Vue/Jest. @@ -21,6 +22,7 @@ export function getMountOptions(mockData) { // Mock const files mocks.storeActions = storeActions; mocks.storeMutations = storeMutations; + mocks.navigationScenarios = navigationScenarios; // Mock $store and $router when accessing this.$store/$router mocks.$store = mockData.store; diff --git a/src/layouts/component-test/component-test.vue b/src/layouts/component-test/component-test.vue index b2452d700..35a8c2a2d 100644 --- a/src/layouts/component-test/component-test.vue +++ b/src/layouts/component-test/component-test.vue @@ -39,7 +39,10 @@ - + @@ -573,7 +576,7 @@ - @@ -601,7 +604,7 @@ import listCard from "@/ux-components/list-card/list-card"; import listButton from "@/ux-components/list-button/list-button"; import alert from "@/ux-components/alert/alert"; import vehicleBanner from "@/common-components/vehicle-banner/vehicle-banner"; -import siteHeader from "@/common-components/site-header/site-header"; +import funnelHeader from "@/common-components/funnel-header/funnel-header"; import listButtonHorizontal from "@/ux-components/list-button-horizontal/list-button-horizontal"; import checkbox from "@/ux-components/checkbox/checkbox"; export default { @@ -616,7 +619,8 @@ export default { vehicleBanner, listButtonHorizontal, siteHeader, - checkbox + checkbox, + funnelHeader, }, data() { return { diff --git a/src/layouts/vehicle-make/make-question/make-question.spec.js b/src/layouts/vehicle-make/make-question/make-question.spec.js new file mode 100644 index 000000000..84dd4f7e8 --- /dev/null +++ b/src/layouts/vehicle-make/make-question/make-question.spec.js @@ -0,0 +1,82 @@ +import makeQuestion from "@/layouts/vehicle-make/make-question/make-question"; +import { shallowMount } from "@vue/test-utils"; +import { getMountOptions } from "@/helpers/unit-test-helper.js"; +import store from "@/store"; +jest.mock("@/store", () => { return {}; }, {virtual: true}); + +describe("make-question.vue", () => { + test("Selected make is emitted upon selection.", async () => { + + //Arrange + const { wrapper } = setupMocks({ modelValueProp: "honda" }); + const makeToSelect = "ford"; + + //Act + wrapper.setData({ selectedMake: makeToSelect }); + await wrapper.vm.$nextTick(); + + //Assert + expect(wrapper.emitted()["update:modelValue"][0]).toEqual(["ford"]); + }); +}); + +describe("make-question.vue", () => { + test("CMS question text is used as radio question text.", async () => { + + //Arrange + const { wrapper, cmsContent } = setupMocks({ cmsQuestionText: "What make is your vehicle?" }); + + //Act + makeQuestion.methods.initializeComponent.call(wrapper.vm, cmsContent, null); + + //Assert + const buttonQuestionComponent = await wrapper.findComponent({ name: "buttonQuestion" }); + expect(buttonQuestionComponent.attributes("questiontext")).toBe("What make is your vehicle?"); + }); +}); + +describe("make-question.vue", () => { + test("Data from store api are used as radio question answers.", async () => { + + //Arrange + const { wrapper, cmsContent } = setupMocks({ dataFromStoreApi: ["honda", "ford", "dodge"] }); + + //Act + const initialData = makeQuestion.methods.loadInitialData.call(wrapper.vm); + makeQuestion.methods.initializeComponent.call(wrapper.vm, cmsContent, initialData); + + //Assert + const buttonQuestionComponent = await wrapper.findComponent({ name: "buttonQuestion" }); + expect(buttonQuestionComponent.attributes("answers")).toBe("honda,ford,dodge"); + }); +}); + + +function setupMocks({ + modelValueProp = "1900", + cmsQuestionText = "CMS text goes here", + dataFromStoreApi = [], +}) { + + //Mock store + store.dispatch = jest.fn(() => dataFromStoreApi); + store.getters = { vehicle: {year: 2019} }; + const mountOptions = getMountOptions({ + store: { + dispatch: store.dispatch, + getters: store.getters, + }, + }); + + //Mock props + mountOptions.propsData = { + modelValue: modelValueProp, + }; + const wrapper = shallowMount(makeQuestion, mountOptions); + + //Mock CMS content + const cmsContent = { + QuestionText: cmsQuestionText + }; + return { wrapper, cmsContent }; +} \ No newline at end of file diff --git a/src/layouts/vehicle-make/make-question/make-question.vue b/src/layouts/vehicle-make/make-question/make-question.vue new file mode 100644 index 000000000..765c7a542 --- /dev/null +++ b/src/layouts/vehicle-make/make-question/make-question.vue @@ -0,0 +1,46 @@ + + + + + diff --git a/src/layouts/vehicle-make/vehicle-make.spec.js b/src/layouts/vehicle-make/vehicle-make.spec.js new file mode 100644 index 000000000..120f8de4f --- /dev/null +++ b/src/layouts/vehicle-make/vehicle-make.spec.js @@ -0,0 +1,120 @@ +import { shallowMount, flushPromises } from "@vue/test-utils"; +import { getMountOptions } from "@/helpers/unit-test-helper.js"; +import vehicleMake from "@/layouts/vehicle-make/vehicle-make.vue"; +import makeQuestion from "@/layouts/vehicle-make/make-question/make-question"; +import { settleAllPromises } from "@/helpers/layout-helper.js"; +import { nextTick } from "vue"; + +// Mock our module for promises. +jest.mock("@/helpers/layout-helper.js", () => ({ + settleAllPromises: jest.fn(), +})); + +describe("vehicle-make.vue", () => { + test("Make question component is initized with api data", async (done) => { + + //Arrange + const radioQuestionCmsContent = { QuestionText: "What make is your vehicle?" }; + const makeQuestionInitialData = ["honda", "ford", "dodge"]; + const { wrapper, apiPromise } = setupMocks( { + radioQuestionCmsContent: radioQuestionCmsContent, + makeQuestionInitialData: makeQuestionInitialData, + } ); + + //Act + vehicleMake.beforeRouteEnter.call(wrapper.vm, { query: { fmgPage: "vehicle-make" } }, undefined, (c) => c(wrapper.vm)); + + //Assert + apiPromise.finally(() => { + expect(makeQuestion.methods.initializeComponent).toHaveBeenCalledWith(radioQuestionCmsContent, makeQuestionInitialData); + done(); + }); + }); +}); + +describe("vehicle-make.vue", () => { + test("Page header is passed data from CMS", async (done) => { + + //Arrange + const { wrapper, apiPromise } = setupMocks( { pageHeaderWidgetHeaderText: "Select a make to get started" }); + + //Act + vehicleMake.beforeRouteEnter.call(wrapper.vm, { query: { fmgPage: "vehicle-make" } }, undefined, (c) => c(wrapper.vm)); + + //Assert + const header = await wrapper.find(".Header"); + apiPromise.finally(() => { + expect(header.attributes("text")).toEqual("Select a make to get started"); + done(); + }); + }); +}); + +describe("vehicle-make.vue", () => { + test("BackButtonAction triggers a router.navigate change", async (done) => { + + //Arrange + const { wrapper, apiPromise } = setupMocks( { + pageHeaderWidgetHeaderText: "Select a make to get started", + mountOptionsMockData: { + router: { + navigate: jest.fn() + }, + }, + }); + + //Act + vehicleMake.beforeRouteEnter.call(wrapper.vm, { query: { fmgPage: "vehicle-make" } }, undefined, (c) => c(wrapper.vm)); + wrapper.vm.backButtonAction(); + await nextTick(); + + //Assert + apiPromise.finally(() => { + expect(wrapper.vm.$router.navigate).toHaveBeenCalled(); + done(); + }); + }); +}); + +function setupMocks({ + radioQuestionCmsContent = {}, + makeQuestionInitialData = {}, + pageHeaderWidgetHeaderText = {}, + mountOptionsMockData = {}, +}) { + + //Mock api responses + const apiResponses = { + cmsContent: { + PageHeaderWidget: [{ HeaderText: pageHeaderWidgetHeaderText }], + RadioQuestionWidget: [radioQuestionCmsContent], + VehicleBannerWidget: [ + { + GenericVehicleImage: + "https://digitalconsumercms-dev.safelite.com/images/default-source/default-album/blurred-image.jpg?sfvrsn=a6ce3034_3", + }, + ], + SiteHeaderWidget: [ + { + LogoImage: + "https://digitalconsumercms-dev.safelite.com/images/default-source/default-album/safelite-logo.svg?sfvrsn=45e7ed06_3", + }, + ], + }, + makeQuestionInitialData: makeQuestionInitialData, + }; + const apiPromise = Promise.resolve(apiResponses); + settleAllPromises.mockImplementation(() => apiPromise); + + //Mock make question methods + makeQuestion.methods = { + loadInitialData: jest.fn(), + initializeComponent: jest.fn(), + }; + const mountOptions = getMountOptions(mountOptionsMockData); + const wrapper = shallowMount(vehicleMake, mountOptions); + const makeQuestionWrapper = wrapper.findComponent({ name: "makeQuestion" }); + makeQuestionWrapper.vm.initializeComponent = makeQuestion.methods.initializeComponent; + + return { wrapper, apiPromise }; +} \ No newline at end of file diff --git a/src/layouts/vehicle-make/vehicle-make.vue b/src/layouts/vehicle-make/vehicle-make.vue index f04bea0ec..be5e925ea 100644 --- a/src/layouts/vehicle-make/vehicle-make.vue +++ b/src/layouts/vehicle-make/vehicle-make.vue @@ -1,3 +1,90 @@ - Vehicle Make Layout - Do Something Cool Here - \ No newline at end of file + + + + + + + + + + + + + diff --git a/src/layouts/vehicle-model/model-question/model-question.spec.js b/src/layouts/vehicle-model/model-question/model-question.spec.js new file mode 100644 index 000000000..d6259eb72 --- /dev/null +++ b/src/layouts/vehicle-model/model-question/model-question.spec.js @@ -0,0 +1,82 @@ +import modelQuestion from "@/layouts/vehicle-model/model-question/model-question"; +import { shallowMount } from "@vue/test-utils"; +import { getMountOptions } from "@/helpers/unit-test-helper.js"; +import store from "@/store"; +jest.mock("@/store", () => { return {}; }, {virtual: true}); + +describe("model-question.vue", () => { + test("Selected model is emitted upon selection.", async () => { + + //Arrange + const { wrapper } = setupMocks({ modelValueProp: "Accord" }); + const modelToSelect = "Civic"; + + //Act + wrapper.setData({ selectedModel: modelToSelect }); + await wrapper.vm.$nextTick(); + + //Assert + expect(wrapper.emitted()["update:modelValue"][0]).toEqual(["Civic"]); + }); +}); + +describe("model-question.vue", () => { + test("CMS question text is used as radio question text.", async () => { + + //Arrange + const { wrapper, cmsContent } = setupMocks({ cmsQuestionText: "What model is your vehicle?" }); + + //Act + modelQuestion.methods.initializeComponent.call(wrapper.vm, cmsContent, null); + + //Assert + const buttonQuestionComponent = await wrapper.findComponent({ name: "buttonQuestion" }); + expect(buttonQuestionComponent.attributes("questiontext")).toBe("What model is your vehicle?"); + }); +}); + +describe("model-question.vue", () => { + test("Data from store api are used as radio question answers.", async () => { + + //Arrange + const { wrapper, cmsContent } = setupMocks({ dataFromStoreApi: ["accord", "civic", "insight"] }); + + //Act + const initialData = modelQuestion.methods.loadInitialData.call(wrapper.vm); + modelQuestion.methods.initializeComponent.call(wrapper.vm, cmsContent, initialData); + + //Assert + const buttonQuestionComponent = await wrapper.findComponent({ name: "buttonQuestion" }); + expect(buttonQuestionComponent.attributes("answers")).toBe("accord,civic,insight"); + }); +}); + + +function setupMocks({ + modelValueProp = "1900", + cmsQuestionText = "CMS text goes here", + dataFromStoreApi = [], +}) { + + //Mock store + store.dispatch = jest.fn(() => dataFromStoreApi); + store.getters = { vehicle: {year: 2019, make: 'honda'} }; + const mountOptions = getMountOptions({ + store: { + dispatch: store.dispatch, + getters: store.getters, + }, + }); + + //Mock props + mountOptions.propsData = { + modelValue: modelValueProp, + }; + const wrapper = shallowMount(modelQuestion, mountOptions); + + //Mock CMS content + const cmsContent = { + QuestionText: cmsQuestionText + }; + return { wrapper, cmsContent }; +} \ No newline at end of file diff --git a/src/layouts/vehicle-model/model-question/model-question.vue b/src/layouts/vehicle-model/model-question/model-question.vue new file mode 100644 index 000000000..95b72909b --- /dev/null +++ b/src/layouts/vehicle-model/model-question/model-question.vue @@ -0,0 +1,46 @@ + + + + + diff --git a/src/layouts/vehicle-model/vehicle-model.spec.js b/src/layouts/vehicle-model/vehicle-model.spec.js new file mode 100644 index 000000000..8ee3885de --- /dev/null +++ b/src/layouts/vehicle-model/vehicle-model.spec.js @@ -0,0 +1,93 @@ +import { shallowMount, flushPromises } from "@vue/test-utils"; +import { getMountOptions } from "@/helpers/unit-test-helper.js"; +import vehicleModel from "@/layouts/vehicle-model/vehicle-model.vue"; +import modelQuestion from "@/layouts/vehicle-model/model-question/model-question"; +import { settleAllPromises } from "@/helpers/layout-helper.js"; +import { nextTick } from "vue"; + +// Mock our module for promises. +jest.mock("@/helpers/layout-helper.js", () => ({ + settleAllPromises: jest.fn(), +})); + +describe("vehicle-model.vue", () => { + test("Model question component is initized with api data", async (done) => { + + //Arange + const radioQuestionCmsContent = { QuestionText: "What model is your vehicle?" }; + const modelQuestionInitialData = ["accord", "civic", "insight"]; + const { wrapper, apiPromise } = setupMocks( { + radioQuestionCmsContent: radioQuestionCmsContent, + modelQuestionInitialData: modelQuestionInitialData, + } ); + + //Act + vehicleModel.beforeRouteEnter.call(wrapper.vm, { query: { fmgPage: "vehicle-model" } }, undefined, (c) => c(wrapper.vm)); + + //Assert + apiPromise.finally(() => { + expect(modelQuestion.methods.initializeComponent).toHaveBeenCalledWith(radioQuestionCmsContent, modelQuestionInitialData); + done(); + }); + }); +}); + +describe("vehicle-model.vue", () => { + test("Page header is passed data from CMS", async (done) => { + + //Arange + const { wrapper, apiPromise } = setupMocks( { pageHeaderWidgetHeaderText: "Select a model to get started" }); + + //Act + vehicleModel.beforeRouteEnter.call(wrapper.vm, { query: { fmgPage: "vehicle-model" } }, undefined, (c) => c(wrapper.vm)); + + //Assert + const header = await wrapper.find(".Header"); + apiPromise.finally(() => { + expect(header.attributes("text")).toEqual("Select a model to get started"); + done(); + }); + }); +}); + +function setupMocks({ + radioQuestionCmsContent = {}, + modelQuestionInitialData = {}, + pageHeaderWidgetHeaderText = {}, +}) { + + //Mock api responses + const apiResponses = { + cmsContent: { + PageHeaderWidget: [{ HeaderText: pageHeaderWidgetHeaderText }], + RadioQuestionWidget: [radioQuestionCmsContent], + VehicleBannerWidget: [ + { + GenericVehicleImage: + "https://digitalconsumercms-dev.safelite.com/images/default-source/default-album/blurred-image.jpg?sfvrsn=a6ce3034_3", + }, + ], + SiteHeaderWidget: [ + { + LogoImage: + "https://digitalconsumercms-dev.safelite.com/images/default-source/default-album/safelite-logo.svg?sfvrsn=45e7ed06_3", + }, + ], + }, + modelQuestionInitialData: modelQuestionInitialData, + }; + const apiPromise = Promise.resolve(apiResponses); + settleAllPromises.mockImplementation(() => apiPromise); + + //Mock model question methods + modelQuestion.methods = { + loadInitialData: jest.fn(), + initializeComponent: jest.fn(), + }; + const mountOptions = getMountOptions({ }); + const wrapper = shallowMount(vehicleModel, mountOptions); + const modelQuestionWrapper = wrapper.findComponent({ name: "modelQuestion" }); + modelQuestionWrapper.vm.initializeComponent = modelQuestion.methods.initializeComponent; + + return { wrapper, apiPromise }; +} \ No newline at end of file diff --git a/src/layouts/vehicle-model/vehicle-model.vue b/src/layouts/vehicle-model/vehicle-model.vue new file mode 100644 index 000000000..1d86c99b4 --- /dev/null +++ b/src/layouts/vehicle-model/vehicle-model.vue @@ -0,0 +1,90 @@ + + + + + + + + + + + + + + diff --git a/src/layouts/vehicle-style/style-question/style-question.spec.js b/src/layouts/vehicle-style/style-question/style-question.spec.js new file mode 100644 index 000000000..f03ce5e96 --- /dev/null +++ b/src/layouts/vehicle-style/style-question/style-question.spec.js @@ -0,0 +1,82 @@ +import styleQuestion from "@/layouts/vehicle-style/style-question/style-question"; +import { shallowMount } from "@vue/test-utils"; +import { getMountOptions } from "@/helpers/unit-test-helper.js"; +import store from "@/store"; +jest.mock("@/store", () => { return {}; }, {virtual: true}); + +describe("style-question.vue", () => { + test("Selected style is emitted upon selection.", async () => { + + //Arrange + const { wrapper } = setupMocks({ modelValueProp: "2 Door" }); + const styleToSelect = "4 Door"; + + //Act + wrapper.setData({ selectedStyle: styleToSelect }); + await wrapper.vm.$nextTick(); + + //Assert + expect(wrapper.emitted()["update:modelValue"][0]).toEqual(["4 Door"]); + }); +}); + +describe("style-question.vue", () => { + test("CMS question text is used as radio question text.", async () => { + + //Arrange + const { wrapper, cmsContent } = setupMocks({ cmsQuestionText: "What style is your vehicle?" }); + + //Act + styleQuestion.methods.initializeComponent.call(wrapper.vm, cmsContent, null); + + //Assert + const buttonQuestionComponent = await wrapper.findComponent({ name: "buttonQuestion" }); + expect(buttonQuestionComponent.attributes("questiontext")).toBe("What style is your vehicle?"); + }); +}); + +describe("style-question.vue", () => { + test("Data from store api are used as radio question answers.", async () => { + + //Arrange + const { wrapper, cmsContent } = setupMocks({ dataFromStoreApi: ["2 Door", "4 Door"] }); + + //Act + const initialData = styleQuestion.methods.loadInitialData.call(wrapper.vm); + styleQuestion.methods.initializeComponent.call(wrapper.vm, cmsContent, initialData); + + //Assert + const buttonQuestionComponent = await wrapper.findComponent({ name: "buttonQuestion" }); + expect(buttonQuestionComponent.attributes("answers")).toBe("2 Door,4 Door"); + }); +}); + + +function setupMocks({ + modelValueProp = "1900", + cmsQuestionText = "CMS text goes here", + dataFromStoreApi = [], +}) { + + //Mock store + store.dispatch = jest.fn(() => dataFromStoreApi); + store.getters = { vehicle: {year: 2019, make: 'honda', model: 'civc'} }; + const mountOptions = getMountOptions({ + store: { + dispatch: store.dispatch, + getters: store.getters, + }, + }); + + //Mock props + mountOptions.propsData = { + modelValue: modelValueProp, + }; + const wrapper = shallowMount(styleQuestion, mountOptions); + + //Mock CMS content + const cmsContent = { + QuestionText: cmsQuestionText + }; + return { wrapper, cmsContent }; +} \ No newline at end of file diff --git a/src/layouts/vehicle-style/style-question/style-question.vue b/src/layouts/vehicle-style/style-question/style-question.vue new file mode 100644 index 000000000..17a008473 --- /dev/null +++ b/src/layouts/vehicle-style/style-question/style-question.vue @@ -0,0 +1,46 @@ + + + + + diff --git a/src/layouts/vehicle-style/vehicle-style.spec.js b/src/layouts/vehicle-style/vehicle-style.spec.js new file mode 100644 index 000000000..90dd6e926 --- /dev/null +++ b/src/layouts/vehicle-style/vehicle-style.spec.js @@ -0,0 +1,120 @@ +import { shallowMount, flushPromises } from "@vue/test-utils"; +import { getMountOptions } from "@/helpers/unit-test-helper.js"; +import vehicleStyle from "@/layouts/vehicle-style/vehicle-style.vue"; +import styleQuestion from "@/layouts/vehicle-style/style-question/style-question"; +import { settleAllPromises } from "@/helpers/layout-helper.js"; +import { nextTick } from "vue"; + +// Mock our module for promises. +jest.mock("@/helpers/layout-helper.js", () => ({ + settleAllPromises: jest.fn(), +})); + +describe("vehicle-style.vue", () => { + test("Style question component is initized with api data", async (done) => { + + //Arrange + const radioQuestionCmsContent = { QuestionText: "What style is your vehicle?" }; + const styleQuestionInitialData = ["2 Door", "4 Door"]; + const { wrapper, apiPromise } = setupMocks( { + radioQuestionCmsContent: radioQuestionCmsContent, + styleQuestionInitialData: styleQuestionInitialData, + } ); + + //Act + vehicleStyle.beforeRouteEnter.call(wrapper.vm, { query: { fmgPage: "vehicle-style" } }, undefined, (c) => c(wrapper.vm)); + + //Assert + apiPromise.finally(() => { + expect(styleQuestion.methods.initializeComponent).toHaveBeenCalledWith(radioQuestionCmsContent, styleQuestionInitialData); + done(); + }); + }); +}); + +describe("vehicle-style.vue", () => { + test("Page header is passed data from CMS", async (done) => { + + //Arrange + const { wrapper, apiPromise } = setupMocks( { pageHeaderWidgetHeaderText: "Select a style to get started" }); + + //Act + vehicleStyle.beforeRouteEnter.call(wrapper.vm, { query: { fmgPage: "vehicle-style" } }, undefined, (c) => c(wrapper.vm)); + + //Assert + const header = await wrapper.find(".Header"); + apiPromise.finally(() => { + expect(header.attributes("text")).toEqual("Select a style to get started"); + done(); + }); + }); +}); + +describe("vehicle-style.vue", () => { + test("BackButtonAction triggers a router.navigate change", async (done) => { + + //Arrange + const { wrapper, apiPromise } = setupMocks( { + pageHeaderWidgetHeaderText: "Select a style to get started", + mountOptionsMockData: { + router: { + navigate: jest.fn() + }, + }, + }); + + //Act + vehicleStyle.beforeRouteEnter.call(wrapper.vm, { query: { fmgPage: "vehicle-style" } }, undefined, (c) => c(wrapper.vm)); + wrapper.vm.backButtonAction(); + await nextTick(); + + //Assert + apiPromise.finally(() => { + expect(wrapper.vm.$router.navigate).toHaveBeenCalled(); + done(); + }); + }); +}); + +function setupMocks({ + radioQuestionCmsContent = {}, + styleQuestionInitialData = {}, + pageHeaderWidgetHeaderText = {}, + mountOptionsMockData = {}, +}) { + + //Mock api responses + const apiResponses = { + cmsContent: { + PageHeaderWidget: [{ HeaderText: pageHeaderWidgetHeaderText }], + RadioQuestionWidget: [radioQuestionCmsContent], + VehicleBannerWidget: [ + { + GenericVehicleImage: + "https://digitalconsumercms-dev.safelite.com/images/default-source/default-album/blurred-image.jpg?sfvrsn=a6ce3034_3", + }, + ], + SiteHeaderWidget: [ + { + LogoImage: + "https://digitalconsumercms-dev.safelite.com/images/default-source/default-album/safelite-logo.svg?sfvrsn=45e7ed06_3", + }, + ], + }, + styleQuestionInitialData: styleQuestionInitialData, + }; + const apiPromise = Promise.resolve(apiResponses); + settleAllPromises.mockImplementation(() => apiPromise); + + //Mock style question methods + styleQuestion.methods = { + loadInitialData: jest.fn(), + initializeComponent: jest.fn(), + }; + const mountOptions = getMountOptions(mountOptionsMockData); + const wrapper = shallowMount(vehicleStyle, mountOptions); + const styleQuestionWrapper = wrapper.findComponent({ name: "styleQuestion" }); + styleQuestionWrapper.vm.initializeComponent = styleQuestion.methods.initializeComponent; + + return { wrapper, apiPromise }; +} \ No newline at end of file diff --git a/src/layouts/vehicle-style/vehicle-style.vue b/src/layouts/vehicle-style/vehicle-style.vue new file mode 100644 index 000000000..6b5d4bf86 --- /dev/null +++ b/src/layouts/vehicle-style/vehicle-style.vue @@ -0,0 +1,90 @@ + + + + + + + + + + + + + + diff --git a/src/layouts/vehicle-year/vehicle-year.spec.js b/src/layouts/vehicle-year/vehicle-year.spec.js index ae3d44a6a..594630ba4 100644 --- a/src/layouts/vehicle-year/vehicle-year.spec.js +++ b/src/layouts/vehicle-year/vehicle-year.spec.js @@ -13,7 +13,7 @@ jest.mock("@/helpers/layout-helper.js", () => ({ describe("vehicle-year.vue", () => { test("Year question component is initized with api data", async (done) => { - //Arange + //Arrange const radioQuestionCmsContent = { QuestionText: "What year is your vehicle?" }; const yearQuestionInitialData = ["2023", "2022", "2021"]; const { wrapper, apiPromise } = setupMocks( { @@ -35,7 +35,7 @@ describe("vehicle-year.vue", () => { describe("vehicle-year.vue", () => { test("Page header is passed data from CMS", async (done) => { - //Arange + //Arrange const { wrapper, apiPromise } = setupMocks( { pageHeaderWidgetHeaderText: "Select a year to get started" }); //Act @@ -54,6 +54,7 @@ function setupMocks({ radioQuestionCmsContent = {}, yearQuestionInitialData = {}, pageHeaderWidgetHeaderText = {}, + mountOptionsMockData = {}, }) { //Mock api responses @@ -84,7 +85,7 @@ function setupMocks({ loadInitialData: jest.fn(), initializeComponent: jest.fn(), }; - const mountOptions = getMountOptions({ }); + const mountOptions = getMountOptions(mountOptionsMockData); const wrapper = shallowMount(vehicleYear, mountOptions); const yearQuestionWrapper = wrapper.findComponent({ name: "yearQuestion" }); yearQuestionWrapper.vm.initializeComponent = yearQuestion.methods.initializeComponent; diff --git a/src/layouts/vehicle-year/vehicle-year.vue b/src/layouts/vehicle-year/vehicle-year.vue index 8dc9b72d9..48508adc2 100644 --- a/src/layouts/vehicle-year/vehicle-year.vue +++ b/src/layouts/vehicle-year/vehicle-year.vue @@ -1,10 +1,13 @@ - + - + @@ -14,14 +17,12 @@ - - diff --git a/src/mixins/base-mixin.js b/src/mixins/base-mixin.js index afe618125..14da7a16b 100644 --- a/src/mixins/base-mixin.js +++ b/src/mixins/base-mixin.js @@ -1,5 +1,6 @@ import { storeActions } from "@/constants/store-actions.js"; import { storeMutations } from "@/constants/store-mutations.js"; +import { navigationScenarios } from "@/router/router-constants/navigation-scenarios"; import { widgetNames } from "@/constants/widget-names.js"; export default { @@ -31,6 +32,9 @@ export default { storeMutations() { return storeMutations; }, + navigationScenarios() { + return navigationScenarios; + }, widgetNames() { return widgetNames; }, diff --git a/src/router/router-constants/navigation-scenarios.js b/src/router/router-constants/navigation-scenarios.js index 65947744e..48e36996b 100644 --- a/src/router/router-constants/navigation-scenarios.js +++ b/src/router/router-constants/navigation-scenarios.js @@ -2,6 +2,7 @@ const navigationScenarios = { SELECTED_YEAR: "SELECTED_YEAR", SELECTED_MODEL: "SELECTED_MODEL", SELECTED_MAKE: "SELECTED_MAKE", + SELECTED_STYLE: "SELECTED_STYLE", CLICKED_BACK: "CLICKED_BACK", }; diff --git a/src/router/router-constants/routing-table.js b/src/router/router-constants/routing-table.js index 826357f63..21dc6ca65 100644 --- a/src/router/router-constants/routing-table.js +++ b/src/router/router-constants/routing-table.js @@ -37,6 +37,19 @@ const routingTable = [ } ], }, + { + fmgPageValue: fmgPageValues.VEHICLE_STYLE, + maps: [ + { + scenario: navigationScenarios.SELECTED_STYLE, + destinationFmgPageValue: fmgPageValues.VEHICLE_STYLE, + }, + { + scenario: navigationScenarios.CLICKED_BACK, + destinationFmgPageValue: fmgPageValues.VEHICLE_MODEL, + } + ], + }, ]; -export { routingTable }; +export { routingTable }; \ No newline at end of file diff --git a/src/store/index.js b/src/store/index.js index 036694868..daf34e38c 100644 --- a/src/store/index.js +++ b/src/store/index.js @@ -64,6 +64,18 @@ export default createStore({ updateYear(state, year) { state.order.vehicle.year = year; }, + updateMake(state, make) { + state.order.vehicle.make = make; + }, + updateModel(state, model) { + state.order.vehicle.model = model; + }, + updateStyle(state, style) { + state.order.vehicle.style = style; + }, + }, + getters: { + vehicle: state => state.order.vehicle }, actions: { // Vehicle API Actions diff --git a/src/store/store.spec.js b/src/store/store.spec.js index 76acc0dbf..fcfca4576 100644 --- a/src/store/store.spec.js +++ b/src/store/store.spec.js @@ -118,6 +118,73 @@ describe("Actions", () => { // Assert expect(pageData).toBe("Page Info Data"); }); + + it("Should return data from url retrieved", async () => { + // Arrange + let returnData = []; + + // Act + globalMethods.callHttpClient.mockImplementation(() => { + return Promise.resolve({ + data: { + Result: "2018 Honda Civic", + }, + }); + }); + await store + .dispatch("lookupVehicleByYmms", { year: "2018", make: "Honda", model: "Civic", style: "2 Door"}) + .then((response) => { + returnData = response.data.Result; + }); + + // Assert + expect(returnData).toBe("2018 Honda Civic"); + }); + + it("Should return vehicle data from url retrieved", async () => { + // Arrange + let returnData = []; + + // Act + globalMethods.callHttpClient.mockImplementation(() => { + return Promise.resolve({ + data: { + Result: "2021 Honda Civic", + }, + }); + }); + await store + .dispatch("lookupVehicleByVin", { vin: "12345678"}) + .then((response) => { + returnData = response.data.Result; + }); + + // Assert + expect(returnData).toBe("2021 Honda Civic"); + }); + + it("Should return vehicle image data from url retrieved", async () => { + // Arrange + let returnData = []; + + // Act + globalMethods.callMockHttpClient = jest.fn(); + globalMethods.callMockHttpClient.mockImplementation(() => { + return Promise.resolve({ + data: { + Result: "2008_honda_civic.jpg", + }, + }); + }); + await store + .dispatch("getEvoxImage", { relativeUrl: "evox_image.com"}) + .then((response) => { + returnData = response.data.Result; + }); + + // Assert + expect(returnData).toBe("2008_honda_civic.jpg"); + }); }); describe("Mutations", () => { @@ -128,4 +195,34 @@ describe("Mutations", () => { // Assert expect(store.state.order.vehicle.year).toBe(2020); }); + + it("Should update the make property in the store", () => { + // Act + store.commit("updateMake", "Honda"); + + // Assert + expect(store.state.order.vehicle.make).toBe("Honda"); + }); + + it("Should update the model property in the store", () => { + // Act + store.commit("updateModel", "Civic"); + + // Assert + expect(store.state.order.vehicle.model).toBe("Civic"); + }); + + it("Should update the style property in the store", () => { + // Act + store.commit("updateStyle", "2 Door"); + + // Assert + expect(store.state.order.vehicle.style).toBe("2 Door"); + }); +}); + +describe("Getters", () => { + const vehicle = store.getters.vehicle; + + expect(typeof vehicle).toBe('object'); }); diff --git a/src/ux-components/button-back/button-back.spec.js b/src/ux-components/button-back/button-back.spec.js index 3d0843e10..b8bc3256d 100644 --- a/src/ux-components/button-back/button-back.spec.js +++ b/src/ux-components/button-back/button-back.spec.js @@ -1 +1,23 @@ -test.todo("some test to be written in the future"); +import { shallowMount } from "@vue/test-utils"; +import buttonBack from "./button-back"; + +describe("back button", () => { + + test("renders a button", () => { + // Arrange + const myFunction = () => {}; + + // Act + const wrapper = shallowMount(buttonBack, { + propsData: { + backButtonAction: myFunction, + backButtonAccessibleText: "something", + }, + }); + + // Assert + expect(wrapper.find("button").exists()).toBe(true); + wrapper.unmount(); + }); + +}); diff --git a/src/ux-components/button-back/button-back.vue b/src/ux-components/button-back/button-back.vue index aa462accc..92145ac87 100644 --- a/src/ux-components/button-back/button-back.vue +++ b/src/ux-components/button-back/button-back.vue @@ -1,5 +1,10 @@ - + @@ -9,7 +14,18 @@ @@ -17,6 +33,9 @@ export default { .back-button-wrapper { border: none; background: transparent; + text-decoration: none; + border-bottom: none; + .button-back { display: inline-flex; position: relative; diff --git a/src/ux-components/header/header.spec.js b/src/ux-components/header/header.spec.js deleted file mode 100644 index 2c287ac79..000000000 --- a/src/ux-components/header/header.spec.js +++ /dev/null @@ -1,16 +0,0 @@ -import { shallowMount } from "@vue/test-utils"; -import Header from "./header"; - -describe("Header.vue", () => { - it("Should render the 'text' prop value as a span value for the header span text value.", () => { - // Act - const wrapper = shallowMount(Header, { - propsData: { - text: "Header Content", - }, - }); - - // Assert - expect(wrapper.find("h2").text()).toContain("Header Content"); - }); -}); diff --git a/src/ux-components/header/header.vue b/src/ux-components/header/header.vue deleted file mode 100644 index 3395a7e93..000000000 --- a/src/ux-components/header/header.vue +++ /dev/null @@ -1,22 +0,0 @@ - - - - {{ text }} - - - - - - - diff --git a/src/ux-components/list-button/list-button.vue b/src/ux-components/list-button/list-button.vue index 89de52e64..49109fb28 100644 --- a/src/ux-components/list-button/list-button.vue +++ b/src/ux-components/list-button/list-button.vue @@ -28,7 +28,7 @@ export default { props: { isMultiSelect: Boolean, groupName: String, /* Required, unique for each button GROUP */ - buttonID: String, /* Required, unique for each button. Used for button id, label and */ + buttonID: [Number,String], /* Required, unique for each button. Used for button id, label and */ isRequired: Boolean, /* Optional, default is false */ screenReaderOnlyText: String, /* Optional, copy to be read by screenreader */ buttonLabelSubCopy: String, /* Optional, used for multi-line buttons */
Vehicle Make Layout - Do Something Cool Here