417 lines
13 KiB
JavaScript
417 lines
13 KiB
JavaScript
// @vitest-environment jsdom
|
|
import { mount } from "@vue/test-utils";
|
|
import { nextTick } from "vue";
|
|
import { beforeEach, describe, expect, it, vi } from "vitest";
|
|
|
|
const mocks = vi.hoisted(() => ({
|
|
getDepartmentsGuest: vi.fn(),
|
|
locationRef: {
|
|
value: {
|
|
coords: {
|
|
latitude: 55.5,
|
|
longitude: 12.4,
|
|
},
|
|
},
|
|
},
|
|
getDistance: vi.fn(),
|
|
}));
|
|
|
|
vi.mock("@/components/pagination/departmentTabs.vue", () => ({
|
|
getDepartmentsGuest: mocks.getDepartmentsGuest,
|
|
}));
|
|
|
|
vi.mock("@/components/displays/department/pos/steps/mobile/objects/PosDepartmentStepMobileFlow.vue", () => ({
|
|
locations: {
|
|
location: mocks.locationRef,
|
|
normalizeCoordinatePair: (coordinates, { allowZeroPair = true } = {}) => {
|
|
const latitude = Number(coordinates?.latitude);
|
|
const longitude = Number(coordinates?.longitude);
|
|
if (!Number.isFinite(latitude) || !Number.isFinite(longitude)) {
|
|
return null;
|
|
}
|
|
if (latitude < -90 || latitude > 90 || longitude < -180 || longitude > 180) {
|
|
return null;
|
|
}
|
|
if (!allowZeroPair && latitude === 0 && longitude === 0) {
|
|
return null;
|
|
}
|
|
return { latitude, longitude };
|
|
},
|
|
getDistance: mocks.getDistance,
|
|
},
|
|
}));
|
|
|
|
const { useWashDepartments } = await import("@/composables/useWashDepartments.js");
|
|
|
|
const mountedWrappers = [];
|
|
|
|
const mountDepartments = (options = {}) => {
|
|
let departments = null;
|
|
const wrapper = mount({
|
|
setup() {
|
|
departments = useWashDepartments(options);
|
|
return {};
|
|
},
|
|
template: "<div />",
|
|
});
|
|
mountedWrappers.push(wrapper);
|
|
|
|
return departments;
|
|
};
|
|
|
|
describe("useWashDepartments", () => {
|
|
beforeEach(() => {
|
|
mountedWrappers.splice(0).forEach((wrapper) => wrapper.unmount());
|
|
mocks.getDepartmentsGuest.mockReset();
|
|
mocks.getDistance.mockReset();
|
|
vi.restoreAllMocks();
|
|
mocks.locationRef.value = {
|
|
coords: {
|
|
latitude: 55.5,
|
|
longitude: 12.4,
|
|
},
|
|
};
|
|
|
|
mocks.getDistance.mockImplementation((_from, to) => {
|
|
if (to.latitude === 55.6) {
|
|
return 8;
|
|
}
|
|
if (to.latitude === 55.7) {
|
|
return 2;
|
|
}
|
|
return 20;
|
|
});
|
|
});
|
|
|
|
it("fetches guest departments and resolves the nearest one", async () => {
|
|
mocks.getDepartmentsGuest.mockResolvedValue([
|
|
{
|
|
id: 1,
|
|
name: "North",
|
|
address: "A",
|
|
latitude: 55.6,
|
|
longitude: 12.5,
|
|
lanes: [{ id: 10, products: [1, 2] }],
|
|
self_serve_enabled: true,
|
|
},
|
|
{
|
|
id: 2,
|
|
name: "South",
|
|
address: "B",
|
|
latitude: 55.7,
|
|
longitude: 12.6,
|
|
lanes: [{ id: 11, products: [2, 3] }],
|
|
self_serve_enabled: true,
|
|
},
|
|
]);
|
|
|
|
const departments = mountDepartments({ includeLanes: true });
|
|
|
|
await departments.fetchDepartments();
|
|
|
|
expect(mocks.getDepartmentsGuest).toHaveBeenCalledWith({ include_lanes: true });
|
|
expect(departments.nearestDepartment.value).toMatchObject({ id: 2, name: "South", distance: 2 });
|
|
expect(departments.availableProductIds.value).toEqual([2, 3]);
|
|
expect(departments.doesCurrentDepartmentSelectionHaveSelfServeEnabled.value).toBe(true);
|
|
});
|
|
|
|
it("allows forced selection and resets back to geolocation ordering", async () => {
|
|
mocks.getDepartmentsGuest.mockResolvedValue([
|
|
{ id: 1, name: "North", address: "A", latitude: 55.6, longitude: 12.5, lanes: [], self_serve_enabled: true },
|
|
{ id: 2, name: "South", address: "B", latitude: 55.7, longitude: 12.6, lanes: [], self_serve_enabled: true },
|
|
]);
|
|
|
|
const departments = mountDepartments({
|
|
canAccessSuperUser: () => true,
|
|
});
|
|
|
|
await departments.fetchDepartments();
|
|
departments.startDepartmentSearch();
|
|
departments.selectDepartment(1);
|
|
|
|
expect(departments.isSearchingDepartments.value).toBe(false);
|
|
expect(departments.isForcingNearestDepartment.value).toBe(true);
|
|
expect(departments.nearestDepartment.value).toMatchObject({ id: 1, name: "North" });
|
|
|
|
departments.clearForcedDepartment();
|
|
await nextTick();
|
|
|
|
expect(departments.isForcingNearestDepartment.value).toBe(false);
|
|
expect(departments.forceNearestDepartmentEvaluationId.value).toBe(0);
|
|
expect(departments.nearestDepartment.value).toMatchObject({ id: 2, name: "South" });
|
|
});
|
|
|
|
it("keeps the wash start page usable when the guest departments request fails", async () => {
|
|
vi.spyOn(console, "warn").mockImplementation(() => {});
|
|
mocks.getDepartmentsGuest.mockRejectedValueOnce(new Error("Network Error"));
|
|
const departments = mountDepartments({ includeLanes: true });
|
|
|
|
await expect(departments.fetchDepartments()).resolves.toEqual([]);
|
|
|
|
expect(departments.guestDepartments.value).toEqual([]);
|
|
expect(departments.nearestDepartment.value).toBeNull();
|
|
expect(departments.departmentFetchError.value).toBe("Network Error");
|
|
expect(mocks.getDepartmentsGuest).toHaveBeenCalledWith({ include_lanes: true });
|
|
});
|
|
|
|
it("clears the department fetch error after a successful retry", async () => {
|
|
vi.spyOn(console, "warn").mockImplementation(() => {});
|
|
const department = {
|
|
id: 3,
|
|
name: "Glostrup",
|
|
address: "Fabriksparken 18",
|
|
latitude: 55.6,
|
|
longitude: 12.5,
|
|
self_serve_enabled: true,
|
|
lanes: [{ id: 7, status: "AVAILABLE", selfserve_enabled: true }],
|
|
};
|
|
mocks.getDepartmentsGuest.mockRejectedValueOnce(new Error("Gateway Timeout")).mockResolvedValueOnce([department]);
|
|
const departments = mountDepartments({ includeLanes: true });
|
|
|
|
await departments.fetchDepartments();
|
|
await departments.fetchDepartments();
|
|
|
|
expect(departments.departmentFetchError.value).toBeNull();
|
|
expect(departments.guestDepartments.value).toEqual([department]);
|
|
expect(departments.nearestDepartment.value).toMatchObject({
|
|
id: 3,
|
|
name: "Glostrup",
|
|
lanes: [{ id: 7 }],
|
|
});
|
|
});
|
|
|
|
it("uses the first self-serve department with an enabled lane as fallback when coordinates are missing", async () => {
|
|
mocks.locationRef.value = null;
|
|
mocks.getDepartmentsGuest.mockResolvedValue([
|
|
{
|
|
id: 1,
|
|
name: "Disabled module",
|
|
address: "A",
|
|
latitude: 55.6,
|
|
longitude: 12.5,
|
|
lanes: [{ id: 10, selfserve_enabled: true }],
|
|
self_serve_enabled: false,
|
|
},
|
|
{
|
|
id: 2,
|
|
name: "Disabled lane",
|
|
address: "B",
|
|
latitude: 55.7,
|
|
longitude: 12.6,
|
|
lanes: [{ id: 11, selfserve_enabled: false }],
|
|
self_serve_enabled: true,
|
|
},
|
|
{
|
|
id: 3,
|
|
name: "Fallback",
|
|
address: "C",
|
|
latitude: 55.8,
|
|
longitude: 12.7,
|
|
lanes: [
|
|
{ id: 12, selfserve_enabled: "off" },
|
|
{ id: 13, selfserve_enabled: true, products: [4] },
|
|
],
|
|
self_serve_enabled: true,
|
|
},
|
|
]);
|
|
|
|
const departments = mountDepartments({ includeLanes: true });
|
|
|
|
await departments.fetchDepartments();
|
|
|
|
expect(mocks.getDistance).not.toHaveBeenCalled();
|
|
expect(departments.nearestDepartment.value).toMatchObject({
|
|
id: 3,
|
|
name: "Fallback",
|
|
distance: null,
|
|
lanes: [{ id: 13 }],
|
|
});
|
|
expect(departments.departmentSelectionStrategy.value).toBe("fallback");
|
|
expect(departments.isDepartmentSelectionFallbackBased.value).toBe(true);
|
|
expect(departments.isDepartmentSelectionDistanceBased.value).toBe(false);
|
|
});
|
|
|
|
it("treats non-finite browser coordinates as missing instead of distance-selecting a department", async () => {
|
|
mocks.locationRef.value = {
|
|
coords: {
|
|
latitude: Number.POSITIVE_INFINITY,
|
|
longitude: Number.POSITIVE_INFINITY,
|
|
},
|
|
};
|
|
mocks.getDepartmentsGuest.mockResolvedValue([
|
|
{
|
|
id: 1,
|
|
name: "Fallback",
|
|
address: "A",
|
|
latitude: 55.6,
|
|
longitude: 12.5,
|
|
lanes: [{ id: 10, selfserve_enabled: true }],
|
|
self_serve_enabled: true,
|
|
},
|
|
{
|
|
id: 2,
|
|
name: "Other",
|
|
address: "B",
|
|
latitude: 55.7,
|
|
longitude: 12.6,
|
|
lanes: [{ id: 11, selfserve_enabled: true }],
|
|
self_serve_enabled: true,
|
|
},
|
|
]);
|
|
|
|
const departments = mountDepartments({ includeLanes: true });
|
|
|
|
await departments.fetchDepartments();
|
|
|
|
expect(mocks.getDistance).not.toHaveBeenCalled();
|
|
expect(departments.nearestDepartment.value).toMatchObject({ id: 1, name: "Fallback", distance: null });
|
|
expect(departments.departmentSelectionStrategy.value).toBe("fallback");
|
|
});
|
|
|
|
it("skips default zero department coordinates when selecting by GPS", async () => {
|
|
mocks.getDistance.mockImplementation((_from, to) => {
|
|
if (to.latitude === 55.7) {
|
|
return 12;
|
|
}
|
|
return 1;
|
|
});
|
|
mocks.getDepartmentsGuest.mockResolvedValue([
|
|
{
|
|
id: 1,
|
|
name: "Default coordinate row",
|
|
address: "A",
|
|
latitude: 0,
|
|
longitude: 0,
|
|
lanes: [{ id: 10, selfserve_enabled: true }],
|
|
self_serve_enabled: true,
|
|
},
|
|
{
|
|
id: 2,
|
|
name: "Real coordinate row",
|
|
address: "B",
|
|
latitude: 55.7,
|
|
longitude: 12.6,
|
|
lanes: [{ id: 11, selfserve_enabled: true }],
|
|
self_serve_enabled: true,
|
|
},
|
|
]);
|
|
|
|
const departments = mountDepartments({ includeLanes: true });
|
|
|
|
await departments.fetchDepartments();
|
|
|
|
expect(mocks.getDistance.mock.calls.every(([, to]) => to.latitude === 55.7 && to.longitude === 12.6)).toBe(true);
|
|
expect(departments.nearestDepartment.value).toMatchObject({
|
|
id: 2,
|
|
name: "Real coordinate row",
|
|
distance: 12,
|
|
});
|
|
expect(departments.departmentSelectionStrategy.value).toBe("distance");
|
|
});
|
|
|
|
it("pushes departments with default zero coordinates after valid coordinates when sorting by distance", async () => {
|
|
mocks.getDistance.mockImplementation((_from, to) => {
|
|
if (to.latitude === 55.7) {
|
|
return 12;
|
|
}
|
|
if (to.latitude === 55.8) {
|
|
return 8;
|
|
}
|
|
return 1;
|
|
});
|
|
const departments = mountDepartments({ includeLanes: true });
|
|
const unsortedDepartments = [
|
|
{ id: 1, name: "Default coordinate row", latitude: 0, longitude: 0 },
|
|
{ id: 2, name: "Farther valid row", latitude: 55.7, longitude: 12.6 },
|
|
{ id: 3, name: "Nearer valid row", latitude: 55.8, longitude: 12.7 },
|
|
];
|
|
|
|
expect(departments.orderDepartmentsByDistance(unsortedDepartments).map((department) => department.id)).toEqual([
|
|
3, 2, 1,
|
|
]);
|
|
});
|
|
|
|
it("clears the selected department for empty department responses", async () => {
|
|
mocks.getDepartmentsGuest.mockResolvedValueOnce([
|
|
{ id: 1, name: "North", address: "A", latitude: 55.6, longitude: 12.5, lanes: [], self_serve_enabled: true },
|
|
]);
|
|
const departments = mountDepartments();
|
|
|
|
await departments.fetchDepartments();
|
|
expect(departments.nearestDepartment.value).toMatchObject({ id: 1 });
|
|
|
|
mocks.getDepartmentsGuest.mockResolvedValueOnce([]);
|
|
await departments.fetchDepartments();
|
|
|
|
expect(departments.guestDepartments.value).toEqual([]);
|
|
expect(departments.nearestDepartment.value).toBeNull();
|
|
expect(departments.departmentSelectionStrategy.value).toBeNull();
|
|
});
|
|
|
|
it("does not fallback to disabled departments when coordinates are missing", async () => {
|
|
mocks.locationRef.value = { value: null };
|
|
mocks.getDepartmentsGuest.mockResolvedValue([
|
|
{
|
|
id: 1,
|
|
name: "Module disabled",
|
|
address: "A",
|
|
latitude: 55.6,
|
|
longitude: 12.5,
|
|
lanes: [{ id: 10, selfserve_enabled: true }],
|
|
self_serve_enabled: false,
|
|
},
|
|
{
|
|
id: 2,
|
|
name: "Lane disabled",
|
|
address: "B",
|
|
latitude: 55.7,
|
|
longitude: 12.6,
|
|
lanes: [{ id: 11, selfserve_enabled: "no" }],
|
|
self_serve_enabled: true,
|
|
},
|
|
]);
|
|
|
|
const departments = mountDepartments({ includeLanes: true });
|
|
|
|
await departments.fetchDepartments();
|
|
|
|
expect(departments.nearestDepartment.value).toBeNull();
|
|
expect(departments.departmentSelectionStrategy.value).toBeNull();
|
|
});
|
|
|
|
it("allows regular users to force a department when coordinates are missing", async () => {
|
|
mocks.locationRef.value = {};
|
|
mocks.getDepartmentsGuest.mockResolvedValue([
|
|
{
|
|
id: 1,
|
|
name: "Fallback",
|
|
address: "A",
|
|
latitude: 55.6,
|
|
longitude: 12.5,
|
|
lanes: [{ id: 10 }],
|
|
self_serve_enabled: true,
|
|
},
|
|
{
|
|
id: 2,
|
|
name: "Manual choice",
|
|
address: "B",
|
|
latitude: 55.7,
|
|
longitude: 12.6,
|
|
lanes: [{ id: 11 }],
|
|
self_serve_enabled: true,
|
|
},
|
|
]);
|
|
|
|
const departments = mountDepartments({ canAccessSuperUser: () => false });
|
|
|
|
await departments.fetchDepartments();
|
|
departments.startDepartmentSearch();
|
|
departments.selectDepartment(2);
|
|
|
|
expect(departments.isSearchingDepartments.value).toBe(false);
|
|
expect(departments.isForcingNearestDepartment.value).toBe(true);
|
|
expect(departments.nearestDepartment.value).toMatchObject({ id: 2, name: "Manual choice" });
|
|
expect(departments.departmentSelectionStrategy.value).toBe("forced");
|
|
});
|
|
});
|