import { shallowMount } from "@vue/test-utils"; import scheduling from "./scheduling"; import { getMountOptions } from "@/helpers/unit-test-helper.js"; import { settleAllPromises } from "@/helpers/layout-helper"; import { AppointmentTypeStrings } from "@/constants/schedule-constants"; import store from "@/store"; jest.mock("@/store", () => ({ dispatch: jest.fn().mockResolvedValue(null), getters: { order: { serviceLocation: { zipCode: "43235", zipCodeCtu: "1234", appointmentType: null, address: null, address2: null, city: null, state: "OH", isVehicleProtected: null, }, payment: { isInsurance: false, insuranceCoverage: { isVerified: false }, billToAccountNumber: "12345", }, referralNumber: "", damage: { isRepair: false }, lineItems: { glassParts: [] }, policy: { isItac: false, isNoComp: false }, isRecalAcknowledgedForScheduling: "", }, lineItems: { supportingItems: [] }, applicationUser: { experiments: [], }, }, })); jest.mock("@/helpers/layout-helper", () => ({ settleAllPromises: jest.fn().mockResolvedValue({}), })); jest.mock("@/helpers/cms-content-helper", () => ({ fetchCmsContentForPage: jest.fn().mockResolvedValue({}), })); jest.mock("@/helpers/heritage-integration/navigation-helper", () => ({ navigateToHeritageFunnel: jest.fn(), })); jest.mock("@/helpers/debug-log-helper", () => ({ debugLog: jest.fn(), })); jest.mock("@/helpers/page-prerequisites-helper.js", () => ({ flushPagePrereqsLogs: jest.fn(), hasServiceZipInfo: jest.fn(() => true), hasGlassPartsOrRepairInfo: jest.fn(() => true), hasInsuranceInfo: jest.fn(() => true), })); function setupMocks({ isAppleBrowser = false } = {}) { const dispatchStoreAction = jest.fn().mockResolvedValue(null); const baseMixin = { methods: { getCmsContent: jest.fn(() => ""), setCmsContent: jest.fn(), getTotalLineItemPrice: jest.fn(() => 0), isAppleBrowser: jest.fn(() => isAppleBrowser), dispatchStoreAction, }, computed: { storeActions() { return { SAVE_SERVICE_LOCATION: "saveServiceLocation", SAVE_SCHEDULE: "saveSchedule", SAVE_WAITLIST_REQUESTED: "saveWaitListRequested", SAVE_SUPPORTING_ITEMS_SUPPRESSING_STATE_RESETTING: "saveSupportingItemsSuppressingStateResetting", }; }, navigationScenarios() { return { CLICKED_FORWARD: "clickedForward", CLICKED_FORWARD_WITH_MOBILE_SERVICE: "clickedForwardWithMobileService", CLICKED_BACK: "clickedBack", }; }, pageName() { return "scheduling"; }, }, }; const mountOptions = getMountOptions({ route: { name: "scheduling" }, router: { navigate: jest.fn(), navigateWithSaving: jest.fn(), navigateWithoutSaving: jest.fn(), }, }); mountOptions.global.mixins = [baseMixin]; const wrapper = shallowMount(scheduling, mountOptions); return { wrapper, dispatchStoreAction }; } describe("scheduling.vue", () => { describe("intercept overlay", () => { test("does not render interceptOverlay when isLoadingDates is false", async () => { const { wrapper } = setupMocks(); await wrapper.setData({ isLoadingDates: false }); expect(wrapper.find("intercept-overlay-stub").exists()).toBe(false); wrapper.unmount(); }); test("renders interceptOverlay when isLoadingDates is true", async () => { const { wrapper } = setupMocks(); await wrapper.setData({ isLoadingDates: true }); expect(wrapper.find("intercept-overlay-stub").exists()).toBe(true); wrapper.unmount(); }); }); describe("handleRequestMoreDates", () => { test("sets isLoadingDates to true while fetching and false after resolving", async () => { let resolve; settleAllPromises.mockReturnValueOnce( new Promise((r) => { resolve = r; }) ); const { wrapper } = setupMocks(); const fetchPromise = wrapper.vm.handleRequestMoreDates(); expect(wrapper.vm.isLoadingDates).toBe(true); resolve({}); await fetchPromise; expect(wrapper.vm.isLoadingDates).toBe(false); wrapper.unmount(); }); test("completes without invoking loadingModal methods (stub has none)", async () => { // The loadingModal stub rendered by shallowMount has no showModal/hideModal. // If those calls were still in handleRequestMoreDates they would throw here. settleAllPromises.mockResolvedValueOnce({}); const { wrapper } = setupMocks(); await expect(wrapper.vm.handleRequestMoreDates()).resolves.toBeUndefined(); wrapper.unmount(); }); test("batches inshop provider requests into a single getShopTimeSlotsV2 call", async () => { store.dispatch.mockClear(); settleAllPromises.mockResolvedValueOnce({ inshopTimeSlots: { providerTimeSlots: [ { providerNumber: "05018", days: [{ date: "2026-07-22", timeSlots: [] }] }, { providerNumber: "05019", days: [{ date: "2026-07-22", timeSlots: [] }] }, ], }, }); const { wrapper } = setupMocks(); wrapper.vm.inshopProvidersAndTimeSlots = [ { provider: { providerNumber: "05018" }, timeSlots: { days: [] } }, { provider: { providerNumber: "05019" }, timeSlots: { days: [] } }, ]; wrapper.vm.datePickerEndDate = "2026-07-21"; await wrapper.vm.handleRequestMoreDates(); const shopTimeSlotDispatches = store.dispatch.mock.calls.filter( ([actionName]) => actionName === "getShopTimeSlotsV2" ); expect(shopTimeSlotDispatches).toHaveLength(1); expect(shopTimeSlotDispatches[0][1].payload.providerNumbers).toEqual([ "05018", "05019", ]); expect(shopTimeSlotDispatches[0][1].payload.endDate).toBeDefined(); wrapper.unmount(); }); test("maps batched inshop providerTimeSlots back onto provider entries", async () => { settleAllPromises.mockResolvedValueOnce({ inshopTimeSlots: { providerTimeSlots: [ { providerNumber: "05018", days: [{ date: "2026-07-22", timeSlots: [{ id: "slot-a" }] }], }, { providerNumber: "05019", days: [{ date: "2026-07-22", timeSlots: [{ id: "slot-b" }] }], }, ], }, }); const { wrapper } = setupMocks(); wrapper.vm.inshopProvidersAndTimeSlots = [ { provider: { providerNumber: "05018" }, timeSlots: { days: [] } }, { provider: { providerNumber: "05019" }, timeSlots: { days: [] } }, ]; wrapper.vm.datePickerEndDate = "2026-07-21"; await wrapper.vm.handleRequestMoreDates(); expect(wrapper.vm.inshopProvidersAndTimeSlots[0].timeSlots.days).toHaveLength(1); expect( wrapper.vm.inshopProvidersAndTimeSlots[0].timeSlots.days[0].timeSlots[0].id ).toBe("slot-a"); expect( wrapper.vm.inshopProvidersAndTimeSlots[1].timeSlots.days[0].timeSlots[0].id ).toBe("slot-b"); wrapper.unmount(); }); }); describe("service zip", () => { test("prefills zipSearchCode and billToAccountNumber from store and renders zip search", () => { const { wrapper } = setupMocks(); expect(wrapper.vm.zipSearchCode).toBe("43235"); expect(wrapper.vm.billToAccountNumber).toBe("12345"); expect(wrapper.find("scheduling-zip-search-stub").exists()).toBe(true); wrapper.unmount(); }); test("reloads providers and timeslots when zip-searched is emitted", async () => { store.dispatch.mockClear(); store.dispatch.mockImplementation((action) => { if (action === "getProviders") { return Promise.resolve({ data: { shopProviders: [{ providerNumber: "05018" }], mobileProviderNumber: "12345", }, }); } return Promise.resolve(null); }); settleAllPromises.mockResolvedValueOnce({ inshopTimeSlots: { providerTimeSlots: [ { providerNumber: "05018", days: [{ date: "2026-07-22", timeSlots: [] }] }, ], }, mobileTimeSlots: { days: [{ date: "2026-07-22", timeSlots: [] }] }, }); const { wrapper } = setupMocks(); await wrapper.setData({ isLoadingDates: false }); wrapper.vm.datePickerEndDate = "2026-08-15"; wrapper.vm.selectedScheduling = { appointmentType: "Mobile" }; const initialDatePickerKey = wrapper.vm.datePickerKey; await wrapper.vm.onZipSearched({ zipCode: "44101", billToAccountNumber: "87291" }); expect(store.dispatch).toHaveBeenCalledWith("getProviders", { payload: { serviceZipCode: "44101" }, pageNameToLog: "scheduling", }); expect(wrapper.vm.billToAccountNumber).toBe("87291"); expect(settleAllPromises).toHaveBeenCalled(); expect(wrapper.vm.datePickerKey).toBe(initialDatePickerKey + 1); expect(wrapper.vm.selectedDate).toBeNull(); expect(wrapper.vm.selectedScheduling).toBeNull(); expect(wrapper.vm.isWaitlistRequested).toBe(false); expect(wrapper.vm.inshopProvidersAndTimeSlots).toHaveLength(1); expect(wrapper.vm.mobileProviderAndTimeSlot.providerNumber).toBe("12345"); wrapper.unmount(); }); test("anchors to zip search when mobile zip is clicked", () => { const { wrapper } = setupMocks(); wrapper.vm.$refs.schedulingZipSearch.focusZipInput = jest.fn(); wrapper.vm.onMobileZipCodeClicked(); expect(wrapper.vm.$refs.schedulingZipSearch.focusZipInput).toHaveBeenCalled(); wrapper.unmount(); }); }); describe("forwardButtonAction", () => { test("saves service location, schedule, waitlist and navigates for inshop selection", async () => { const { wrapper, dispatchStoreAction } = setupMocks(); wrapper.vm.selectedDate = "2026-07-22"; wrapper.vm.isWaitlistRequested = true; wrapper.vm.selectedScheduling = { appointmentType: AppointmentTypeStrings.IN_SHOP, providerNumber: "05018", routeCodeId: "route-inshop", }; wrapper.vm.inshopProvidersAndTimeSlots = [ { provider: { providerNumber: "05018", address: { streetAddress: "123 Main", city: "Columbus", state: "OH", zipCode: "43235", zipCodeCtu: "9999", }, }, timeSlots: { estimatedServiceMinutesMinimum: 60, estimatedServiceMinutesMaximum: 120, days: [ { date: "2026-07-22", timeSlots: [ { id: "route-inshop", startTime: "09:00", endTime: "10:00", }, ], }, ], }, }, ]; await wrapper.vm.forwardButtonAction(); expect(dispatchStoreAction).toHaveBeenCalledWith( "saveServiceLocation", expect.objectContaining({ appointmentType: AppointmentTypeStrings.IN_SHOP, zipCodeCtu: "9999", provider: expect.objectContaining({ providerNumber: "05018" }), }), false ); expect(dispatchStoreAction).toHaveBeenCalledWith( "saveSchedule", expect.objectContaining({ date: "2026-07-22", routeCode: "route-inshop", startTime: "09:00", endTime: "10:00", jobMinMinutes: "60", jobMaxMinutes: "120", }), false ); expect(dispatchStoreAction).toHaveBeenCalledWith("saveWaitListRequested", true, false); expect(wrapper.vm.$router.navigateWithSaving).toHaveBeenCalledWith( "clickedForward", "scheduling" ); wrapper.unmount(); }); test("navigates with mobile scenario and clears provider address for mobile selection", async () => { const { wrapper, dispatchStoreAction } = setupMocks(); wrapper.vm.selectedDate = "2026-07-22"; wrapper.vm.selectedScheduling = { appointmentType: AppointmentTypeStrings.MOBILE, providerNumber: "MOBILE1", routeCodeId: "route-mobile", isPremiumAppointment: false, }; wrapper.vm.mobileProviderAndTimeSlot = { providerNumber: "MOBILE1", timeSlots: { estimatedServiceMinutesMinimum: 90, estimatedServiceMinutesMaximum: 150, days: [ { date: "2026-07-22", timeSlots: [ { id: "route-mobile", startTime: "08:00", endTime: "12:00", }, ], }, ], }, }; await wrapper.vm.forwardButtonAction(); expect(dispatchStoreAction).toHaveBeenCalledWith( "saveServiceLocation", expect.objectContaining({ appointmentType: AppointmentTypeStrings.MOBILE, provider: { providerNumber: "MOBILE1", address: { streetAddress: null, city: null, state: null, zipCode: null, zipCodeCtu: null, }, }, }), false ); expect(wrapper.vm.$router.navigateWithSaving).toHaveBeenCalledWith( "clickedForwardWithMobileService", "scheduling" ); wrapper.unmount(); }); }); describe("forwardButtonAction (recal acknowledgement)", () => { test("opens recal modal and does not navigate when required", async () => { store.getters.order.lineItems = { glassParts: [ { partType: "WINDSHIELD", requiresRecalibration: true, canSafeliteRecalibrate: false, }, ], }; const { wrapper } = setupMocks(); wrapper.vm.$refs.navbar.removeLoader = jest.fn(); wrapper.vm.$refs.recalAckModal.openModal = jest.fn(); await wrapper.setData({ isRecalAcknowledgedForScheduling: "" }); await wrapper.vm.forwardButtonAction(); expect(wrapper.vm.$refs.recalAckModal.openModal).toHaveBeenCalled(); expect(wrapper.vm.$router.navigateWithSaving).not.toHaveBeenCalled(); wrapper.unmount(); store.getters.order.lineItems = { glassParts: [] }; }); test("navigates forward after recal acknowledgement is saved", async () => { const { wrapper } = setupMocks(); await wrapper.setData({ isRecalAcknowledgedForScheduling: "Yes" }); await wrapper.vm.forwardButtonAction(); expect(store.dispatch).toHaveBeenCalledWith( "saveIsRecalAcknowledgedForScheduling", "Yes" ); expect(wrapper.vm.$router.navigateWithSaving).toHaveBeenCalled(); wrapper.unmount(); }); test("onRecalAcknowledged sets acknowledgement and continues forward", async () => { const { wrapper } = setupMocks(); wrapper.vm.forwardButtonAction = jest.fn(); wrapper.vm.onRecalAcknowledged("Yes"); expect(wrapper.vm.isRecalAcknowledgedForScheduling).toBe("Yes"); expect(wrapper.vm.forwardButtonAction).toHaveBeenCalled(); wrapper.unmount(); }); }); describe("in-shop Maps link", () => { const address = { city: "Lewis Center", state: "OH", streetAddress: "1343 Cameron Ave", zipCode: "43035", }; const query = encodeURIComponent( "safelite,Safelite Autoglass, 1343 Cameron Ave, Lewis Center, OH 43035" ); test("onInshopAddressClicked opens Maps in a new tab for Google and Apple", () => { const openSpy = jest.spyOn(window, "open").mockImplementation(() => null); const { wrapper } = setupMocks(); wrapper.vm.onInshopAddressClicked({ address }); expect(openSpy).toHaveBeenCalledWith( `https://www.google.com/maps/search/?api=1&query=${query}`, "_blank", "noopener,noreferrer" ); wrapper.unmount(); openSpy.mockClear(); const { wrapper: appleWrapper } = setupMocks({ isAppleBrowser: true }); appleWrapper.vm.onInshopAddressClicked({ address }); expect(openSpy).toHaveBeenCalledWith( `https://maps.apple.com/?q=${query}`, "_blank", "noopener,noreferrer" ); appleWrapper.unmount(); openSpy.mockRestore(); }); }); });