Add Danish self-serve labels and guided wash flow steps. Update UI with guided step actions and bottom control integration. Fix translations and refine E2E/unit test coverage for self-serve flow behavior.

This commit is contained in:
Jeppe Bundgaard
2026-04-29 16:01:34 +02:00
parent a15d6841c5
commit fc9c3700a1
10 changed files with 513 additions and 62 deletions
+102
View File
@@ -1116,6 +1116,7 @@ function buildPathOutcomesResponse(request = {}) {
question_count: 2,
question_ids: [11, 12],
max_states: 2048,
path_sample_count: 2,
},
outcomes: [
{
@@ -1220,6 +1221,103 @@ function buildPathOutcomesResponse(request = {}) {
node_ids: ["question:11"],
},
],
paths: [
{
id: "path-1",
result: "Allowed",
summary: "Allowed / MACHINE / 1 task / 2 signals",
allowed: true,
services: ["MACHINE"],
tasks: [
{
id: 41,
node_id: "task:41",
label: "Fold mirrors",
services: ["MACHINE"],
buttons: ["reset", 1, "start"],
order_priority: 1,
},
],
signals: [
{
sequence: 1,
runtime_stage: "eligibility_sync",
signal_type: "session_event",
relay_role: "SESSION",
source: "none",
virtual: false,
predicted_status: "sent",
payload: { allowed: true, allowed_services: ["MACHINE"] },
},
{
sequence: 2,
runtime_stage: "machine_start_signal",
signal_type: "shelly_event",
relay_role: "MACHINE",
relay_id: "M-7",
target_gateway_label: "Virtual Studio Gateway",
target_binding: "binding:virtual-main:M-7:0",
source: "virtual",
virtual: true,
predicted_status: "virtual_only",
payload: { event: "input.toggle_on", bill_machine_wash: true },
},
],
task_count: 1,
signal_count: 2,
answers: [
{
question_id: 11,
question: "Are mirrors folded?",
node_id: "question:11",
answer: true,
answer_label: "Yes",
},
{
question_id: 12,
question: "Is the lift lowered?",
node_id: "question:12",
answer: true,
answer_label: "Yes",
},
],
scope: { lane: "Lane 7", vehicle_type: vehicleType },
node_ids: ["question:11", "question:12", "task:41", "binding:virtual-main:M-7:0"],
},
{
id: "path-2",
result: "Blocked",
summary: "Blocked / No services / 0 tasks / 1 signal",
allowed: false,
services: [],
tasks: [],
signals: [
{
sequence: 1,
runtime_stage: "eligibility_sync",
signal_type: "session_event",
relay_role: "SESSION",
source: "none",
virtual: false,
predicted_status: "skipped",
payload: { allowed: false, allowed_services: [] },
},
],
task_count: 0,
signal_count: 1,
answers: [
{
question_id: 11,
question: "Are mirrors folded?",
node_id: "question:11",
answer: false,
answer_label: "No",
},
],
scope: { lane: "Lane 7", vehicle_type: vehicleType },
node_ids: ["question:11"],
},
],
warnings: [],
truncated: false,
};
@@ -2140,6 +2238,10 @@ test.describe("All-in-one self-serve studio", () => {
await expect.poll(() => captured.pathOutcomes[0]?.vehicle_type_id).toBe(8);
await expect(page.getByTestId("studio-path-outcomes-summary")).toContainText("2");
await expect(page.getByTestId("studio-path-outcome-detail")).toContainText("MACHINE");
await expect(page.getByTestId("studio-path-results")).toContainText("Paths and Results");
await expect(page.getByTestId("studio-path-results")).toContainText("Allowed / MACHINE / 1 task / 2 signals");
await expect(page.getByTestId("studio-path-results")).toContainText("Are mirrors folded?");
await expect(page.getByTestId("studio-path-results")).toContainText("Yes");
await expect(page.getByTestId("studio-path-signal-timeline")).toContainText("machine_start_signal / MACHINE");
await expect(page.getByTestId("studio-path-signal-timeline")).toContainText('"bill_machine_wash":true');
await page.getByTestId("studio-path-outcome-focus").click();
+50
View File
@@ -44,6 +44,12 @@ async function selectVehicleType(page, vehicleTypeId = 2) {
await expect(page.getByTestId("self-serve-vehicle-selector-description")).toContainText(/Truck|Van|Car/);
}
async function advanceGuidedWashToLastStep(page) {
for (let index = 0; index < 5; index += 1) {
await page.getByTestId("self-serve-guided-next").click();
}
}
function waitForLaneCommandRequest(page, command) {
return page.waitForRequest((request) => {
if (!request.url().includes("/modules/self-serve/lane/command")) {
@@ -196,6 +202,9 @@ test.describe("Self-serve wash", () => {
await page.reload({ waitUntil: "domcontentloaded" });
await expect(page.getByTestId("self-serve-live-elapsed")).toBeVisible({ timeout: 10_000 });
await advanceGuidedWashToLastStep(page);
await expect(page.getByTestId("self-serve-guided-next")).toBeHidden();
await expect(page.getByTestId("self-serve-nav-complete")).toBeVisible();
await page.getByTestId("self-serve-nav-complete").click();
await expect(page.getByTestId("self-serve-completed-step")).toBeVisible();
await page.getByTestId("self-serve-nav-close").click();
@@ -260,8 +269,49 @@ test.describe("Self-serve wash", () => {
await page.goto("/user/wash/start");
await expect(page.getByTestId("self-serve-live-elapsed")).toBeVisible({ timeout: 10_000 });
await expect(page.getByTestId("self-serve-department-name")).toContainText("Odense");
await expect(page.getByTestId("self-serve-nav-help")).toContainText("Har du brug for hjælp?");
expect(requests.commands).toHaveLength(0);
const bottomActions = page.getByTestId("self-serve-bottom-actions");
const guidedActions = bottomActions.getByTestId("self-serve-guided-bottom-actions");
const sessionActions = bottomActions.getByTestId("self-serve-session-bottom-actions");
const gateActions = bottomActions.getByTestId("self-serve-property-gate-actions");
await expect(bottomActions).toBeVisible();
await expect
.poll(async () =>
bottomActions.evaluate((element) =>
Array.from(element.children).map((child) => child.getAttribute("data-testid"))
)
)
.toEqual([
"self-serve-session-bottom-actions",
"self-serve-property-gate-actions",
"self-serve-guided-bottom-actions",
]);
await expect(guidedActions.getByTestId("self-serve-guided-prev")).toBeVisible();
await expect(guidedActions.getByTestId("self-serve-guided-next")).toBeVisible();
await expect(sessionActions.getByTestId("self-serve-nav-help")).toContainText("Har du brug for hjælp?");
await expect(sessionActions.getByTestId("self-serve-nav-complete")).toBeHidden();
await expect(guidedActions.getByTestId("self-serve-nav-complete")).toBeHidden();
await expect(gateActions).toBeVisible();
await expect(gateActions.getByTestId("self-serve-nav-open-property-access-gate")).toBeVisible();
await expect(gateActions.getByTestId("self-serve-nav-open-property-exit-gate")).toBeVisible();
await advanceGuidedWashToLastStep(page);
await expect(page.getByText("6 / 6")).toBeVisible();
await expect(guidedActions.getByTestId("self-serve-guided-next")).toBeHidden();
await expect(guidedActions.getByTestId("self-serve-nav-complete")).toBeVisible();
if ((page.viewportSize()?.width || 0) <= 768) {
await expect(bottomActions).toHaveCSS("position", "fixed");
const footerBox = await page.locator(".fixed-bottom-footer").boundingBox();
const actionsBox = await bottomActions.boundingBox();
expect(footerBox).not.toBeNull();
expect(actionsBox).not.toBeNull();
expect(actionsBox.y + actionsBox.height).toBeLessThanOrEqual(footerBox.y + 2);
}
const accessCommandRequestPromise = waitForLaneCommandRequest(page, "OPEN_PROPERTY_ACCESS_GATE");
await page.getByTestId("self-serve-nav-open-property-access-gate").click();
const accessCommandRequest = await accessCommandRequestPromise;
+43 -2
View File
@@ -688,11 +688,52 @@ describe("MyWashStart", () => {
await flushPromises();
await wrapper.get('[data-testid="self-serve-nav-open-property-access-gate"]').trigger("click");
await wrapper.get('[data-testid="self-serve-nav-open-property-exit-gate"]').trigger("click");
const bottomActions = wrapper.get('[data-testid="self-serve-bottom-actions"]');
const guidedActions = bottomActions.get('[data-testid="self-serve-guided-bottom-actions"]');
const sessionActions = bottomActions.get('[data-testid="self-serve-session-bottom-actions"]');
const gateActions = bottomActions.get('[data-testid="self-serve-property-gate-actions"]');
const navigationActions = wrapper.get(".buttons");
const guidedPreviousButton = guidedActions.get('[data-testid="self-serve-guided-prev"]');
const guidedNextButton = guidedActions.get('[data-testid="self-serve-guided-next"]');
const helpButton = sessionActions.get('[data-testid="self-serve-nav-help"]');
const accessGateButton = gateActions.get('[data-testid="self-serve-nav-open-property-access-gate"]');
const exitGateButton = gateActions.get('[data-testid="self-serve-nav-open-property-exit-gate"]');
expect(bottomActions.classes()).toContain("self-serve-bottom-actions");
expect(bottomActions.element.children[0]).toBe(sessionActions.element);
expect(bottomActions.element.children[1]).toBe(gateActions.element);
expect(bottomActions.element.children[2]).toBe(guidedActions.element);
expect(guidedPreviousButton.classes()).toContain("self-serve-bottom-actions__button");
expect(guidedNextButton.classes()).toContain("self-serve-bottom-actions__button");
expect(helpButton.classes()).toContain("self-serve-bottom-actions__button");
expect(sessionActions.find('[data-testid="self-serve-nav-complete"]').exists()).toBe(false);
expect(guidedActions.find('[data-testid="self-serve-nav-complete"]').exists()).toBe(false);
expect(accessGateButton.classes()).toContain("self-serve-bottom-actions__button");
expect(exitGateButton.classes()).toContain("self-serve-bottom-actions__button");
expect(navigationActions.find('[data-testid="self-serve-guided-prev"]').exists()).toBe(false);
expect(navigationActions.find('[data-testid="self-serve-guided-next"]').exists()).toBe(false);
expect(navigationActions.find('[data-testid="self-serve-nav-help"]').exists()).toBe(false);
expect(navigationActions.find('[data-testid="self-serve-nav-complete"]').exists()).toBe(false);
expect(navigationActions.find('[data-testid="self-serve-nav-open-property-access-gate"]').exists()).toBe(false);
expect(navigationActions.find('[data-testid="self-serve-nav-open-property-exit-gate"]').exists()).toBe(false);
await accessGateButton.trigger("click");
await exitGateButton.trigger("click");
expect(mocks.openPropertyAccessGate).toHaveBeenCalledWith(7);
expect(mocks.openPropertyExitGate).toHaveBeenCalledWith(7);
for (let index = 0; index < 5; index += 1) {
await guidedActions.get('[data-testid="self-serve-guided-next"]').trigger("click");
}
expect(guidedActions.find('[data-testid="self-serve-guided-next"]').exists()).toBe(false);
const completeButton = guidedActions.get('[data-testid="self-serve-nav-complete"]');
expect(completeButton.classes()).toContain("self-serve-bottom-actions__button");
await completeButton.trigger("click");
expect(mocks.onStopWash).toHaveBeenCalledWith(7);
});
it("restores an active wash from the server when local progress is missing", async () => {
@@ -0,0 +1,41 @@
import { describe, expect, it } from "vitest";
import { guidedWashFlowSteps } from "@/constants/guidedWashFlowSteps";
import da from "@/i18n/locales/da.json";
describe("Danish self-serve labels", () => {
it("uses Danish gate and assistance text", () => {
expect(da.self_wash.open_property_access_gate).toBe("Åbn adgangsport");
expect(da.self_wash.open_property_exit_gate).toBe("Åbn udgangsport");
expect(da.self_wash.assistance).toBe("Har du brug for hjælp?");
});
it("uses Danish guided wash step text", () => {
expect(guidedWashFlowSteps.map((step) => step.title)).toEqual([
"Højtryksforvask",
"Sæbepåføring",
"Børstevask",
"Højtryksskylning",
"Vokspåføring",
"Spot Free-skylning",
]);
expect(guidedWashFlowSteps[0]).toMatchObject({
content: "Spul med højtryk for at løsne skidt.",
brushes: [
{
label: "Sort slange",
description: "til højtryksforvask.",
warning: "Min. afstand til bil: 20 cm.",
},
],
});
expect(guidedWashFlowSteps[5]).toMatchObject({
content: "Skyl hurtigt førerhuset med Spot Free for at undgå pletter.",
brushes: [
{
label: "Hvid slange",
description: "til Spot Free-skylning.",
},
],
});
});
});
@@ -43,6 +43,24 @@ describe("SelfServe guided/completed components", () => {
expect(wrapper.emitted("update:currentStep")).toEqual([[1]]);
});
it("can hide guided step actions when the parent owns bottom controls", () => {
const wrapper = mountWithApp(SelfServeGuidedInstructions, {
props: {
currentStep: 0,
showActions: false,
steps: [
{ title: "Foam", content: "Apply foam", brushes: [] },
{ title: "Rinse", content: "Rinse off", brushes: [] },
],
},
messages,
});
expect(wrapper.find('[data-testid="self-serve-guided-prev"]').exists()).toBe(false);
expect(wrapper.find('[data-testid="self-serve-guided-next"]').exists()).toBe(false);
expect(wrapper.get('[data-testid="self-serve-guided-step-0"]').text()).toContain("Apply foam");
});
it("formats the completed duration through the completion message", () => {
const wrapper = mountWithApp(SelfServeCompletedStep, {
props: {