363 lines
13 KiB
JavaScript
363 lines
13 KiB
JavaScript
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
|
|
const mocks = vi.hoisted(() => ({
|
|
request: vi.fn(),
|
|
parseErrorMessage: vi.fn((error) => error?.message || String(error || "Unknown error")),
|
|
}));
|
|
|
|
vi.mock("@/components/session/token/SessionUser.vue", () => ({
|
|
SessionUser: {
|
|
request: mocks.request,
|
|
functions: {
|
|
parseErrorMessage: mocks.parseErrorMessage,
|
|
},
|
|
},
|
|
}));
|
|
|
|
import {
|
|
__resetMachineConnectivityStoreForTests,
|
|
parseRelayStatus,
|
|
relayKinds,
|
|
useMachineConnectivity,
|
|
} from "@/views/dashboards/superUserDashboard/selfserve/components/SelfServeMachineConnectivity.vue";
|
|
|
|
const statusEndpoints = {
|
|
MACHINE: "/modules/self-serve/lane/relay/machine/status",
|
|
PROGRAM_PICKER: "/modules/self-serve/lane/relay/machine_program_picker/status",
|
|
CLEANER: "/modules/self-serve/lane/relay/machine_cleaner/status",
|
|
};
|
|
|
|
const setEndpoints = {
|
|
MACHINE: "/modules/self-serve/lane/relay/machine/set",
|
|
PROGRAM_PICKER: "/modules/self-serve/lane/relay/machine_program_picker/set",
|
|
CLEANER: "/modules/self-serve/lane/relay/machine_cleaner/set",
|
|
};
|
|
|
|
const inProgressEndpoint = "/modules/self-serve/lane/wash/in-progress";
|
|
|
|
const buildRelayStatus = (
|
|
laneId,
|
|
relay,
|
|
{ relayId = `${relay.toLowerCase()}-${laneId}`, online = true, on = false } = {}
|
|
) => ({
|
|
lane_id: laneId,
|
|
relay,
|
|
relay_id: relayId,
|
|
online,
|
|
on,
|
|
});
|
|
|
|
const buildInProgressDetails = (
|
|
laneId,
|
|
{ inProgress = false, session = null, customer = null, vehicle = null } = {}
|
|
) => ({
|
|
lane_id: laneId,
|
|
in_progress: inProgress,
|
|
session,
|
|
customer,
|
|
vehicle,
|
|
});
|
|
|
|
const createDeferred = () => {
|
|
let resolve;
|
|
let reject;
|
|
const promise = new Promise((res, rej) => {
|
|
resolve = res;
|
|
reject = rej;
|
|
});
|
|
return { promise, resolve, reject };
|
|
};
|
|
|
|
describe("self-serve machine connectivity store", () => {
|
|
beforeEach(() => {
|
|
vi.useRealTimers();
|
|
mocks.request.mockReset();
|
|
mocks.parseErrorMessage.mockReset();
|
|
mocks.parseErrorMessage.mockImplementation((error) => error?.message || String(error || "Unknown error"));
|
|
__resetMachineConnectivityStoreForTests();
|
|
});
|
|
|
|
afterEach(() => {
|
|
vi.useRealTimers();
|
|
__resetMachineConnectivityStoreForTests();
|
|
});
|
|
|
|
it("hydrates all relay statuses for a lane via shared store", async () => {
|
|
const laneId = 7;
|
|
|
|
mocks.request.mockImplementation(async (endpoint) => {
|
|
if (endpoint === statusEndpoints.MACHINE) {
|
|
return { data: { data: buildRelayStatus(laneId, relayKinds.MACHINE, { on: true }) } };
|
|
}
|
|
if (endpoint === statusEndpoints.PROGRAM_PICKER) {
|
|
return { data: { data: buildRelayStatus(laneId, relayKinds.PROGRAM_PICKER, { on: false }) } };
|
|
}
|
|
if (endpoint === statusEndpoints.CLEANER) {
|
|
return { data: { data: buildRelayStatus(laneId, relayKinds.CLEANER, { online: false, on: false }) } };
|
|
}
|
|
throw new Error(`Unexpected endpoint: ${endpoint}`);
|
|
});
|
|
|
|
const connectivity = useMachineConnectivity(laneId);
|
|
await connectivity.fetchAllRelayStatuses();
|
|
|
|
expect(connectivity.statuses.value.MACHINE).toMatchObject({ lane_id: laneId, on: true, online: true });
|
|
expect(connectivity.statuses.value.PROGRAM_PICKER).toMatchObject({ lane_id: laneId, on: false, online: true });
|
|
expect(connectivity.statuses.value.CLEANER).toMatchObject({ lane_id: laneId, on: false, online: false });
|
|
expect(connectivity.error.value).toBeNull();
|
|
});
|
|
|
|
it("toggles a relay and refreshes relay status when set response is envelope-only", async () => {
|
|
const laneId = 11;
|
|
let machineOn = false;
|
|
|
|
mocks.request.mockImplementation(async (endpoint, method, payload) => {
|
|
if (endpoint === setEndpoints.MACHINE && method === "POST") {
|
|
machineOn = Boolean(payload?.on);
|
|
return { data: { success: true } };
|
|
}
|
|
if (endpoint === statusEndpoints.MACHINE && method === "GET") {
|
|
return { data: { data: buildRelayStatus(laneId, relayKinds.MACHINE, { on: machineOn }) } };
|
|
}
|
|
if (endpoint === statusEndpoints.PROGRAM_PICKER && method === "GET") {
|
|
return { data: { data: buildRelayStatus(laneId, relayKinds.PROGRAM_PICKER) } };
|
|
}
|
|
if (endpoint === statusEndpoints.CLEANER && method === "GET") {
|
|
return { data: { data: buildRelayStatus(laneId, relayKinds.CLEANER) } };
|
|
}
|
|
throw new Error(`Unexpected endpoint: ${endpoint}`);
|
|
});
|
|
|
|
const connectivity = useMachineConnectivity(laneId);
|
|
|
|
await connectivity.fetchAllRelayStatuses();
|
|
expect(connectivity.statuses.value.MACHINE?.on).toBe(false);
|
|
|
|
await connectivity.toggleRelay(relayKinds.MACHINE, true);
|
|
expect(connectivity.statuses.value.MACHINE?.on).toBe(true);
|
|
});
|
|
|
|
it("runs force and command actions through shared connectivity helpers", async () => {
|
|
const laneId = 15;
|
|
|
|
mocks.request.mockResolvedValue({ data: { success: true } });
|
|
|
|
const connectivity = useMachineConnectivity(laneId);
|
|
await connectivity.forceEnableMachine({ duration: 120, licensePlate: "ab12345" });
|
|
await connectivity.forceDisableMachine("cd67890");
|
|
await connectivity.forceStopLane({ sessionId: 91, bill: true, reason: "clear app runtime" });
|
|
await connectivity.stopWash();
|
|
|
|
expect(mocks.request).toHaveBeenNthCalledWith(1, "/modules/self-serve/lane/force/machine/enable", "POST", {
|
|
lane_id: laneId,
|
|
duration: 120,
|
|
license_plate: "AB12345",
|
|
});
|
|
expect(mocks.request).toHaveBeenNthCalledWith(2, "/modules/self-serve/lane/force/machine/disable", "POST", {
|
|
lane_id: laneId,
|
|
license_plate: "CD67890",
|
|
});
|
|
expect(mocks.request).toHaveBeenNthCalledWith(3, "/modules/self-serve/lane/force/stop", "POST", {
|
|
lane_id: laneId,
|
|
session_id: 91,
|
|
bill: true,
|
|
reason: "clear app runtime",
|
|
});
|
|
expect(mocks.request).toHaveBeenNthCalledWith(4, "/modules/self-serve/lane/command", "POST", {
|
|
lane_id: laneId,
|
|
command: "STOP",
|
|
});
|
|
});
|
|
|
|
it("runs entrance/exit gate actions through shared connectivity helpers", async () => {
|
|
const laneId = 18;
|
|
|
|
mocks.request.mockResolvedValue({ data: { success: true } });
|
|
|
|
const connectivity = useMachineConnectivity(laneId);
|
|
await connectivity.openEntranceGate();
|
|
await connectivity.openExitGate();
|
|
|
|
expect(mocks.request).toHaveBeenNthCalledWith(1, "/modules/self-serve/lane/gate/open", "POST", {
|
|
lane_id: laneId,
|
|
gate: "ENTRANCE",
|
|
});
|
|
expect(mocks.request).toHaveBeenNthCalledWith(2, "/modules/self-serve/lane/gate/open", "POST", {
|
|
lane_id: laneId,
|
|
gate: "EXIT",
|
|
});
|
|
});
|
|
|
|
it("fetches in-progress wash details through shared connectivity helpers", async () => {
|
|
const laneId = 19;
|
|
|
|
mocks.request.mockResolvedValue({
|
|
data: {
|
|
data: buildInProgressDetails(laneId, {
|
|
inProgress: true,
|
|
session: { id: 55, reg: "AB12345", customer_number: 9001 },
|
|
customer: { id: 2, customer_number: 9001, display_name: "Jane Doe" },
|
|
vehicle: { id: 3, reg: "AB12345" },
|
|
}),
|
|
},
|
|
});
|
|
|
|
const connectivity = useMachineConnectivity(laneId);
|
|
const details = await connectivity.fetchInProgressDetails();
|
|
|
|
expect(mocks.request).toHaveBeenCalledWith(inProgressEndpoint, "GET", { lane_id: laneId });
|
|
expect(details).toMatchObject({
|
|
lane_id: laneId,
|
|
in_progress: true,
|
|
customer: { display_name: "Jane Doe" },
|
|
vehicle: { reg: "AB12345" },
|
|
});
|
|
expect(connectivity.inProgressDetails.value).toMatchObject({
|
|
lane_id: laneId,
|
|
in_progress: true,
|
|
});
|
|
expect(connectivity.inProgressLoading.value).toBe(false);
|
|
});
|
|
|
|
it("sets lane error when in-progress details request fails", async () => {
|
|
const laneId = 20;
|
|
|
|
mocks.request.mockRejectedValue(new Error("in-progress unavailable"));
|
|
|
|
const connectivity = useMachineConnectivity(laneId);
|
|
await expect(connectivity.fetchInProgressDetails()).rejects.toThrow("in-progress unavailable");
|
|
expect(connectivity.error.value).toContain("in-progress unavailable");
|
|
expect(connectivity.inProgressLoading.value).toBe(false);
|
|
});
|
|
|
|
it("starts and stops polling without leaking background status requests", async () => {
|
|
vi.useFakeTimers();
|
|
const laneId = 4;
|
|
|
|
mocks.request.mockImplementation(async (endpoint) => {
|
|
if (endpoint === inProgressEndpoint) {
|
|
return { data: { data: buildInProgressDetails(laneId) } };
|
|
}
|
|
|
|
const relay = Object.entries(statusEndpoints).find(([, value]) => value === endpoint)?.[0];
|
|
if (!relay) {
|
|
throw new Error(`Unexpected endpoint: ${endpoint}`);
|
|
}
|
|
return { data: { data: buildRelayStatus(laneId, relay, { on: false }) } };
|
|
});
|
|
|
|
const connectivity = useMachineConnectivity(laneId);
|
|
await connectivity.startPolling(1000);
|
|
|
|
expect(connectivity.isPolling.value).toBe(true);
|
|
await vi.advanceTimersByTimeAsync(2200);
|
|
|
|
const statusCallCountBeforeStop = mocks.request.mock.calls.length;
|
|
expect(statusCallCountBeforeStop).toBeGreaterThanOrEqual(12);
|
|
|
|
connectivity.stopPolling();
|
|
expect(connectivity.isPolling.value).toBe(false);
|
|
|
|
await vi.advanceTimersByTimeAsync(2200);
|
|
expect(mocks.request.mock.calls.length).toBe(statusCallCountBeforeStop);
|
|
});
|
|
|
|
it("keeps successful relay statuses and surfaces an error when one relay fetch fails", async () => {
|
|
const laneId = 22;
|
|
|
|
mocks.request.mockImplementation(async (endpoint) => {
|
|
if (endpoint === statusEndpoints.MACHINE) {
|
|
return { data: { data: buildRelayStatus(laneId, relayKinds.MACHINE, { on: true }) } };
|
|
}
|
|
if (endpoint === statusEndpoints.PROGRAM_PICKER) {
|
|
throw new Error("program picker unavailable");
|
|
}
|
|
if (endpoint === statusEndpoints.CLEANER) {
|
|
return { data: { data: buildRelayStatus(laneId, relayKinds.CLEANER, { on: false }) } };
|
|
}
|
|
throw new Error(`Unexpected endpoint: ${endpoint}`);
|
|
});
|
|
|
|
const connectivity = useMachineConnectivity(laneId);
|
|
await connectivity.fetchAllRelayStatuses();
|
|
|
|
expect(connectivity.statuses.value.MACHINE?.on).toBe(true);
|
|
expect(connectivity.statuses.value.CLEANER?.on).toBe(false);
|
|
expect(connectivity.statuses.value.PROGRAM_PICKER).toBeNull();
|
|
expect(connectivity.error.value).toContain("program picker unavailable");
|
|
});
|
|
|
|
it("throws and stores lane error when all relay status requests fail", async () => {
|
|
const laneId = 24;
|
|
|
|
mocks.request.mockRejectedValue(new Error("all relays offline"));
|
|
|
|
const connectivity = useMachineConnectivity(laneId);
|
|
await expect(connectivity.fetchAllRelayStatuses()).rejects.toThrow("all relays offline");
|
|
expect(connectivity.error.value).toContain("all relays offline");
|
|
});
|
|
|
|
it("tracks command loading while lane commands are in-flight and resets after completion", async () => {
|
|
const laneId = 28;
|
|
const deferred = createDeferred();
|
|
|
|
mocks.request.mockReturnValue(deferred.promise);
|
|
|
|
const connectivity = useMachineConnectivity(laneId);
|
|
const stopPromise = connectivity.openEntranceGate();
|
|
|
|
expect(connectivity.commandLoading.value.openEntranceGate).toBe(true);
|
|
|
|
deferred.resolve({ data: { success: true } });
|
|
await stopPromise;
|
|
|
|
expect(connectivity.commandLoading.value.openEntranceGate).toBe(false);
|
|
expect(connectivity.error.value).toBeNull();
|
|
});
|
|
|
|
it("keeps polling active until all subscribers for the same lane have unsubscribed", async () => {
|
|
vi.useFakeTimers();
|
|
const laneId = 31;
|
|
|
|
mocks.request.mockImplementation(async (endpoint) => {
|
|
if (endpoint === inProgressEndpoint) {
|
|
return { data: { data: buildInProgressDetails(laneId) } };
|
|
}
|
|
|
|
const relay = Object.entries(statusEndpoints).find(([, value]) => value === endpoint)?.[0];
|
|
if (!relay) {
|
|
throw new Error(`Unexpected endpoint: ${endpoint}`);
|
|
}
|
|
return { data: { data: buildRelayStatus(laneId, relay) } };
|
|
});
|
|
|
|
const first = useMachineConnectivity(laneId);
|
|
const second = useMachineConnectivity(laneId);
|
|
|
|
await first.startPolling(1000);
|
|
await second.startPolling(1000);
|
|
|
|
const callsAfterStart = mocks.request.mock.calls.length;
|
|
|
|
first.stopPolling();
|
|
expect(first.isPolling.value).toBe(true);
|
|
|
|
await vi.advanceTimersByTimeAsync(1100);
|
|
expect(mocks.request.mock.calls.length).toBeGreaterThan(callsAfterStart);
|
|
|
|
second.stopPolling();
|
|
expect(first.isPolling.value).toBe(false);
|
|
|
|
const callsAfterFinalStop = mocks.request.mock.calls.length;
|
|
await vi.advanceTimersByTimeAsync(1100);
|
|
expect(mocks.request.mock.calls.length).toBe(callsAfterFinalStop);
|
|
});
|
|
|
|
it("maps relay status payloads to display labels", () => {
|
|
expect(parseRelayStatus(null)).toBe("MAINTENANCE");
|
|
expect(parseRelayStatus(buildRelayStatus(1, relayKinds.MACHINE, { online: false }))).toBe("OFFLINE");
|
|
expect(parseRelayStatus(buildRelayStatus(1, relayKinds.MACHINE, { online: true, on: true }))).toBe("ON");
|
|
expect(parseRelayStatus(buildRelayStatus(1, relayKinds.MACHINE, { online: true, on: false }))).toBe("OFF");
|
|
});
|
|
});
|