From 3187b2edc7ba66eacd0650f20301300d029300eb Mon Sep 17 00:00:00 2001 From: Scott Kiener Date: Thu, 26 May 2022 09:21:48 -0400 Subject: [PATCH 01/32] CSR-563 | Alert fix and starting tests Alert was not always snapping into viewport correctly Barebones test outline - NOT ready for PR --- src/ux-components/alert/alert.spec.js | 17 +++++++++++++++++ src/ux-components/alert/alert.vue | 10 +--------- 2 files changed, 18 insertions(+), 9 deletions(-) diff --git a/src/ux-components/alert/alert.spec.js b/src/ux-components/alert/alert.spec.js index 59becf922..a0e65b12f 100644 --- a/src/ux-components/alert/alert.spec.js +++ b/src/ux-components/alert/alert.spec.js @@ -62,6 +62,23 @@ describe("alert.vue", () => { expect(wrapper.vm.alertCopy).toBe("testCopy"); }); + it("Should run scrollContainerToAlert function when the clientBoundingRect is not entirely in the viewport", () => { + // Arrange + const wrapper = shallowMount(alert, { + computed: { + splitAlertCopyForLink: { + get() { + return "TEST"; + }, + } + }, + mixins: [mockMixin] + }); + // Act + + // Assert + }); + }); const mockMixin = { diff --git a/src/ux-components/alert/alert.vue b/src/ux-components/alert/alert.vue index 5c0d7a270..1d983bd42 100644 --- a/src/ux-components/alert/alert.vue +++ b/src/ux-components/alert/alert.vue @@ -94,7 +94,7 @@ export default { if (this.shouldScrollToOnMount && this.$el.style.display != 'none') { var footerHeight = this.getFooterInfoBoxHeight(); if (!this.isAlertInViewport(footerHeight)) { - this.scrollContainerToAlert(footerHeight); + this.$el.scrollIntoView(true); // 'true' attempts to scroll element to top of viewport } } }, @@ -106,14 +106,6 @@ export default { rect.bottom <= (window.innerHeight - footerHeight || document.documentElement.clientHeight - footerHeight) ); }, - scrollContainerToAlert(footerHeight) { - // alert position on page + height of alert + footer height - var scrollToHeight = this.$el.scrollHeight + this.$el.offsetHeight + footerHeight; - // find the div wrapped by the form element - this is the scrollable container - // should be a more future-proof selector in case of CSS class changes - var pageContainerScrollable = document.querySelector('form > div'); - pageContainerScrollable.scrollTo(0, scrollToHeight); - }, }, mounted() { this.ensureAlertIsInViewPort(); From 11a4943e9f2abc4da0518d94fc62f2d2712b0500 Mon Sep 17 00:00:00 2001 From: Leah Schumann Date: Wed, 1 Jun 2022 08:10:19 -0400 Subject: [PATCH 02/32] addressQuestions component unit tests --- jest.config.js | 4 +- .../address-questions.spec.js | 130 ++++++++++++++++++ .../address-questions.spec.js1 | 0 .../address-questions/address-questions.vue | 16 +-- 4 files changed, 140 insertions(+), 10 deletions(-) create mode 100644 src/layouts/address-lookup/customer-questions/address-questions/address-questions.spec.js delete mode 100644 src/layouts/address-lookup/customer-questions/address-questions/address-questions.spec.js1 diff --git a/jest.config.js b/jest.config.js index f2c8b5f28..e93300967 100644 --- a/jest.config.js +++ b/jest.config.js @@ -6,7 +6,7 @@ module.exports = { transform: { "^.+\\.vue$": "vue-jest" }, moduleFileExtensions: ["js", "vue"], collectCoverageFrom: [ - "src/**/*.{js,vue}", + //"src/**/*.{js,vue}", "!src/main.js", "!src/constants/*.js", "!src/router/**/*.js", @@ -22,7 +22,7 @@ module.exports = { // 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/address-lookup/customer-questions/address-questions/address-questions.vue", "!src/layouts/address-vehicles/address-vehicles.vue", "!src/layouts/address-vehicles/address-vehicles-question/address-vehicles-question.vue", "!src/ux-components/alert\alert.vue", 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..11a0aa4ed --- /dev/null +++ b/src/layouts/address-lookup/customer-questions/address-questions/address-questions.spec.js @@ -0,0 +1,130 @@ +import { shallowMount } from "@vue/test-utils"; +import addressQuestions from "@/layouts/address-lookup/customer-questions/address-questions/address-questions"; + +const addressModel = { + streetAddress: "", + city: "", + state: "", + zipCode: "", +} + +describe("addressQuestions.vue", () => { + + it("Should render addressQuestions sub-components (textbox-questions and dropdown-questions)", async () => { + // Arrange + const wrapper = shallowMount(addressQuestions); + + // Act + const streetAddress = wrapper.findComponent({ ref: 'autocomplete' }); + const city = wrapper.findComponent({ ref: 'city' }); + const state = wrapper.findComponent({ ref: 'state' }); + const zipCode = wrapper.findComponent({ ref: 'zipCode' }); + + // Assert + expect(streetAddress.exists()).toBe(true); + expect(city.exists()).toBe(true); + expect(state.exists()).toBe(true); + expect(zipCode.exists()).toBe(true); + + }); + + it("Should call the watch handler for the address model when the addressModel is changed", async () => { + // Arrange + const wrapper = shallowMount(addressQuestions, { + propsData: { + modelValue: addressModel, + }, + }); + wrapper.vm.displayNoMatchWarning = true; + + // Act + const newAddressModel = { + streetAddress: "foo", + city: "foo", + state: "foo", + zipCode: "55555", + }; + + wrapper.vm.$options.watch.addressModel.handler.call(wrapper.vm, newAddressModel); + + // Assert + expect(wrapper.vm.addressModel.handler).toHaveBeenCalled; + + }); + + it("Should set this.displayNoMatchWarning to false, if the model changes when it is set to true", async () => { + // Arrange + const wrapper = shallowMount(addressQuestions, { + propsData: { + modelValue: addressModel, + }, + }); + wrapper.vm.displayNoMatchWarning = true; + + // Act + const newAddressModel = { + streetAddress: "foo", + city: "", + state: "", + zipCode: "", + }; + + // Act + wrapper.vm.$options.watch.addressModel.handler.call(wrapper.vm, newAddressModel); + + // Assert + expect(wrapper.vm.displayNoMatchWarning === false); + + }); + + it("Should it not should set this.displayNoMatchWarning to false when it is set to true, if the model if prepopulated", async () => { + // Arrange + const wrapper = shallowMount(addressQuestions, { + propsData: { + modelValue: addressModel, + }, + }); + wrapper.vm.displayNoMatchWarning = true; + + // Act + const newAddressModel = { + streetAddress: "foo", + city: "foo", + state: "foo", + zipCode: "55555", + }; + + // Act + wrapper.vm.$options.watch.addressModel.handler.call(wrapper.vm, newAddressModel); + + // Assert + expect(wrapper.vm.displayNoMatchWarning === true); + + }); + + it("Should it set this.showAddressFields to true when the model is prepopulated", async () => { + // Arrange + const wrapper = shallowMount(addressQuestions, { + propsData: { + modelValue: addressModel, + }, + }); + wrapper.vm.displayNoMatchWarning = true; + + // Act + const newAddressModel = { + streetAddress: "foo", + city: "foo", + state: "foo", + zipCode: "55555", + }; + + // Act + wrapper.vm.setupAddressLookup(); + + // Assert + expect(wrapper.vm.showAddressFields).toBe(true); + + }); + +}) \ 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..61062ba47 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 @@ -215,12 +215,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,7 +228,7 @@ export default ({ if (autocompleteResultsContainer) { streetAddressField.appendChild(autocompleteResultsContainer); } - + }) addressField1.onchange = function() { @@ -325,7 +325,7 @@ export default ({ watch: { addressModel: { handler(newValue) { - // The first time the address model changes is w + // The first time the address model changes is when the page first loads if (!newValue.city && !newValue.state && !newValue.zipCode) { From 2e847436c8e35c23eea101b8aa40231a593a76a7 Mon Sep 17 00:00:00 2001 From: Scott Kiener Date: Wed, 1 Jun 2022 14:34:19 -0400 Subject: [PATCH 03/32] CSR-563 | Refactor Alert and Alert tests --- src/constants/dynamic-strings.js | 2 +- src/helpers/cms-content-helper.js | 35 +++- .../address-vehicles/address-vehicles.vue | 14 +- src/mixins/base-mixin.js | 4 + src/ux-components/alert/alert.spec.js | 179 +++++++++++++----- src/ux-components/alert/alert.vue | 34 ++-- 6 files changed, 197 insertions(+), 71 deletions(-) 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/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/layouts/address-vehicles/address-vehicles.vue b/src/layouts/address-vehicles/address-vehicles.vue index 34937bfb7..22a237e75 100644 --- a/src/layouts/address-vehicles/address-vehicles.vue +++ b/src/layouts/address-vehicles/address-vehicles.vue @@ -28,8 +28,8 @@ />
- - {{ copy.split(':')[1].split(',')[1] }} + + {{ getRouterLinkDisplayTextFromCopy(copy) }} @@ -68,6 +68,10 @@ import { required } from "@/helpers/validation-rules"; import { Form, defineRule } from "vee-validate"; import { navigateAfterSaveToHeritageFunnel } from "@/helpers/heritage-integration/navigation-helper"; import { isGlassAvailableForCarId } from "@/helpers/damage-helper"; +import { doesCopyContainRouterLink, + splitCopyOnCMSPlaceHolder, + getRouterLinkRouteFromCopy, + getRouterLinkDisplayTextFromCopy, } from "@/helpers/cms-content-helper" // DEFINE VALIDATION RULES defineRule("vehicle-required", required(errorMessages.VEHICLE_REQUIRED)); @@ -116,7 +120,7 @@ export default { }, splitAlertProvideVinBodyForLink() { // Splits content when brackets are found in text so that text can be looped through and router-link can be injected when needed - return this.AlertProvideVinBody.split(/{(.*?)}/g); + return this.splitCopyOnCMSPlaceHolder(this.AlertProvideVinBody); }, VehiclesForQuestions() { const vehiclesData = this.VehiclesFromApi; @@ -145,6 +149,10 @@ export default { }, }, methods: { + doesCopyContainRouterLink, + splitCopyOnCMSPlaceHolder, + getRouterLinkRouteFromCopy, + getRouterLinkDisplayTextFromCopy, arePagePrerequisitesValid() { if ( store.getters.order.vehicle.carId diff --git a/src/mixins/base-mixin.js b/src/mixins/base-mixin.js index d0474d67b..c3840e780 100644 --- a/src/mixins/base-mixin.js +++ b/src/mixins/base-mixin.js @@ -5,6 +5,7 @@ import { navigationScenarios } from "@/router/router-constants/navigation-scenar import { vehicleCategories } from "@/constants/vehicle-categories.js"; import { routerParams } from "@/router/router-constants/router-params"; import { queryStrings } from "@/constants/query-strings"; +import { dynamicStrings } from "@/constants/dynamic-strings"; export default { data() { @@ -65,6 +66,9 @@ export default { }, queryStrings(){ return queryStrings; + }, + dynamicStrings(){ + return dynamicStrings; } }, }; diff --git a/src/ux-components/alert/alert.spec.js b/src/ux-components/alert/alert.spec.js index a0e65b12f..749a664b7 100644 --- a/src/ux-components/alert/alert.spec.js +++ b/src/ux-components/alert/alert.spec.js @@ -1,23 +1,18 @@ 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, { + const wrapper = shallowMount(alert, setupMocks({ propsData: { - isDismissible: true + isDismissible: true, + manualHeadline: 'testHeader', + manualCopy: 'testCopy' }, - computed: { - splitAlertCopyForLink: { - get() { - return "TEST"; - }, - } - }, - mixins: [mockMixin] - }); + })); const wrapperDiv = wrapper.find('div'); @@ -27,19 +22,13 @@ describe("alert.vue", () => { it("Should add specified alert class", async () => { // Arrange - const wrapper = shallowMount(alert, { + const wrapper = shallowMount(alert, setupMocks({ propsData: { - alertClass: 'warning' + alertClass: 'warning', + manualHeadline: 'testHeader', + manualCopy: 'testCopy' }, - computed: { - splitAlertCopyForLink: { - get() { - return "TEST"; - }, - } - }, - mixins: [mockMixin] - }); + })); const wrapperDiv = wrapper.find('div'); @@ -49,34 +38,106 @@ describe("alert.vue", () => { 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, { - propsData: { - manualHeadline: 'testHeader', - manualCopy: 'testCopy' - }, - mixins: [mockMixin] - }); - + const wrapper = shallowMount(alert, setupMocks({})); // Assert expect(wrapper.vm.alertHeadline).toBe("testHeader"); expect(wrapper.vm.alertCopy).toBe("testCopy"); }); - it("Should run scrollContainerToAlert function when the clientBoundingRect is not entirely in the viewport", () => { + it("Should call scrollIntoView() when the clientBoundingRect is not entirely in the viewport (out of view top)", () => { // Arrange - const wrapper = shallowMount(alert, { - computed: { - splitAlertCopyForLink: { - get() { - return "TEST"; - }, - } - }, - mixins: [mockMixin] - }); + 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(); + }); }); @@ -84,6 +145,38 @@ describe("alert.vue", () => { const mockMixin = { methods: { getCmsContent: jest.fn(), - getFooterInfoBoxHeight: jest.fn(()=> 80), + 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; } \ No newline at end of file diff --git a/src/ux-components/alert/alert.vue b/src/ux-components/alert/alert.vue index 1d983bd42..d356d0569 100644 --- a/src/ux-components/alert/alert.vue +++ b/src/ux-components/alert/alert.vue @@ -8,7 +8,7 @@