502 lines
19 KiB
JavaScript
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();
|
|
});
|
|
});
|
|
});
|