diff --git a/src/components/displays/selfServe/SelfServeGuidedInstructions.vue b/src/components/displays/selfServe/SelfServeGuidedInstructions.vue index e783a9f5..1c242f26 100644 --- a/src/components/displays/selfServe/SelfServeGuidedInstructions.vue +++ b/src/components/displays/selfServe/SelfServeGuidedInstructions.vue @@ -2,10 +2,13 @@ import { computed } from "vue"; import { BButton, BField } from "buefy"; -const props = defineProps<{ +const props = withDefaults(defineProps<{ steps: Array; currentStep: number; -}>(); + showActions?: boolean; +}>(), { + showActions: true, +}); const emit = defineEmits<{ (e: "update:currentStep", value: number): void; @@ -67,7 +70,7 @@ const goNext = () => { -
+
{ }); const currentPathOutcomesRequestKey = computed(() => JSON.stringify(pathOutcomesRequestPayload.value)); const pathOutcomeList = computed(() => (Array.isArray(pathOutcomes.value?.outcomes) ? pathOutcomes.value.outcomes : [])); +const pathResultList = computed(() => (Array.isArray(pathOutcomes.value?.paths) ? pathOutcomes.value.paths : [])); const selectedPathOutcome = computed(() => ( pathOutcomeList.value.find((outcome) => outcome.id === selectedPathOutcomeId.value) || pathOutcomeList.value[0] || null )); +const selectedPathResultList = computed(() => { + if (!selectedPathOutcome.value) { + return pathResultList.value; + } + + const matches = pathResultList.value.filter((path) => ( + path.summary === selectedPathOutcome.value.summary + && Boolean(path.allowed) === Boolean(selectedPathOutcome.value.allowed) + )); + + return matches.length > 0 ? matches : pathResultList.value; +}); const pathOutcomesSummary = computed(() => pathOutcomes.value?.summary || {}); const pathOutcomesScope = computed(() => pathOutcomes.value?.scope || null); const pathOutcomesWarnings = computed(() => (Array.isArray(pathOutcomes.value?.warnings) ? pathOutcomes.value.warnings : [])); @@ -599,10 +612,26 @@ const pathOutcomeSignalLabel = (signal = {}) => [ signal.predicted_status, ].filter(Boolean).join(" / "); +const pathResultLabel = (path = {}) => ( + path.summary || `${path.allowed ? "Allowed" : "Blocked"} / ${pathOutcomeServicesLabel(path.services)}` +); + +const pathResultMetricsLabel = (path = {}) => { + const taskCount = Number(path.task_count ?? (Array.isArray(path.tasks) ? path.tasks.length : 0)); + const signalCount = Number(path.signal_count ?? (Array.isArray(path.signals) ? path.signals.length : 0)); + return `${taskCount} task${taskCount === 1 ? "" : "s"} / ${signalCount} signal${signalCount === 1 ? "" : "s"}`; +}; + const pathOutcomeAnswerLabel = (answer = {}) => ( answer.answer_label || (answer.answer === true ? "Yes" : answer.answer === false ? "No" : "Unset") ); +const pathResultKey = (path = {}, index = 0) => ( + path.id || `${index}-${(Array.isArray(path.answers) ? path.answers : []) + .map((answer) => `${answer.question_id}:${pathOutcomeAnswerLabel(answer)}`) + .join("|")}` +); + const selectPathOutcome = (outcome) => { selectedPathOutcomeId.value = outcome?.id || null; }; @@ -625,7 +654,7 @@ const loadPathOutcomes = async () => { pathOutcomesError.value = ""; try { const payload = await requestPost("/department/selfserve/studio/path-outcomes", pathOutcomesRequestPayload.value); - pathOutcomes.value = payload || { outcomes: [], summary: {}, warnings: [], truncated: false }; + pathOutcomes.value = payload || { outcomes: [], paths: [], summary: {}, warnings: [], truncated: false }; pathOutcomesRequestKey.value = currentPathOutcomesRequestKey.value; selectedPathOutcomeId.value = pathOutcomeList.value[0]?.id || null; } catch (error) { @@ -3262,6 +3291,7 @@ onBeforeUnmount(() => {
{{ pathOutcomesSummary.outcome_count || 0 }}Grouped outcomes
{{ pathOutcomesSummary.terminal_path_count || 0 }}Terminal paths
+
{{ pathOutcomesSummary.path_sample_count || pathResultList.length || 0 }}Paths shown
{{ pathOutcomesSummary.question_count || 0 }}Questions
{{ pathOutcomesSummary.state_count || 0 }}States explored
@@ -3351,6 +3381,35 @@ onBeforeUnmount(() => {
+
+
Paths and Results
+
+
+
+ + + + + {{ pathResultLabel(path) }} + {{ pathOutcomeScopeLabel(path.scope) }} / {{ pathResultMetricsLabel(path) }} + +
+
    +
  1. + {{ answer.question }} + {{ pathOutcomeAnswerLabel(answer) }} +
  2. +
+

No answers required for this path.

+
+
+
+
Predicted Signals
@@ -5171,7 +5230,7 @@ onBeforeUnmount(() => { .studio-paths-summary { display: grid; gap: 10px; - grid-template-columns: repeat(4, minmax(0, 1fr)); + grid-template-columns: repeat(5, minmax(0, 1fr)); } .studio-paths-summary > div { @@ -5214,6 +5273,7 @@ onBeforeUnmount(() => { .studio-paths-detail, .studio-paths-section, .studio-path-chain-list, +.studio-path-result-list, .studio-path-task-list, .studio-signal-timeline { display: flex; @@ -5259,7 +5319,8 @@ onBeforeUnmount(() => { .studio-path-outcome-row small, .studio-path-task small, -.studio-path-chain small { +.studio-path-chain small, +.studio-path-result header small { color: #64748b; font-size: 11px; } @@ -5297,31 +5358,55 @@ onBeforeUnmount(() => { } .studio-path-chain-list, +.studio-path-result-list, .studio-path-task-list, .studio-signal-timeline { gap: 8px; } -.studio-path-chain { +.studio-path-chain, +.studio-path-result { background: #f8fafc; border: 1px solid #e2e8f0; border-radius: 7px; padding: 10px; } -.studio-path-chain ol { +.studio-path-result-list { + max-height: 360px; + overflow: auto; + padding-right: 2px; +} + +.studio-path-result header { + align-items: flex-start; + display: grid; + gap: 10px; + grid-template-columns: 24px minmax(0, 1fr); +} + +.studio-path-result header strong, +.studio-path-result header small { + display: block; + overflow-wrap: anywhere; +} + +.studio-path-chain ol, +.studio-path-result ol { display: flex; flex-direction: column; gap: 6px; margin: 8px 0 0 18px; } -.studio-path-chain li { +.studio-path-chain li, +.studio-path-result li { color: #334155; font-size: 12px; } -.studio-path-chain li strong { +.studio-path-chain li strong, +.studio-path-result li strong { color: #0f172a; margin-left: 6px; } diff --git a/src/views/dashboards/userDashboard/wash/MyWashStart.vue b/src/views/dashboards/userDashboard/wash/MyWashStart.vue index d5838ff4..98cdc924 100644 --- a/src/views/dashboards/userDashboard/wash/MyWashStart.vue +++ b/src/views/dashboards/userDashboard/wash/MyWashStart.vue @@ -253,6 +253,25 @@ const { steps, clickableSteps, isNextButtonDisabled, handleConfirmNext, targetSt currentStep.value = steps.VEHICLE; +const isLastGuidedWashStep = computed(() => currentGuidedWashStep.value >= guidedWashFlowSteps.length - 1); + +const goPreviousGuidedWashStep = () => { + currentGuidedWashStep.value = Math.max(currentGuidedWashStep.value - 1, 0); +}; + +const goNextGuidedWashStep = () => { + if (isLastGuidedWashStep.value) { + return; + } + + currentGuidedWashStep.value = Math.min(currentGuidedWashStep.value + 1, guidedWashFlowSteps.length - 1); +}; + +const completeGuidedWash = () => { + void onStopWash(washLaneId.value); + currentStep.value = steps.COMPLETED; +}; + const unregisterBeforeUnload = ref void)>(null); const registrationOptions = computed(() => ( @@ -958,16 +977,16 @@ watch(() => forceNearestDepartmentEvaluationId.value, () => {
- + + + +
+
+ {{ $t("self_wash.assistance") }} + +
+ +
+ forceNearestDepartmentEvaluationId.value, () => { forceNearestDepartmentEvaluationId.value, () => { > {{ $t("self_wash.open_property_exit_gate") }} +
+
- {{ $t("self_wash.assistance") }} + {{ $t("common.previous") }} + {{ $t("common.next") }} + + + {{ $t("common.done") }}
- - +
+ {{ $t("self_wash.loading_data") }} @@ -1246,7 +1305,59 @@ watch(() => forceNearestDepartmentEvaluationId.value, () => { padding: 0.45rem 0 0; } +.self-serve-bottom-actions { + display: flex; + flex-direction: column; + gap: 0.75rem; + justify-content: center; + margin: 0.75rem auto 0; + max-width: 30rem; +} + +.self-serve-bottom-actions__row { + display: flex; + gap: 0.75rem; + justify-content: center; + width: 100%; +} + +.self-serve-bottom-actions__button { + flex: 1 1 0; + font-weight: 700; + line-height: 1.15; + max-width: 14rem; + min-height: 2.75rem; + min-width: 0; + white-space: normal; +} + @media screen and (max-width: 768px) { + .self-serve-flow-steps { + padding-bottom: 14rem; + } + + .self-serve-bottom-actions { + background: #ffffff; + border-top: 1px solid #dfe5f0; + bottom: calc(4.75rem + env(safe-area-inset-bottom, 0px)); + box-shadow: 0 -0.25rem 0.75rem rgba(17, 47, 95, 0.08); + left: 0; + margin: 0; + max-width: none; + padding: 0.5rem 0.75rem; + position: fixed; + right: 0; + z-index: 41; + } + + .self-serve-bottom-actions__button { + font-size: 0.88rem; + max-width: none; + min-height: 2.65rem; + padding-left: 0.55rem; + padding-right: 0.55rem; + } + .self-serve-flow-steps :deep(.steps.mobile-compact .step-items .step-item .step-marker) { height: 1.7rem; width: 1.7rem; diff --git a/tests/e2e/self-serve-studio-flow.spec.js b/tests/e2e/self-serve-studio-flow.spec.js index ed2c7737..41f1fc25 100644 --- a/tests/e2e/self-serve-studio-flow.spec.js +++ b/tests/e2e/self-serve-studio-flow.spec.js @@ -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(); diff --git a/tests/e2e/self-serve-wash.spec.js b/tests/e2e/self-serve-wash.spec.js index 5e878b48..350a695f 100644 --- a/tests/e2e/self-serve-wash.spec.js +++ b/tests/e2e/self-serve-wash.spec.js @@ -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; diff --git a/tests/unit/my-wash-start.spec.js b/tests/unit/my-wash-start.spec.js index adc6b6f3..25afc22b 100644 --- a/tests/unit/my-wash-start.spec.js +++ b/tests/unit/my-wash-start.spec.js @@ -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 () => { diff --git a/tests/unit/self-serve-danish-labels.spec.js b/tests/unit/self-serve-danish-labels.spec.js new file mode 100644 index 00000000..14704bdd --- /dev/null +++ b/tests/unit/self-serve-danish-labels.spec.js @@ -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.", + }, + ], + }); + }); +}); diff --git a/tests/unit/self-serve-guided-and-completed.spec.js b/tests/unit/self-serve-guided-and-completed.spec.js index c743529d..56b34d0f 100644 --- a/tests/unit/self-serve-guided-and-completed.spec.js +++ b/tests/unit/self-serve-guided-and-completed.spec.js @@ -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: {