diff --git a/src/layouts/payment/payment-switch/payment-switch-button/payment-switch-button.spec.js b/src/layouts/payment/payment-switch/payment-switch-button/payment-switch-button.spec.js new file mode 100644 index 000000000..eb97a414d --- /dev/null +++ b/src/layouts/payment/payment-switch/payment-switch-button/payment-switch-button.spec.js @@ -0,0 +1,133 @@ +import { shallowMount } from "@vue/test-utils"; +import { getMountOptions } from "@/helpers/unit-test-helper.js"; + +import paymentSwitchButton from "@/layouts/payment/payment-switch/payment-switch-button/payment-switch-button"; + +const testConstants = { + images: { + A: "imageA", + B: "imageB", + NONE: null, + }, + names: { + A: "nameA", + B: "nameB", + }, + content: { + withInline: "Button text with {custom:inlineImage,Alt} inline.", + noInline: "Button text with no inline", + }, +}; + +let cmsContent; + +describe("Payment Switch Button", () => { + beforeEach(() => { + cmsContent = {}; + }); + + describe("Inline images", () => { + test("Renders image if inline-image token is present", () => { + // Arrange + const props = { + buttonText: testConstants.content.withInline, + buttonImage: testConstants.images.A, + buttonImageId: testConstants.names.A, + }; + + const wrapper = setupMocks({ + propsData: props, + }); + + // Act + const img = wrapper.findAll("img"); + + // Assert + expect(img.length).toBe(1); + }); + + test("Renders no image if inline-image token is not present", () => { + // Arrange + const props = { + buttonText: testConstants.content.noInline, + buttonImage: testConstants.images.A, + buttonImageId: testConstants.names.A, + }; + + const wrapper = setupMocks({ + propsData: props, + }); + + // Act + const img = wrapper.findAll("img"); + + // Assert + expect(img.length).toBe(0); + }); + + test("Renders no image, but instead alt text, when token is present but no image is", () => { + // Arrange + const props = { + buttonText: testConstants.content.withInline, + buttonImage: testConstants.images.NONE, + buttonImageId: testConstants.names.A, + }; + + const wrapper = setupMocks({ + propsData: props, + }); + + // Act + const img = wrapper.findAll("img"); + + // Assert + expect(img.length).toBe(0); + expect(wrapper.text()).toContain("Alt"); + }); + }); + + test("Emits click event when clicked", async () => { + // Arrange + const props = { + buttonText: testConstants.content.noInline, + buttonImage: testConstants.images.NONE, + buttonImageId: testConstants.images.NONE, + }; + + const wrapper = setupMocks({ + propsData: props, + }); + + // Act + await wrapper.trigger("click"); + + // Assert + expect(wrapper.emitted("click-event")).toBeTruthy(); + }); +}); + +function setupMocks(customMountOptions = {}) { + customMountOptions.route = { + query: { + fmgPage: "page-name", + } + }; + const mountOptions = getMountOptions(customMountOptions); + + const mockMixin = { + methods: { + getCmsContent: jest.fn((widgetName, cmsFieldName) => { + return cmsContent?.[widgetName]?.[cmsFieldName] ?? ""; + }), + pushEventToGA: jest.fn(), + }, + }; + + mountOptions.global.mixins = [mockMixin]; + + const wrapper = shallowMount(paymentSwitchButton, mountOptions); + + wrapper.vm.setCmsContent = jest.fn(); + + return wrapper; +} \ No newline at end of file diff --git a/src/layouts/payment/payment-switch/payment-switch.spec.js b/src/layouts/payment/payment-switch/payment-switch.spec.js new file mode 100644 index 000000000..210a1660c --- /dev/null +++ b/src/layouts/payment/payment-switch/payment-switch.spec.js @@ -0,0 +1,128 @@ +// component +import paymentSwitch from "@/layouts/payment/payment-switch/payment-switch"; + +// supporting files +import { shallowMount } from "@vue/test-utils"; +import { getMountOptions } from "@/helpers/unit-test-helper"; +import paymentSwitchButton from "@/layouts/payment/payment-switch/payment-switch-button/payment-switch-button"; +import store from "@/store"; + +let mockCmsContent = {}; + +describe("payment-switch", () => { + beforeEach(() => { + store.getters = {}; + mockCmsContent = { + PaymentSwitchWidget: { + Answers: [ + { + Name: "ap", + Text: "Afterpay Text", + SubText: "Afterpay SubText", + ImageId: "Afterpay Id", + AnswerImageUrl: "Afterpay Image", + SubWidgetName: "Afterpay SubWidget", + }, + { + Name: "pp", + Text: "Paypal Text", + SubText: "Paypal SubText", + ImageId: "Paypal Id", + AnswerImageUrl: "Paypal Image", + SubWidgetName: "Paypal SubWidget", + }, + { + Name: "cc", + Text: "Credit Card Text", + SubText: "Credit Card SubText", + ImageId: "Credit Card Id", + AnswerImageUrl: "Credit Card Image", + SubWidgetName: "Credit Card SubWidget", + } + ], + }, + }; + }); + + describe("Always renders two components", () => { + const paymentTypes = ["ap", "cc", "pp"]; + + paymentTypes.forEach((type) => { + test(`Type Chosen: ${type}`, () => { + // Arrange + const wrapper = setupMocks({ + propsData: { + cmsWidgetName: "PaymentSwitchWidget", + paymentType: type, + }, + }); + + // Act + + const buttons = wrapper.findAllComponents(paymentSwitchButton); + const separators = wrapper.findAll(".switch-button-separator"); + + // Assert + expect(buttons.length).toBe(2); + expect(separators.length).toBe(1); + }); + }); + }); + + describe("Correctly maps cms content to dictionary", () => { + test("Maps array to dict", () => { + // Arrange + const wrapper = setupMocks({ + propsData: { + cmsWidgetName: "PaymentSwitchWidget", + paymentType: "ap", + }, + }); + + // Act + const mappedVals = wrapper.vm.paymentMethodsInfo; + + // Assert + expect(mappedVals.ap).toBeDefined(); + expect(mappedVals.ap.buttonText).toContain("Afterpay"); + expect(mappedVals.ap.imageId).toContain("Afterpay"); + expect(mappedVals.ap.answerImageUrl).toContain("Afterpay"); + + expect(mappedVals.pp).toBeDefined(); + expect(mappedVals.pp.buttonText).toContain("Paypal"); + expect(mappedVals.pp.imageId).toContain("Paypal"); + expect(mappedVals.pp.answerImageUrl).toContain("Paypal"); + + expect(mappedVals.cc).toBeDefined(); + expect(mappedVals.cc.buttonText).toContain("Credit Card"); + expect(mappedVals.cc.imageId).toContain("Credit Card"); + expect(mappedVals.cc.answerImageUrl).toContain("Credit Card"); + }); + }); +}); + +function setupMocks(customMountOptions) { + const mountOptions = getMountOptions({ + ...customMountOptions, + }); + + // set all mock stuff + mountOptions.global.mocks["$store"] = store; + mountOptions.mixins = [ + { + methods: { + getCmsContent: jest.fn().mockImplementation((widgetName, fieldName) => { + if (mockCmsContent[widgetName] && mockCmsContent[widgetName][fieldName]) + return mockCmsContent[widgetName][fieldName]; + }), + setCmsContent: jest.fn(), + }, + }, + ]; + + const wrapper = shallowMount(paymentSwitch, mountOptions); + + // act on vm + + return wrapper; +} \ No newline at end of file diff --git a/src/layouts/payment/payment.spec.js b/src/layouts/payment/payment.spec.js new file mode 100644 index 000000000..51a3d31f0 --- /dev/null +++ b/src/layouts/payment/payment.spec.js @@ -0,0 +1,608 @@ +// Components +import payment from "@/layouts/payment/payment"; + +// Supporting Files +import { shallowMount } from "@vue/test-utils"; +import { getMountOptions } from "@/helpers/unit-test-helper"; +import store from "@/store"; +import baseMixin from "@/mixins/base-mixin"; +import { storeActions } from "@/constants/store-actions"; +import { paymentMethods } from "@/constants/payment-method-constants"; + +// Constants +const parts = { + windshield: { + name: "windshield", + canSafeliteRecalibrate: true, + childParts: [], + color: "Green Tint", + description: + "solar, soundproofing, lane keep assist, lane departure warning system, w/adaptive cruise control", + id: "db22fd44-10dd-456f-979b-ff88cf68cca6", + partNumber: "FW04896GTYN", + partType: "WINDSHIELD", + recalibrationType: "STATIC", + requiresCapabilityQuestions: false, + requiresRecalibration: true, + salesTax: 63.86, + sellingPrice: 791.46, + }, + frontWipers: { + name: "front wipers", + partNumber: "SBB16", + description: "SAFELITE BEAM BLADE 16", + partType: "FRONT WIPER", + price: 32.64, + }, + rearWipers: { + name: "rear wipers", + partNumber: "SBBR12A", + description: "SAFELITE REAR BLADE 12A", + partType: "REAR WIPER", + price: 24.48, + }, + rainDefense: { + name: "rain defense", + partNumber: "RAIN DEFENSE", + description: null, + partType: "RAIN DEFENSE", + price: 35.5, + }, +}; + +const pricedParts = { + windshield: { + ...parts.windshield, + kitPrice: 0, + laborAmount: 60, + salesTax: 0, + sellingPrice: 791.46, + }, + frontWipers: { + ...parts.frontWipers, + kitPrice: 0, + laborAmount: 20, + salesTax: 0, + sellingPrice: 32.64 + }, + rearWipers: { + ...parts.rearWipers, + kitPrice: 10, + laborAmount: 10, + salesTax: 0, + sellingPrice: 24.48, + }, + rainDefense: { + ...parts.rainDefense, + kitPrice: 20, + laborAmount: 0, + salesTax: 0, + sellingPrice: 35.5, + }, +}; + +const taxedParts = { + windshield: { + ...pricedParts.windshield, + salesTax: 63.86, + subTotal: 100, + }, + frontWipers: { + ...pricedParts.frontWipers, + salesTax: 2.36, + subTotal: 100, + }, + rearWipers: { + ...pricedParts.rearWipers, + salesTax: 6.00, + subTotal: 100, + }, + rainDefense: { + ...pricedParts.rainDefense, + salesTax: 1.00, + subTotal: 100, + }, +}; + +// Setup global mocks +let mockCmsContent = {}; + +let mockStoreActionData = {}; + +async function mockDispatchStoreAction(actionName) { + return mockStoreActionData[actionName]; +}; + +jest.mock("@/mixins/base-mixin.js", () => ({ + methods: { + dispatchStoreAction: jest.fn().mockImplementation(mockDispatchStoreAction), + dispatchStoreActionWithLogging: jest.fn().mockImplementation(mockDispatchStoreAction), + getAmountDue: jest.fn().mockImplementation(() => 5), + getDisplayAmountDue: jest.fn().mockImplementation(() => "$5.00"), + }, +})); + +jest.mock("@/helpers/promotions-helper", () => ({ + revalidatePromosAndValidateQueryStringPromo: async () => ({}), + getVapsThatNeedToBeAddedToSatisfyPromos: () => [], +})); + +jest.mock("@/helpers/cms-content-helper", () => ({ + fetchCmsContentForPage: () => Promise.resolve("content"), +})); + +describe("payment.vue", () => { + beforeEach(() => { + store.getters = { + order: { + vehicle: { + year: "2020", + make: "acura", + model: "mdx", + style: "4-door sedan", + carId: "dummyCarId", + category: "dummyCategory", + vin: "dummyVin", + }, + serviceLocation: { + address: "add1", + address2: "add2", + city: "city", + state: "state", + zipCode: "zip", + zipCodeCtu: "zipCtu", + appointmentType: "IN_SHOP", + isVehicleProtected: true, + provider: { + providerNumber: 2, + address: { + streetAddress: "add3", + city: "city2", + state: "state2", + zipCode: "zip2", + zipCodeCtu: "zipCtu2", + }, + }, + techNotes: "", + }, + customer: { + firstName: "first", + lastName: "last", + emailAddress: "builddigitaltest@safelite.com", + phoneNumber: "555-555-5555", + isSmsOptIn: false, + }, + damage: { + isRepair: false, + numberOfChips: null, + glassToReplace: [{location: "windshield"}], + }, + lineItems: { + glassParts: [parts.windshield], + supportingItems: [], + vaps: [parts.frontWipers], + promos: [], + }, + payment: { + isInsurance: false, + insuranceCoverage: { + isVerified: null, + coverageStatus: null, + coverageVerificationType: null, + }, + isPia: true, + piaType: "Afterpay", + inactivePromos: [], + }, + schedule: { + date: "date", + startTime: "start", + endTime: "end", + jobMinMinutes: "30", + jobMaxMinutes: "45", + }, + }, + get payment() { + return this.order.payment; + }, + get vehicle() { + return this.order.vehicle; + }, + }; + + mockCmsContent = {}; + + mockStoreActionData = { + [storeActions.GET_SIGNATURE]: { + token: "token", + signature: "signature", + startDate: "startDate", + }, + [storeActions.GET_WIPERS]: [parts.frontWipers, parts.rearWipers], + [storeActions.GET_RAIN_DEFENSE]: parts.rainDefense, + [storeActions.PRICE_ORDER_ITEMS_AND_SAVE_SERVER_DATA]: [pricedParts.windshield, pricedParts.frontWipers], + [storeActions.TAX_ORDER_ITEMS_AND_SAVE_SERVER_DATA]: [taxedParts.windshield, taxedParts.frontWipers], + }; + }); + + afterEach(() => { + jest.clearAllMocks(); + }); + + describe("arePagePrerequisitesValid", () => { + test("Returns true in nominal conditions", () => { + // Arrange + // Store defaults are valid + const wrapper = setupMocks({}); + + // Act + const result = wrapper.vm.arePagePrerequisitesValid(); + + // Assert + expect(result).toBe(true); + }); + + describe("Payment method cases", () => { + test("Returns false if pia options are not valid", () => { + // Arrange + store.getters.order.payment.isPia = null; + store.getters.order.payment.piaType = null; + const wrapper = setupMocks({}); + + // Act + const result = wrapper.vm.arePagePrerequisitesValid(); + + // Assert + expect(result).toBe(false); + }); + + test("Returns false if pay at time of service", () => { + // Arrange + store.getters.order.payment.isPia = false; + store.getters.order.payment.piaType = null; + const wrapper = setupMocks({}); + + // Act + const result = wrapper.vm.arePagePrerequisitesValid(); + + // Assert + expect(result).toBe(false); + }); + }); + }); + + describe("beforeRouteEnter", () => { + test("Properly initializes fields and kicks off hop", async () => { + // Arrange + const vmMock = { + setCmsContent: jest.fn(), + $refs: { + cart: { + cartItems: [], + }, + }, + getPiaLineItems: jest.fn(), + fetchSignatureInfo: jest.fn(), + setIFrameListener: jest.fn(), + + $nextTick: (f) => { + f(); + }, + }; + + const nextF = (f) => { + f(vmMock); + }; + + // Act + await payment.beforeRouteEnter.call( + vmMock, + { query: { fmgPage: "payment" } }, + undefined, + nextF + ); + + // Assert + expect(vmMock.availableVaps).not.toBeUndefined(); + expect(vmMock.lineItems).not.toBeUndefined(); + + expect(vmMock.setCmsContent).toBeCalled(); + expect(vmMock.getPiaLineItems).toBeCalled(); + expect(vmMock.fetchSignatureInfo).toBeCalled(); + expect(vmMock.setIFrameListener).toBeCalled(); + }); + }); + + describe("payment type mapping", () => { + describe("getPaymentType", () => { + test("Maps AFTERPAY -> ap", () => { + // Arrange + store.getters.order.payment.piaType = paymentMethods.AFTERPAY; + const wrapper = setupMocks({}); + + // Act + const mapped = wrapper.vm.getPaymentType(); + + // Assert + expect(mapped).toBe("ap"); + }); + + test("Maps CREDIT_CARD -> cc", () => { + // Arrange + store.getters.order.payment.piaType = paymentMethods.CREDIT_CARD; + const wrapper = setupMocks({}); + + // Act + const mapped = wrapper.vm.getPaymentType(); + + // Assert + expect(mapped).toBe("cc"); + }); + + test("Maps PAYPAL -> pp", () => { + // Arrange + store.getters.order.payment.piaType = paymentMethods.PAYPAL; + const wrapper = setupMocks({}); + + // Act + const mapped = wrapper.vm.getPaymentType(); + + // Assert + expect(mapped).toBe("pp"); + }); + + test("Maps other values to self", () => { + // Arrange + store.getters.order.payment.piaType = "SomeOtherText"; + const wrapper = setupMocks({}); + + // Act + const mapped = wrapper.vm.getPaymentType(); + + // Assert + expect(mapped).toBe("SomeOtherText"); + }); + }); + + describe("isPaypal", () => { + test("Is responsive if initial data changes", async () => { + // Arrange + // note: begins with payment-type = afterpay + const wrapper = setupMocks({}); + + // Act + const preVal = wrapper.vm.isPaypal; + wrapper.vm.paymentType = "pp"; + await wrapper.vm.$nextTick(); + + const postVal = wrapper.vm.isPaypal; + + // Assert + expect(preVal).toBe(false); + expect(postVal).toBe(true); + }); + }); + }); + + describe("getPiaLineItems", () => { + test("Properly formatted output", () => { + // Arrange + const wrapper = setupMocks({}); + const cartItems = [ + taxedParts.windshield, + taxedParts.frontWipers, + taxedParts.rainDefense, + ]; + + // Act + wrapper.vm.getPiaLineItems(cartItems); + const result = wrapper.vm.piaLineItems; + + // Assert + expect(result).toMatch(/^[\w ]+\|[\d]+(.\d\d)?\|1(\|\|[\w ]+\|[\d]+(.\d\d)?\|1)*$/); + }); + + test("Includes repair supply for non-glass orders", () => { + // Arrange + store.getters.order.lineItems.glassParts = null; + + const wrapper = setupMocks({}); + const cartItems = [ + taxedParts.frontWipers, + taxedParts.rainDefense, + ]; + + // Act + wrapper.vm.getPiaLineItems(cartItems); + const result = wrapper.vm.piaLineItems; + + // Assert + expect(result).toMatch(/Repair supplies/); + }); + + test("Does not include promos", () => { + // Arrange + store.getters.order.lineItems.promos = [{ + name: "PROMO", + salesTax: 10, + subTotal: 20, + }]; + + const wrapper = setupMocks({}); + const cartItems = [ + taxedParts.windshield, + taxedParts.frontWipers, + taxedParts.rainDefense, + ]; + + // Act + wrapper.vm.getPiaLineItems(cartItems); + const result = wrapper.vm.piaLineItems; + + // Assert + expect(result).not.toMatch(/PROMO/); + }); + }); + + describe("hop form", () => { + describe("fetchSignatureInfo", () => { + test("Sets auth fields", async () => { + // Arrange + const signature = { + token: "TOKEN", + signature: "SIGNATURE", + startDate: "STARTDATE", + }; + const wrapper = setupMocks({}); + + wrapper.vm.submitHopForm = jest.fn(); + + // Act + await wrapper.vm.fetchSignatureInfo(signature); + + // Assert + expect(wrapper.vm.authToken).toBe(signature.token); + expect(wrapper.vm.authSignature).toBe(signature.signature); + expect(wrapper.vm.authSignatureStart).toBe(signature.startDate); + + expect(wrapper.vm.submitHopForm).toBeCalled(); + }); + + test("If paypal, show the loading modal", async () => { + // Arrange + const signature = { + token: "TOKEN", + signature: "SIGNATURE", + startDate: "STARTDATE", + }; + store.getters.order.payment.piaType = "Paypal"; + const wrapper = setupMocks({}); + + wrapper.vm.submitHopForm = jest.fn(); + wrapper.vm.$refs.loadingModal.showModal = jest.fn(); + + // Act + await wrapper.vm.fetchSignatureInfo(signature); + + // Assert + expect(wrapper.vm.$refs.loadingModal.showModal).toBeCalled(); + }); + }); + + describe("submitHopForm", () => { + test("Submits form", async () => { + // Arrange + const wrapper = setupMocks({}); + + wrapper.vm.$refs.hopForm.submit = jest.fn(); + wrapper.vm.$refs.paymentFrame = null; + + // Act + wrapper.vm.submitHopForm(); + + await wrapper.vm.$nextTick(); + + // Assert + expect(wrapper.vm.$refs.hopForm.submit).toBeCalled(); + }); + }); + + describe("handleIFrameContentWindwMessage", () => { + test("Navigates back if afterpay is closed", () => { + // Arrange + const event = { + data: "afterpayClosed", + }; + + const wrapper = setupMocks({}); + + wrapper.vm.backButtonAction = jest.fn(); + + // Act + wrapper.vm.handleIFrameContentWindwMessage(event); + + // Assert + expect(wrapper.vm.backButtonAction).toBeCalled(); + }); + + test("Blocks UI interaction if credit card is submitted", () => { + // Arrange + const event = { + data: "creditCardSubmit", + }; + + const wrapper = setupMocks({}); + + // Act + wrapper.vm.handleIFrameContentWindwMessage(event); + + // Assert + expect(wrapper.vm.shouldBlockInteraction).toBe(true); + }); + }); + }); + + describe("UI Blocking", () => { + test("UI block appears when toggled", async () => { + // Arrange + const wrapper = setupMocks({}); + + // Act + wrapper.vm.setUIBlock(true); + + await wrapper.vm.$nextTick(); + + const uiBlockElements = wrapper.findAll(".ui-block"); + + // Assert + expect(uiBlockElements.length).toBeGreaterThan(0); + }); + + test("Don't propogate clicks from UI block", async () => { + // Arrange + const wrapper = setupMocks({}); + + const outerDiv = wrapper.find(".container-fluid"); + const clickFn = jest.fn(); + outerDiv.element.addEventListener("click", clickFn); + + // Act + wrapper.vm.setUIBlock(true); + + await wrapper.vm.$nextTick(); + + const uiBlockElement = wrapper.find(".ui-block"); + + await uiBlockElement.trigger("click"); + + // Assert + expect(clickFn).not.toBeCalled(); + }); + }); +}); + +function setupMocks({ customMountOptions }) { + const mountOptions = getMountOptions({ + ...customMountOptions, + }); + + // set all mock stuff + mountOptions.global.mocks["$store"] = store; + mountOptions.mixins = [ + { + methods: { + getCmsContent: jest.fn().mockImplementation((widgetName, fieldName) => { + if (mockCmsContent[widgetName] && mockCmsContent[widgetName][fieldName]) + return mockCmsContent[widgetName][fieldName]; + }), + setCmsContent: jest.fn(), + }, + }, + ]; + + const wrapper = shallowMount(payment, mountOptions); + + // act on vm + + return wrapper; +}