diff --git a/src/common-components/dropdown-question/dropdown-question.spec.js b/src/common-components/dropdown-question/dropdown-question.spec.js index 381f3489..237e087c 100644 --- a/src/common-components/dropdown-question/dropdown-question.spec.js +++ b/src/common-components/dropdown-question/dropdown-question.spec.js @@ -152,7 +152,7 @@ describe("dropdownQuestion.vue", () => { const wrapper = shallowMount(dropdownQuestion, { propsData: { options: {}, - modelValue: 0, + modelValue: "0", }, mixins: [mockMixin] }); diff --git a/src/common-components/dropdown-question/dropdown-question.vue b/src/common-components/dropdown-question/dropdown-question.vue index 8e6ae0fa..43c81ccb 100644 --- a/src/common-components/dropdown-question/dropdown-question.vue +++ b/src/common-components/dropdown-question/dropdown-question.vue @@ -37,6 +37,8 @@ export default { disableAutoFill: Boolean, validationRules: String, cmsWidgetName: String, + hasError: Boolean, + errors: Array }, setup(props) { const propsClone = Object.assign({}, props); diff --git a/src/common-components/site-header/site-header.spec.js b/src/common-components/site-header/site-header.spec.js index ea255efb..0a2cc3c1 100644 --- a/src/common-components/site-header/site-header.spec.js +++ b/src/common-components/site-header/site-header.spec.js @@ -1,25 +1,25 @@ import siteHeader from "@/common-components/site-header/site-header"; import { shallowMount } from "@vue/test-utils"; +import { getMountOptions } from "@/helpers/unit-test-helper.js"; describe("site-header", () => { test("renders the logo image", () => { - const wrapper = shallowMount(siteHeader, { - setProps: { - cmsWidgetName: "header", - }, - mixins: [mockMixin] - }); + const wrapper = setupMocks({mountOptionsMockData: {}}); + expect(wrapper.find("img")).toBeTruthy(); wrapper.unmount(); }); }); -const mockMixin = { - methods: { - getCmsContent: jest.fn() - } +function setupMocks({ + mountOptionsMockData = {}, +}) { + + const mountOptions = getMountOptions(mountOptionsMockData); + const wrapper = shallowMount(siteHeader, mountOptions); + + return wrapper; } - diff --git a/src/common-components/site-header/site-header.vue b/src/common-components/site-header/site-header.vue index 76de47be..b9499a9f 100644 --- a/src/common-components/site-header/site-header.vue +++ b/src/common-components/site-header/site-header.vue @@ -1,14 +1,39 @@ diff --git a/src/common-components/textbox-question/textbox-question.vue b/src/common-components/textbox-question/textbox-question.vue index 7733c02b..3cb13e18 100644 --- a/src/common-components/textbox-question/textbox-question.vue +++ b/src/common-components/textbox-question/textbox-question.vue @@ -55,6 +55,7 @@ export default { validationRules: String, cmsWidgetName: String, maxLength: String, + disableAutoFill: Boolean, }, setup(props) { const propsClone = Object.assign({}, props); diff --git a/src/common-components/vehicle-banner/vehicle-banner.vue b/src/common-components/vehicle-banner/vehicle-banner.vue index 9fffd974..4ffebe12 100644 --- a/src/common-components/vehicle-banner/vehicle-banner.vue +++ b/src/common-components/vehicle-banner/vehicle-banner.vue @@ -9,7 +9,6 @@ \ No newline at end of file diff --git a/src/layouts/vehicle-model/vehicle-model.spec.js b/src/layouts/vehicle-model/vehicle-model.spec.js index 7360f795..2e47b7fb 100644 --- a/src/layouts/vehicle-model/vehicle-model.spec.js +++ b/src/layouts/vehicle-model/vehicle-model.spec.js @@ -117,13 +117,13 @@ function setupMocks({ //Mock api responses const apiResponses = { cmsContent: { - FunnelSubHeaderWidget: pageHeaderWidgetHeaderText, + SiteSubHeaderWidget: pageHeaderWidgetHeaderText, VehicleModelQuestion: buttonQuestionContent, VehicleBannerWidget: { GenericVehicleImage: "https://digitalisscms.dev.safelite.io/images/default-source/default-album/blurred-image.jpg", }, - FunnelHeaderWidget: { + SiteHeaderWidget: { LogoImage: "https://digitalisscms.dev.safelite.io/images/default-source/default-album/logos/insuranceLogo.jpg", }, diff --git a/src/router/index.js b/src/router/index.js index 711243cb..702fd1be 100644 --- a/src/router/index.js +++ b/src/router/index.js @@ -3,6 +3,8 @@ import { lazyLoadComponent } from "@/router/dynamic-routing/component-loader"; import {issPageValues} from '@/router/router-constants/issPage-values'; import { routingTable } from "@/router/router-constants/routing-table"; import { useMainStore } from '@/store'; +import eventBus from "@/helpers/event-bus/event-bus"; +import { globalEvents, globalEventTypes } from "@/constants/events"; const routes = [ { @@ -141,13 +143,25 @@ function getNavigationMap (scenario, currentRoute) { }; function GoToStartOn404(next) { - const errorPageName = issPageValues.ERROR_404; + const errorPageName = issPageValues.VEHICLE_YEAR; router.addRoute({ path: "/", name: errorPageName, component: lazyLoadComponent(errorPageName), }); + // Put item on the bus + eventBus.addEventToBus( + globalEvents.Categories.GLOBAL_ALERT, + globalEvents.SubCategories.PAGE_NOT_FOUND, + { + isDismissible: true, + messageCopy: "You can get a quote by starting on this page.", + messageHeadline: "We're sorry, something went wrong.", + type: globalEventTypes.Danger, + } + ); + next({ name: errorPageName, query: {issPage: errorPageName }, diff --git a/src/router/router-constants/issPage-values.js b/src/router/router-constants/issPage-values.js index 07ea81bd..c31fa083 100644 --- a/src/router/router-constants/issPage-values.js +++ b/src/router/router-constants/issPage-values.js @@ -1,5 +1,4 @@ export const issPageValues = { - ERROR_404: "error-404", WELCOME_PAGE: "welcome-page", VEHICLE_MAKE: "vehicle-make", VEHICLE_YEAR: "vehicle-year", diff --git a/src/store/index.js b/src/store/index.js index b9277311..26e4df41 100644 --- a/src/store/index.js +++ b/src/store/index.js @@ -37,7 +37,8 @@ const getDefaultState = () => { crmCustomerId: null, lastPageVisited: null, experiments: [], - triggeredSiteEntry: false + triggeredSiteEntry: false, + eventBus: [] }, }; }; @@ -49,6 +50,16 @@ export const useMainStore = defineStore({ state: () => state, getters: { vehicle: this.order.vehicle + }, + eventBusItem: (state) => ( eventCategory, eventSubCategory) => { + + const matchedEvent = state.applicationUser.eventBus.find( + ( { category, subCategory } ) => + category === eventCategory && subCategory === eventSubCategory + ); + return matchedEvent?.eventValue; + }, + eventBus: (state) => state.applicationUser.eventBus, }, actions: { @@ -137,6 +148,21 @@ export const useMainStore = defineStore({ this.order.vehicle.style = style; } }, + addEventToBus (event) { + this.applicationUser.eventBus.push(event); + }, + removeEventFromBus (eventData) { + const matchedEvent = this.applicationUser.eventBus.find( + ( {category, subCategory } ) => + category === eventData.category && subCategory === eventData.subCategory + ); + const itemIndex = this.applicationUser.eventBus.indexOf(matchedEvent); + + // If the item exists, remove it. + if (itemIndex > -1) { + this.applicationUser.eventBus.splice(itemIndex, 1); + } + }, // populate initial state populateInitialState() diff --git a/src/store/store.spec.js b/src/store/store.spec.js index c1a6c694..9f5db937 100644 --- a/src/store/store.spec.js +++ b/src/store/store.spec.js @@ -3,19 +3,83 @@ import { createApp } from 'vue'; import { createPinia } from "pinia"; import App from '@/App.vue'; -describe("Store", () => { - let store = null; - beforeAll(() => { - const vueApp = createApp(App); - const pinia = createPinia(); - vueApp.use(pinia); +describe("Store", () => { + + let store; + + const vueApp = createApp(App); + const pinia = createPinia(); + vueApp.use(pinia); + + beforeEach(() => { store = useMainStore(); + store.applicationUser.eventBus = []; + jest.resetAllMocks(); }) + it("Should Store Vehicle Year", () => { let testYear = "2001"; store.updateVehicleYear(testYear); expect(store.order.vehicle.year).toEqual(testYear); }); + + it("Should add events to the bus", () => { + let event = { + category: "TestCategory", + subCategory: "TestSubCategory", + eventValue: { + isDismissible: true, + messageCopy: "You can get a quote by starting on this page.", + messageHeadline: "We're sorry, something went wrong.", + type: "testType", + }, + }; + + store.addEventToBus(event); + + expect(store.applicationUser.eventBus[0]).toEqual(event); + }); + + it("Should remove events from the bus", () => { + let event = { + category: "TestCategory", + subCategory: "TestSubCategory", + eventValue: { + isDismissible: true, + messageCopy: "You can get a quote by starting on this page.", + messageHeadline: "We're sorry, something went wrong.", + type: "testType", + }, + }; + + store.addEventToBus(event); + + expect(store.applicationUser.eventBus.length).toBe(1); + + store.removeEventFromBus({ category: event.category, subCategory: event.subCategory }) + + expect(store.applicationUser.eventBus.length).toBe(0); + }); + + it("Should return correct event using the getter function eventBusItem", () => { + let event = { + category: "TestCategory", + subCategory: "TestSubCategory", + eventValue: { + isDismissible: true, + messageCopy: "You can get a quote by starting on this page.", + messageHeadline: "We're sorry, something went wrong.", + type: "testType", + }, + }; + + store.addEventToBus(event); + + const actual = store.eventBusItem(event.category, event.subCategory) + + expect(actual).toEqual(event.eventValue); + }); + }); \ No newline at end of file diff --git a/src/ux-components/alert/alert.spec.js b/src/ux-components/alert/alert.spec.js new file mode 100644 index 00000000..343323b9 --- /dev/null +++ b/src/ux-components/alert/alert.spec.js @@ -0,0 +1,222 @@ +import { shallowMount } from "@vue/test-utils"; +import { getMountOptions } from "@/helpers/unit-test-helper.js"; +import alert from "./alert"; + +describe("alert.vue", () => { + it("Should add class 'alert-dismissible' if isDismissible is true", async () => { + // Arrange + const wrapper = shallowMount( + alert, + setupMocks({ + propsData: { + isDismissible: true, + manualHeadline: "testHeader", + manualCopy: "testCopy", + }, + }) + ); + + const wrapperDiv = wrapper.find("div"); + + // Assert + expect(wrapperDiv.classes()).toContain("alert-dismissible"); + }); + + it("Should add specified alert class", async () => { + // Arrange + const wrapper = shallowMount( + alert, + setupMocks({ + propsData: { + alertClass: "warning", + manualHeadline: "testHeader", + manualCopy: "testCopy", + }, + }) + ); + + const wrapperDiv = wrapper.find("div"); + + // Assert + expect(wrapperDiv.classes()).toContain("warning"); + }); + + it("Should update alert Headline to manualHeadline datam entered and alert copy to manualCopy datam entered when no cmsWidgetName entered", async () => { + // Arrange + const wrapper = shallowMount(alert, setupMocks({})); + // Assert + expect(wrapper.vm.alertHeadline).toBe("testHeader"); + expect(wrapper.vm.alertCopy).toBe("testCopy"); + }); + + it("Should container a tag if the manualCopy contains a {routerLink: testName, testLink} placeholder", () => { + // Arrange & Act + const wrapper = shallowMount( + alert, + setupMocks({ + propsData: { + manualHeadline: "testHeader", + manualCopy: "testCopy with a {routerLink: testName, testLink} inside of it", + }, + stubs: ["router-link"], + }) + ); + // Assert + expect(wrapper.find("router-link").exists()).toBe(true); + }); + + it("Should contain 'n+1'

tags if the body copy has 'n'

tags", () => { + // Arrange & Act + const wrapper = shallowMount( + alert, + setupMocks({ + propsData: { + manualHeadline: "testHeader", + manualCopy: + "

testCopy with a {routerLink: testName, testLink} inside of it

and two paragraphs

", + }, + stubs: ["router-link"], + }) + ); + // Assert + expect(wrapper.findAll("p").length === 3).toBe(true); + }); + + it("Should call scrollIntoView() when the clientBoundingRect is not entirely in the viewport (out of view top)", () => { + // Arrange + var viewPortHeight = 200; + setUpViewPort(viewPortHeight); + + Element.prototype.getBoundingClientRect = jest.fn(() => { + return { top: -100, bottom: 200 }; + }); + + var mockScrollIntoView = jest.fn(); + Element.prototype.scrollIntoView = mockScrollIntoView; + + // Act + const wrapper = shallowMount(alert, setupMocks({})); + + // Assert + // This is an implementation detail - we just need to test that the final step of snapping + // the window to the alert is working. If using a different function to accomplish that + // just swap this out with the new function + expect(mockScrollIntoView).toHaveBeenCalled(); + }); + + it("Should call scrollIntoView() when the clientBoundingRect is not entirely in the viewport (bottom is hidden behind footer)", () => { + // Arrange + var viewPortHeight = 240; + setUpViewPort(viewPortHeight); + + Element.prototype.getBoundingClientRect = jest.fn(() => { + return { top: 100, bottom: 200 }; + }); + + var mockScrollIntoView = jest.fn(); + Element.prototype.scrollIntoView = mockScrollIntoView; + + // Act + const wrapper = shallowMount(alert, setupMocks({})); + + // Assert + // This is an implementation detail - we just need to test that the final step of snapping + // the window to the alert is working. If using a different function to accomplish that + // just swap this out with the new function + expect(mockScrollIntoView).toHaveBeenCalled(); + }); + + it("Should not call scrollIntoView() when the clientBoundingRect is not entirely in the viewport but 'shouldScrollToOnMount' is false", () => { + // Arrange + var viewPortHeight = 200; + setUpViewPort(viewPortHeight); + + Element.prototype.getBoundingClientRect = jest.fn(() => { + return { top: -100, bottom: 200 }; + }); + + var mockScrollIntoView = jest.fn(); + Element.prototype.scrollIntoView = mockScrollIntoView; + + // Act + const wrapper = shallowMount( + alert, + setupMocks({ + propsData: { + shouldScrollToOnMount: false, + manualHeadline: "testHeader", + manualCopy: "testCopy", + }, + }) + ); + + // Assert + // This is an implementation detail - we just need to test that the final step of snapping + // the window to the alert is working. If using a different function to accomplish that + // just swap this out with the new function + expect(mockScrollIntoView).not.toHaveBeenCalled(); + }); + + it("Should not call scrollIntoView() when the clientBoundingRect is entirely in the viewport", () => { + // Arrange + var viewPortHeight = 500; + setUpViewPort(viewPortHeight); + + Element.prototype.getBoundingClientRect = jest.fn(() => { + return { top: 100, bottom: 200 }; + }); + + var mockScrollIntoView = jest.fn(); + Element.prototype.scrollIntoView = mockScrollIntoView; + + // Act + const wrapper = shallowMount(alert, setupMocks({})); + + // Assert + // This is an implementation detail - we just need to test that the final step of snapping + // the window to the alert is working. If using a different function to accomplish that + // just swap this out with the new function + expect(mockScrollIntoView).not.toHaveBeenCalled(); + }); +}); + +const mockMixin = { + methods: { + getCmsContent: jest.fn(), + getFooterInfoBoxHeight: jest.fn(() => 50), + }, + computed: { + dynamicStrings: jest.fn(() => { + return { ROUTER_LINK: "routerLink:" }; + }), + }, +}; + +function setUpViewPort(height) { + Object.defineProperty(global.window, "innerHeight", { + writable: true, + configurable: true, + value: height, + }); + + Object.defineProperty(window.document.documentElement, "clientHeight", { + writable: true, + configurable: true, + value: height, + }); +} + +function setupMocks(mountOptionsMockData = {}) { + const defaultMountOptions = { + propsData: { + manualHeadline: "testHeader", + manualCopy: "testCopy", + }, + mixins: [mockMixin], + }; + const baseMountOptions = getMountOptions( + Object.assign(defaultMountOptions, mountOptionsMockData) + ); + const allMountOptions = Object.assign(defaultMountOptions, baseMountOptions); + return allMountOptions; +} diff --git a/src/ux-components/alert/alert.vue b/src/ux-components/alert/alert.vue new file mode 100644 index 00000000..d0fe33ac --- /dev/null +++ b/src/ux-components/alert/alert.vue @@ -0,0 +1,189 @@ + + + + +