DigitalConsumer.FixMyGlass/src/layouts/scheduling/scheduling.spec.js
2026-08-03 08:33:38 -04:00

502 lines
19 KiB
JavaScript

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();
});
});
});