import { ref } from "vue"; import { describe, expect, it, vi } from "vitest"; import { useWashSessionActions } from "@/composables/useWashSessionActions.js"; const createActions = (overrides = {}) => { const request = vi.fn(async () => ({ data: { success: true } })); const alertFn = vi.fn(); const options = { request, alertFn, nearestDepartment: ref({ id: 1, lanes: [{ id: 7 }], }), vehicleTypeSelect: ref(2), washLaneId: ref(7), washInProgress: ref(true), washStartTime: ref(Date.now() - 30_000), completedDurationMs: ref(null), now: ref(Date.now()), currentStep: ref(4), steps: { VEHICLE: 0, QUESTIONS: 1, SELECT_LANE: 2, TASKS: 3, WASH_IN_PROGRESS: 4, COMPLETED: 5, }, radioWashType: ref("Manual"), activeTasks: ref([]), completedTasks: ref({}), saveProgress: vi.fn(), clearProgress: vi.fn(), startElapsedTimer: vi.fn(), stopElapsedTimer: vi.fn(), updateLaneAllowedServices: vi.fn(), fetchWashSummary: vi.fn(), enableMachineRelay: vi.fn(), isServiceAllowed: vi.fn(() => true), ...overrides, }; return { request: options.request, alertFn, options, actions: useWashSessionActions(options), }; }; describe("useWashSessionActions property gate commands", () => { it("opens property access gate with explicit lane id and tracks loading state", async () => { const deferred = {}; deferred.promise = new Promise((resolve) => { deferred.resolve = resolve; }); const { actions, request } = createActions({ request: vi.fn(() => deferred.promise), }); const commandPromise = actions.openPropertyAccessGate(9); expect(actions.openingPropertyAccessGate.value).toBe(true); deferred.resolve({ data: { success: true } }); await commandPromise; expect(actions.openingPropertyAccessGate.value).toBe(false); expect(request).toHaveBeenCalledWith("/modules/self-serve/lane/command", "post", { lane_id: 9, command: "OPEN_PROPERTY_ACCESS_GATE", customer_number: null, license_plate: null, }); }); it("opens property exit gate using active wash lane when lane argument is omitted", async () => { const { actions, request } = createActions(); await actions.openPropertyExitGate(); expect(request).toHaveBeenCalledWith("/modules/self-serve/lane/command", "post", { lane_id: 7, command: "OPEN_PROPERTY_EXIT_GATE", customer_number: null, license_plate: null, }); }); it("warns and skips command when no lane id can be resolved", async () => { const { actions, request, alertFn } = createActions({ washLaneId: ref(null), }); const result = await actions.openPropertyAccessGate(); expect(result).toBeNull(); expect(request).not.toHaveBeenCalled(); expect(alertFn).toHaveBeenCalledWith("Ingen vaskebane valgt."); }); it("keeps flow non-blocking when property gate command fails", async () => { const consoleErrorSpy = vi.spyOn(console, "error").mockImplementation(() => {}); const { actions, alertFn } = createActions({ request: vi.fn(async () => { throw new Error("command failed"); }), }); const result = await actions.openPropertyAccessGate(); expect(result).toBeNull(); expect(actions.openingPropertyAccessGate.value).toBe(false); expect(alertFn).toHaveBeenCalledWith("command failed"); consoleErrorSpy.mockRestore(); }); it("can suppress property exit gate alerts for automatic wash completion", async () => { const { actions, alertFn } = createActions({ request: vi.fn(async () => ({ data: { success: false, data: { message: "No exit gate configured", }, }, })), }); const result = await actions.openPropertyExitGate(7, { suppressAlert: true }); expect(result).toBeNull(); expect(alertFn).not.toHaveBeenCalled(); }); it("builds dynamic image URLs from ordered task button steps", () => { const activeTasks = ref([ { id: 101, services: ["PROGRAM_PICKER"], buttons: ["reset", 0], dynamic_images_vehicle_type: 2, }, { id: 102, buttons: [2, "start", 5], dynamic_images_vehicle_type: 3, }, ]); const completedTasks = ref({ 101: true }); const { actions } = createActions({ activeTasks, completedTasks, }); const url = new URL(actions.dynamicImageUrl.value, "https://app.example.test"); expect(JSON.parse(url.searchParams.get("buttons"))).toEqual(["program_picker", "reset", 0, 2, "start", 5]); expect(url.searchParams.get("current_step")).toBe("3"); expect(url.searchParams.get("thumb_position")).toBe("2"); expect(actions.machineStartCurrentStep.value).toBe(3); }); it("does not enter wash-in-progress state when the start command fails", async () => { const consoleErrorSpy = vi.spyOn(console, "error").mockImplementation(() => {}); const washInProgress = ref(false); const washLaneId = ref(null); const washStartTime = ref(null); const currentStep = ref(2); const request = vi.fn(async (_url, _method, body) => { if (body.command === "START") { return { data: { success: false, data: { message: "Edge gateway command timed out", }, }, }; } return { data: { success: true } }; }); const { actions, alertFn } = createActions({ request, washInProgress, washLaneId, washStartTime, currentStep, }); const result = await actions.onStartWash(7, "ab12345", 12345679, 4); expect(result).toBe(false); expect(washInProgress.value).toBe(false); expect(washLaneId.value).toBeNull(); expect(washStartTime.value).toBeNull(); expect(currentStep.value).toBe(2); expect(actions.isStartingWash.value).toBe(false); expect(alertFn).toHaveBeenCalledWith("Edge gateway command timed out"); expect(request).toHaveBeenCalledWith("/modules/self-serve/lane/command", "post", { lane_id: 7, command: "START", customer_number: 12345679, license_plate: "AB12345", defer_relay_side_effects: true, }); consoleErrorSpy.mockRestore(); }); it("does not duplicate action alerts when allowed-service setup fails before start", async () => { const consoleErrorSpy = vi.spyOn(console, "error").mockImplementation(() => {}); const washInProgress = ref(false); const washLaneId = ref(null); const washStartTime = ref(null); const currentStep = ref(2); const updateLaneAllowedServices = vi.fn(async () => { throw new Error("Edge gateway command timed out"); }); const { actions, request, alertFn } = createActions({ updateLaneAllowedServices, washInProgress, washLaneId, washStartTime, currentStep, }); const result = await actions.onStartWash(7, "ab12345", 12345679, 4); expect(result).toBe(false); expect(updateLaneAllowedServices).toHaveBeenCalledWith(7); expect(request).not.toHaveBeenCalled(); expect(alertFn).not.toHaveBeenCalled(); expect(washInProgress.value).toBe(false); expect(currentStep.value).toBe(2); consoleErrorSpy.mockRestore(); }); it("keeps the wash started when the post-start machine relay retry fails", async () => { const consoleErrorSpy = vi.spyOn(console, "error").mockImplementation(() => {}); const washInProgress = ref(false); const washLaneId = ref(null); const washStartTime = ref(null); const currentStep = ref(2); const enableMachineRelay = vi.fn(async () => { throw new Error("Edge gateway command timed out"); }); const { actions, request, alertFn } = createActions({ washInProgress, washLaneId, washStartTime, currentStep, radioWashType: ref("Machine"), enableMachineRelay, isServiceAllowed: vi.fn(() => true), }); const result = await actions.onStartWash(7, "ab12345", 12345679, 4); expect(result).toBe(true); expect(washInProgress.value).toBe(true); expect(washLaneId.value).toBe(7); expect(currentStep.value).toBe(4); expect(enableMachineRelay).toHaveBeenCalledWith(7); expect(alertFn).not.toHaveBeenCalled(); expect(request).toHaveBeenCalledWith("/modules/self-serve/lane/command", "post", { lane_id: 7, command: "START", customer_number: 12345679, license_plate: "AB12345", defer_relay_side_effects: true, }); consoleErrorSpy.mockRestore(); }); it("keeps the active wash open when the stop command fails", async () => { const washInProgress = ref(true); const washLaneId = ref(7); const washStartTime = ref(Date.now() - 45_000); const completedDurationMs = ref(null); const clearProgress = vi.fn(); const stopElapsedTimer = vi.fn(); const request = vi.fn(async (_url, _method, body) => { if (body.command === "STOP") { return { data: { success: false, data: { message: "Stop command failed", }, }, }; } return { data: { success: true } }; }); const { actions, alertFn } = createActions({ request, washInProgress, washLaneId, washStartTime, completedDurationMs, clearProgress, stopElapsedTimer, }); const result = await actions.onStopWash(7); expect(result).toBe(false); expect(alertFn).toHaveBeenCalledWith("Stop command failed"); expect(washInProgress.value).toBe(true); expect(washLaneId.value).toBe(7); expect(completedDurationMs.value).toBeNull(); expect(stopElapsedTimer).not.toHaveBeenCalled(); expect(clearProgress).not.toHaveBeenCalled(); }); it("clears active wash state when stop reports the lane is already unoccupied", async () => { const washInProgress = ref(true); const washLaneId = ref(7); const washStartTime = ref(1_000); const completedDurationMs = ref(null); const now = ref(46_000); const clearProgress = vi.fn(); const stopElapsedTimer = vi.fn(); const request = vi.fn(async (_url, _method, body) => { if (body.command === "STOP") { return { data: { success: false, data: { message: "Failed to execute command: Cannot stop lane: Lane is not occupied.", }, }, }; } return { data: { success: true } }; }); const { actions, alertFn } = createActions({ request, washInProgress, washLaneId, washStartTime, completedDurationMs, now, clearProgress, stopElapsedTimer, }); const result = await actions.onStopWash(7); expect(result).toBe(true); expect(alertFn).not.toHaveBeenCalled(); expect(completedDurationMs.value).toBe(45_000); expect(washInProgress.value).toBe(false); expect(washLaneId.value).toBeNull(); expect(stopElapsedTimer).toHaveBeenCalledTimes(1); expect(clearProgress).toHaveBeenCalledTimes(1); }); it("clears active wash state only after the stop command succeeds", async () => { const washInProgress = ref(true); const washLaneId = ref(7); const washStartTime = ref(1_000); const completedDurationMs = ref(null); const now = ref(61_000); const clearProgress = vi.fn(); const stopElapsedTimer = vi.fn(); const { actions, request, alertFn } = createActions({ washInProgress, washLaneId, washStartTime, completedDurationMs, now, clearProgress, stopElapsedTimer, }); const result = await actions.onStopWash(7); expect(result).toBe(true); expect(alertFn).not.toHaveBeenCalled(); expect(request).toHaveBeenCalledWith("/modules/self-serve/lane/command", "post", { lane_id: 7, command: "STOP", customer_number: null, license_plate: null, }); expect(completedDurationMs.value).toBe(60_000); expect(washInProgress.value).toBe(false); expect(washLaneId.value).toBeNull(); expect(stopElapsedTimer).toHaveBeenCalledTimes(1); expect(clearProgress).toHaveBeenCalledTimes(1); }); });