Handle "STOP" command failures by interpreting "lane not occupied" as a safe-to-clear state. Refactor error handling, add clearActiveWashState, and extend unit/E2E tests with this scenario.

This commit is contained in:
Jeppe Bundgaard
2026-04-30 14:52:40 +02:00
parent 5d1199a133
commit bdffdca420
3 changed files with 163 additions and 25 deletions
+69 -25
View File
@@ -31,6 +31,7 @@ export function useWashSessionActions(options) {
const openingPropertyAccessGate = ref(false);
const openingPropertyExitGate = ref(false);
const isStartingWash = ref(false);
const lastCommandErrorMessage = ref(null);
const extractCommandErrorMessage = (
source,
@@ -46,9 +47,7 @@ export function useWashSessionActions(options) {
source?.message,
];
const message = candidates.find((candidate) => (
typeof candidate === "string" && candidate.trim() !== ""
));
const message = candidates.find((candidate) => typeof candidate === "string" && candidate.trim() !== "");
return message || fallback;
};
@@ -85,6 +84,19 @@ export function useWashSessionActions(options) {
return totalButtonsCompleted;
};
const isAlreadyStoppedStopError = (message) => {
const normalized = String(message || "").toLowerCase();
return normalized.includes("cannot stop lane") && normalized.includes("not occupied");
};
const clearActiveWashState = () => {
completedDurationMs.value = washStartTime.value ? now.value - washStartTime.value : 0;
washInProgress.value = false;
washLaneId.value = null;
stopElapsedTimer();
clearProgress();
};
const getThumbPosition = () => {
// Get the position of the thumb based on the session state
let defaultThumbPosition = 0;
@@ -107,18 +119,30 @@ export function useWashSessionActions(options) {
return null;
}
return `${API_URL}/department/lanes/dynamic-image?department=${departmentId}&lane=${laneId}&current_step=${machineStartCurrentStep.value}&buttons=${JSON.stringify(getButtonsToPress())}&vehicle_type=${vehicleTypeSelect.value}&thumb_position=${getThumbPosition()}`;
return `${API_URL}/department/lanes/dynamic-image?department=${departmentId}&lane=${laneId}&current_step=${
machineStartCurrentStep.value
}&buttons=${JSON.stringify(getButtonsToPress())}&vehicle_type=${
vehicleTypeSelect.value
}&thumb_position=${getThumbPosition()}`;
});
const executeSelfServeCommand = async (laneId, command, args = {
customer_number: null,
license_plate: null,
}) => {
const executeSelfServeCommand = async (
laneId,
command,
args = {
customer_number: null,
license_plate: null,
},
options = {}
) => {
if (!nearestDepartment.value) {
alertFn("Ingen afdeling valgt.");
return null;
}
const { suppressAlert = false } = options;
lastCommandErrorMessage.value = null;
try {
const response = await request("/modules/self-serve/lane/command", "post", {
lane_id: laneId,
@@ -127,14 +151,22 @@ export function useWashSessionActions(options) {
});
const successValue = response?.data?.success ?? response?.success;
if (successValue === false) {
alertFn(extractCommandErrorMessage(response));
const message = extractCommandErrorMessage(response);
lastCommandErrorMessage.value = message;
if (!suppressAlert) {
alertFn(message);
}
return null;
}
return response;
} catch (error) {
console.error(`Error executing command ${command} on lane ${laneId}:`, error);
alertFn(extractCommandErrorMessage(error));
const message = extractCommandErrorMessage(error);
lastCommandErrorMessage.value = message;
if (!suppressAlert) {
alertFn(message);
}
return null;
}
};
@@ -167,13 +199,11 @@ export function useWashSessionActions(options) {
}
};
const openPropertyAccessGate = async (laneId = null) => (
executePropertyGateCommand("OPEN_PROPERTY_ACCESS_GATE", laneId, openingPropertyAccessGate)
);
const openPropertyAccessGate = async (laneId = null) =>
executePropertyGateCommand("OPEN_PROPERTY_ACCESS_GATE", laneId, openingPropertyAccessGate);
const openPropertyExitGate = async (laneId = null) => (
executePropertyGateCommand("OPEN_PROPERTY_EXIT_GATE", laneId, openingPropertyExitGate)
);
const openPropertyExitGate = async (laneId = null) =>
executePropertyGateCommand("OPEN_PROPERTY_EXIT_GATE", laneId, openingPropertyExitGate);
const onStartWash = async (laneId, licensePlate, customerNumber, targetStep = steps.WASH_IN_PROGRESS) => {
if (isStartingWash.value) {
@@ -263,22 +293,36 @@ export function useWashSessionActions(options) {
return false;
}
const stopResponse = await executeSelfServeCommand(laneId, "STOP");
const stopResponse = await executeSelfServeCommand(
laneId,
"STOP",
{
customer_number: null,
license_plate: null,
},
{ suppressAlert: true }
);
if (!stopResponse) {
if (isAlreadyStoppedStopError(lastCommandErrorMessage.value)) {
clearActiveWashState();
return true;
}
alertFn(lastCommandErrorMessage.value || "Der opstod en fejl ved udforelse af kommandoen. Prov igen senere.");
return false;
}
completedDurationMs.value = washStartTime.value ? (now.value - washStartTime.value) : 0;
washInProgress.value = false;
washLaneId.value = null;
stopElapsedTimer();
clearProgress();
clearActiveWashState();
return true;
};
watch([completedTasks, activeTasks], () => {
getCompletedButtons();
}, { deep: true });
watch(
[completedTasks, activeTasks],
() => {
getCompletedButtons();
},
{ deep: true }
);
return {
machineStartCurrentStep,
+49
View File
@@ -633,6 +633,55 @@ test.describe("Self-serve wash", () => {
await expect(page.getByTestId("self-serve-nav-complete")).toBeVisible();
});
test("already-ended stop response completes stale in-progress progress", async ({ page }) => {
await seedSavedProgress(page, {
washInProgress: true,
washLaneId: 7,
washStartTime: Date.now() - 30_000,
currentStep: 4,
licensePlateInput: "AB12345",
vehicleTypeSelect: 2,
radioLaneOption: 7,
radioWashType: "Manual",
customerNumberInput: "12345679",
});
await mockApi(page, {
authenticated: true,
permissions: ["user"],
selfServe: {
commandResponses: [
{
success: false,
data: {
message: "Failed to execute command: Cannot stop lane: Lane is not occupied.",
},
},
],
},
});
await primeSession(page, {
token: "self-serve-stop-already-ended-token",
permissions: ["user"],
});
await page.goto("/user/wash/start");
await expect(page.getByTestId("self-serve-live-elapsed")).toBeVisible({ timeout: 10_000 });
await advanceGuidedWashToLastStep(page);
const stopCommandRequestPromise = waitForLaneCommandRequest(page, "STOP");
await page.getByTestId("self-serve-nav-complete").click();
const stopCommandRequest = await stopCommandRequestPromise;
expect(stopCommandRequest.postDataJSON?.()).toMatchObject({
lane_id: 7,
command: "STOP",
});
await expect(page.getByTestId("self-serve-action-error")).toBeHidden();
await expect(page.getByTestId("self-serve-completed-step")).toBeVisible();
await expect(page.getByTestId("self-serve-live-elapsed")).toBeHidden();
});
test("edge gateway preview and summary refreshes stay read-only until confirmation", async ({ page }) => {
const requests = captureSelfServeGatewayRequests(page);
@@ -279,6 +279,51 @@ describe("useWashSessionActions property gate commands", () => {
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);