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:
@@ -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}¤t_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}¤t_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,
|
||||
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user