2436 lines
80 KiB
JavaScript
2436 lines
80 KiB
JavaScript
// @vitest-environment jsdom
|
|
import { flushPromises } from "@vue/test-utils";
|
|
import { nextTick } from "vue";
|
|
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
|
|
const mocks = vi.hoisted(() => {
|
|
const asMockRef = (value) => ({ value, __v_isRef: true });
|
|
|
|
const nearestDepartment = {
|
|
value: {
|
|
id: 6,
|
|
name: "Roskilde",
|
|
address: "Industrivej 45, 4000 Roskilde",
|
|
self_serve_enabled: true,
|
|
lanes: [{ id: 7, name: "7", status: "AVAILABLE", products: [2], machine_available: true }],
|
|
},
|
|
};
|
|
const guestDepartments = { value: [nearestDepartment.value] };
|
|
const doesCurrentDepartmentSelectionHaveSelfServeEnabled = asMockRef(true);
|
|
const answers = { value: {} };
|
|
const completedTasks = { value: {} };
|
|
const visibleQuestions = { value: [{ id: 11, question: "Question 11" }] };
|
|
const activeTasks = { value: [] };
|
|
const conditions = { value: [] };
|
|
const rules = { value: [] };
|
|
const allowedServices = { value: ["MACHINE"] };
|
|
const selfServeAllowed = { value: false };
|
|
const selfServeLane = { value: { id: 7 } };
|
|
const selfServeSession = { value: null };
|
|
const resolvedVehicleTypeId = { value: null };
|
|
const selectedGrantCustomerNumber = asMockRef(null);
|
|
const sessionSubuserId = asMockRef(null);
|
|
|
|
return {
|
|
nearestDepartment,
|
|
guestDepartments,
|
|
doesCurrentDepartmentSelectionHaveSelfServeEnabled,
|
|
answers,
|
|
completedTasks,
|
|
visibleQuestions,
|
|
activeTasks,
|
|
conditions,
|
|
rules,
|
|
allowedServices,
|
|
selfServeAllowed,
|
|
selfServeLane,
|
|
selfServeSession,
|
|
resolvedVehicleTypeId,
|
|
fetchDepartments: vi.fn(async () => guestDepartments.value),
|
|
startAutoRefresh: vi.fn(),
|
|
stopAutoRefresh: vi.fn(),
|
|
startDepartmentSearch: vi.fn(),
|
|
selectDepartment: vi.fn(),
|
|
clearForcedDepartment: vi.fn(),
|
|
evaluateLocationDepartments: vi.fn(),
|
|
fetchSelfServeDataInternal: vi.fn(),
|
|
fetchWashSummary: vi.fn(),
|
|
syncVehicleAnswer: vi.fn(),
|
|
updateLaneAllowedServices: vi.fn(),
|
|
enableMachineRelay: vi.fn(),
|
|
downloadAttachment: vi.fn(),
|
|
answerQuestion: vi.fn((questionId, value) => {
|
|
answers.value = { ...answers.value, [questionId]: value };
|
|
}),
|
|
restoredProgressPayload: null,
|
|
saveProgress: vi.fn(),
|
|
clearProgress: vi.fn(),
|
|
restoreProgress: vi.fn(),
|
|
startElapsedTimer: vi.fn(),
|
|
stopElapsedTimer: vi.fn(),
|
|
registerBeforeUnload: vi.fn(() => vi.fn()),
|
|
markDestroying: vi.fn(),
|
|
handleConfirmNext: vi.fn(),
|
|
isNextButtonDisabled: vi.fn(() => false),
|
|
onStartWash: vi.fn(),
|
|
onStopWash: vi.fn(),
|
|
openPropertyAccessGate: vi.fn(),
|
|
openPropertyExitGate: vi.fn(),
|
|
setShowFooterInContent: vi.fn(),
|
|
fetchCustomerVehicles: vi.fn(),
|
|
fetchVehicleTypeOptions: vi.fn(),
|
|
addVehicle: vi.fn(),
|
|
sessionRequest: vi.fn(),
|
|
sessionCustomerNumber: null,
|
|
selectedGrantCustomerNumber,
|
|
sessionSubuserId,
|
|
washSessionActionOptions: null,
|
|
};
|
|
});
|
|
|
|
const stoppingWashStorageKey = "mywash_stopping_v1";
|
|
|
|
vi.mock("@/components/session/token/SessionUser.vue", async () => {
|
|
const { ref } = await import("vue");
|
|
mocks.sessionCustomerNumber = ref(12345679);
|
|
|
|
return {
|
|
SessionUser: {
|
|
user: {
|
|
customer_number: mocks.sessionCustomerNumber,
|
|
},
|
|
subuser: {
|
|
id: mocks.sessionSubuserId,
|
|
selectedGrantCustomerNumber: mocks.selectedGrantCustomerNumber,
|
|
},
|
|
canAccessSuperUser: () => true,
|
|
canAccessDeveloper: () => false,
|
|
request: vi.fn(async (...args) => {
|
|
const response = await mocks.sessionRequest(...args);
|
|
return response ?? { status: 200, data: { data: {} } };
|
|
}),
|
|
functions: {
|
|
contact: {
|
|
onClickCallPhoneNumber: vi.fn(),
|
|
},
|
|
},
|
|
objects: {
|
|
vehicles: {
|
|
add: vi.fn(async (...args) => {
|
|
mocks.addVehicle(...args);
|
|
return { status: 200, data: { data: {} } };
|
|
}),
|
|
get: {
|
|
all: vi.fn(async () => {
|
|
mocks.fetchCustomerVehicles();
|
|
return [{ reg: "AB12345", type: 2 }];
|
|
}),
|
|
},
|
|
columns: {
|
|
type: {
|
|
options: vi.fn(async () => {
|
|
mocks.fetchVehicleTypeOptions();
|
|
return [{ id: 2, name: "Truck", product: { id: 2, description: "Truck" } }];
|
|
}),
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
};
|
|
});
|
|
|
|
vi.mock("@/components/displays/department/pos/steps/mobile/objects/PosDepartmentStepMobileFlow.vue", () => ({
|
|
locations: {
|
|
location: {
|
|
value: {
|
|
coords: {
|
|
latitude: 55.6,
|
|
longitude: 12.5,
|
|
},
|
|
},
|
|
},
|
|
hasValidCoordinatePair: (coordinates) => {
|
|
const latitude = Number(coordinates?.latitude);
|
|
const longitude = Number(coordinates?.longitude);
|
|
return Number.isFinite(latitude) && Number.isFinite(longitude);
|
|
},
|
|
},
|
|
}));
|
|
|
|
vi.mock("@/components/viewport/conditions/ViewPortFooterOptions.vue", () => ({
|
|
setShowFooterInContent: mocks.setShowFooterInContent,
|
|
}));
|
|
|
|
vi.mock("@/composables/useWashDepartments", () => ({
|
|
useWashDepartments: () => ({
|
|
guestDepartments: mocks.guestDepartments,
|
|
nearestDepartment: mocks.nearestDepartment,
|
|
isForcingNearestDepartment: { value: false },
|
|
forceNearestDepartmentEvaluationId: { value: 0 },
|
|
isSearchingDepartments: { value: false },
|
|
departmentFetchError: { value: null },
|
|
isDepartmentSelectionFallbackBased: { value: false },
|
|
availableProductIds: { value: [2] },
|
|
doesCurrentDepartmentSelectionHaveSelfServeEnabled: mocks.doesCurrentDepartmentSelectionHaveSelfServeEnabled,
|
|
fetchDepartments: mocks.fetchDepartments,
|
|
evaluateLocationDepartments: mocks.evaluateLocationDepartments,
|
|
startDepartmentSearch: mocks.startDepartmentSearch,
|
|
selectDepartment: mocks.selectDepartment,
|
|
clearForcedDepartment: mocks.clearForcedDepartment,
|
|
startAutoRefresh: mocks.startAutoRefresh,
|
|
stopAutoRefresh: mocks.stopAutoRefresh,
|
|
}),
|
|
}));
|
|
|
|
vi.mock("@/composables/useSelfServeLogic", async () => {
|
|
const { ref } = await import("vue");
|
|
mocks.resolvedVehicleTypeId = ref(mocks.resolvedVehicleTypeId.value);
|
|
|
|
return {
|
|
useSelfServeLogic: () => ({
|
|
loading: { value: false },
|
|
error: { value: null },
|
|
lane: mocks.selfServeLane,
|
|
questions: { value: [] },
|
|
conditions: mocks.conditions,
|
|
rules: mocks.rules,
|
|
tasks: { value: [] },
|
|
answers: mocks.answers,
|
|
completedTasks: mocks.completedTasks,
|
|
allowedServices: mocks.allowedServices,
|
|
resolvedVehicleTypeId: mocks.resolvedVehicleTypeId,
|
|
allowed: mocks.selfServeAllowed,
|
|
session: mocks.selfServeSession,
|
|
visibleQuestions: mocks.visibleQuestions,
|
|
activeTasks: mocks.activeTasks,
|
|
allVisibleQuestionsAnswered: { value: false },
|
|
machineAvailable: { value: true },
|
|
fetchSelfServeData: mocks.fetchSelfServeDataInternal,
|
|
fetchWashSummary: mocks.fetchWashSummary,
|
|
syncVehicleAnswer: mocks.syncVehicleAnswer,
|
|
evaluateRule: () => true,
|
|
evaluateCondition: () => true,
|
|
isQuestionVisible: () => true,
|
|
isServiceAllowed: () => true,
|
|
updateLaneAllowedServices: mocks.updateLaneAllowedServices,
|
|
enableMachineRelay: mocks.enableMachineRelay,
|
|
downloadAttachment: mocks.downloadAttachment,
|
|
answerQuestion: mocks.answerQuestion,
|
|
}),
|
|
};
|
|
});
|
|
|
|
vi.mock("@/composables/useWashProgress", () => ({
|
|
useWashProgress: (options = {}) => ({
|
|
completedDurationMs: { value: null },
|
|
now: { value: Date.now() },
|
|
isRestoring: { value: false },
|
|
formattedElapsed: { value: "00:00" },
|
|
saveProgress: mocks.saveProgress,
|
|
clearProgress: mocks.clearProgress,
|
|
restoreProgress: vi.fn(() => {
|
|
mocks.restoreProgress();
|
|
|
|
if (mocks.restoredProgressPayload && typeof options.applyRestoredState === "function") {
|
|
options.applyRestoredState(mocks.restoredProgressPayload);
|
|
}
|
|
|
|
return mocks.restoredProgressPayload || null;
|
|
}),
|
|
startElapsedTimer: mocks.startElapsedTimer,
|
|
stopElapsedTimer: mocks.stopElapsedTimer,
|
|
registerBeforeUnload: mocks.registerBeforeUnload,
|
|
markDestroying: mocks.markDestroying,
|
|
}),
|
|
}));
|
|
|
|
vi.mock("@/composables/useWashFlowState", () => ({
|
|
useWashFlowState: () => ({
|
|
steps: {
|
|
VEHICLE: 0,
|
|
QUESTIONS: 1,
|
|
SELECT_LANE: 2,
|
|
TASKS: 3,
|
|
WASH_IN_PROGRESS: 4,
|
|
COMPLETED: 5,
|
|
},
|
|
clickableSteps: {
|
|
0: () => true,
|
|
1: () => true,
|
|
2: () => true,
|
|
3: () => true,
|
|
4: () => false,
|
|
},
|
|
isNextButtonDisabled: mocks.isNextButtonDisabled,
|
|
handleConfirmNext: mocks.handleConfirmNext,
|
|
targetStepForStart: () => 3,
|
|
}),
|
|
}));
|
|
|
|
vi.mock("@/composables/useWashSessionActions", async () => {
|
|
const { ref } = await import("vue");
|
|
|
|
return {
|
|
useWashSessionActions: (options = {}) => {
|
|
mocks.washSessionActionOptions = options;
|
|
|
|
return {
|
|
dynamicImageUrl: ref("https://cdn.example.test/dynamic.png"),
|
|
onStartWash: mocks.onStartWash,
|
|
onStopWash: mocks.onStopWash,
|
|
openPropertyAccessGate: mocks.openPropertyAccessGate,
|
|
openPropertyExitGate: mocks.openPropertyExitGate,
|
|
openingPropertyAccessGate: ref(false),
|
|
openingPropertyExitGate: ref(false),
|
|
isStartingWash: ref(false),
|
|
};
|
|
},
|
|
};
|
|
});
|
|
|
|
import { SessionUser } from "@/components/session/token/SessionUser.vue";
|
|
import MyWashStart from "@/views/dashboards/userDashboard/wash/MyWashStart.vue";
|
|
import { mountWithApp } from "./helpers/mountWithApp.js";
|
|
|
|
const BStepsStub = {
|
|
template: `
|
|
<div class="b-steps-stub">
|
|
<slot />
|
|
<slot
|
|
name="navigation"
|
|
:previous="{ disabled: false, action: () => {} }"
|
|
:next="{ disabled: false, action: () => {} }"
|
|
/>
|
|
</div>
|
|
`,
|
|
};
|
|
|
|
const BStepItemStub = {
|
|
template: "<div class='b-step-item-stub'><slot /></div>",
|
|
};
|
|
|
|
const BButtonStub = {
|
|
emits: ["click"],
|
|
template: "<button v-bind='$attrs' @click=\"$emit('click', $event)\"><slot /></button>",
|
|
};
|
|
|
|
const stubComponents = {
|
|
PosDepartmentStepMobile1Location: {
|
|
template: "<div data-testid='location-stub' />",
|
|
},
|
|
SelfServeDepartmentHeader: {
|
|
template: "<div data-testid='department-header-stub' />",
|
|
},
|
|
SelfServeVehicleStep: {
|
|
emits: ["update:registration-number", "update:customer-number", "select-vehicle-type"],
|
|
template: `
|
|
<div data-testid="vehicle-step-stub">
|
|
<button data-testid="emit-registration" @click="$emit('update:registration-number', 'AB12345')">registration</button>
|
|
<button data-testid="emit-registration-unknown" @click="$emit('update:registration-number', 'ZZ99999')">registration-unknown</button>
|
|
<button data-testid="emit-customer-number" @click="$emit('update:customer-number', '12345679')">customer</button>
|
|
<button data-testid="emit-vehicle-type" @click="$emit('select-vehicle-type', { id: 2, name: 'Truck' })">vehicle</button>
|
|
</div>
|
|
`,
|
|
},
|
|
VehicleInputSection: {
|
|
emits: ["update:registration-number", "update:customer-number", "select-vehicle-type"],
|
|
template: `
|
|
<div data-testid="vehicle-step-stub">
|
|
<button data-testid="emit-registration" @click="$emit('update:registration-number', 'AB12345')">registration</button>
|
|
<button data-testid="emit-registration-unknown" @click="$emit('update:registration-number', 'ZZ99999')">registration-unknown</button>
|
|
<button data-testid="emit-customer-number" @click="$emit('update:customer-number', '12345679')">customer</button>
|
|
<button data-testid="emit-vehicle-type" @click="$emit('select-vehicle-type', { id: 2, name: 'Truck' })">vehicle</button>
|
|
</div>
|
|
`,
|
|
},
|
|
SelfServeQuestionsStep: {
|
|
emits: ["answer-question"],
|
|
template: `
|
|
<div>
|
|
<button data-testid="emit-answer" @click="$emit('answer-question', 11, true)">answer</button>
|
|
<button data-testid="emit-answer-21" @click="$emit('answer-question', 21, false)">answer 21</button>
|
|
</div>
|
|
`,
|
|
},
|
|
SelfServeLaneStep: {
|
|
props: ["isLaneAvailable"],
|
|
emits: ["update:selected-lane-id"],
|
|
template: `
|
|
<div>
|
|
<span data-testid="lane-7-available">{{ String(isLaneAvailable({ id: 7, status: 'OCCUPIED', selfserve_enabled: true })) }}</span>
|
|
<button data-testid="emit-lane" @click="$emit('update:selected-lane-id', 7)">lane</button>
|
|
</div>
|
|
`,
|
|
},
|
|
LaneSelectionSection: {
|
|
props: ["isLaneAvailable"],
|
|
emits: ["update:selected-lane-id"],
|
|
template: `
|
|
<div>
|
|
<span data-testid="lane-7-available">{{ String(isLaneAvailable({ id: 7, status: 'OCCUPIED', selfserve_enabled: true })) }}</span>
|
|
<button data-testid="emit-lane" @click="$emit('update:selected-lane-id', 7)">lane</button>
|
|
</div>
|
|
`,
|
|
},
|
|
WashTypeSelector: {
|
|
emits: ["update:model-value"],
|
|
template: `
|
|
<div>
|
|
<button data-testid="emit-machine-wash" @click="$emit('update:model-value', 'Machine')">machine wash</button>
|
|
</div>
|
|
`,
|
|
},
|
|
SelfServeTasksStep: {
|
|
props: ["activeTasks", "dynamicImageUrl"],
|
|
template: `
|
|
<div data-testid="tasks-step-stub">
|
|
<img v-if="dynamicImageUrl" data-testid="tasks-dynamic-image-stub" :src="dynamicImageUrl" alt="" />
|
|
<span
|
|
v-for="task in activeTasks"
|
|
:key="task.id"
|
|
:data-testid="\`rendered-task-\${task.id}\`"
|
|
>
|
|
{{ task.task }}
|
|
</span>
|
|
</div>
|
|
`,
|
|
},
|
|
SelfServeGuidedInstructions: {
|
|
props: ["currentStep"],
|
|
template: "<div data-testid='guided-step-stub'>{{ currentStep }}</div>",
|
|
},
|
|
SelfServeCompletedStep: {
|
|
template: "<div data-testid='completed-step-stub' />",
|
|
},
|
|
ErrorBanner: {
|
|
props: ["message", "type", "iconLeft", "loading", "showRetry", "retryTestId"],
|
|
emits: ["retry"],
|
|
template: `
|
|
<div v-if="message && (message.value === undefined || message.value)" data-testid="error-banner-stub">
|
|
<span>{{ message && message.value !== undefined ? message.value : message }}</span>
|
|
<button v-if="showRetry" :data-testid="retryTestId || 'error-banner-retry'" @click="$emit('retry')">retry</button>
|
|
</div>
|
|
`,
|
|
},
|
|
WashProgressCard: {
|
|
props: [
|
|
"currentGuidedWashStep",
|
|
"guidedWashFlowSteps",
|
|
"formattedElapsed",
|
|
"isCompletingWash",
|
|
"openingPropertyAccessGate",
|
|
"openingPropertyExitGate",
|
|
"showProgressActions",
|
|
],
|
|
emits: [
|
|
"update:currentGuidedWashStep",
|
|
"goPreviousGuidedWashStep",
|
|
"goNextGuidedWashStep",
|
|
"completeWash",
|
|
"openPropertyAccessGate",
|
|
"openPropertyExitGate",
|
|
"requestAssistance",
|
|
],
|
|
computed: {
|
|
elapsedText() {
|
|
return this.formattedElapsed && this.formattedElapsed.value !== undefined
|
|
? this.formattedElapsed.value
|
|
: this.formattedElapsed;
|
|
},
|
|
normalizedCurrentGuidedWashStep() {
|
|
return Number(
|
|
this.currentGuidedWashStep && this.currentGuidedWashStep.value !== undefined
|
|
? this.currentGuidedWashStep.value
|
|
: this.currentGuidedWashStep || 0
|
|
);
|
|
},
|
|
normalizedGuidedWashFlowSteps() {
|
|
return this.guidedWashFlowSteps && this.guidedWashFlowSteps.value !== undefined
|
|
? this.guidedWashFlowSteps.value
|
|
: this.guidedWashFlowSteps || [];
|
|
},
|
|
isFinishingWash() {
|
|
return !!(
|
|
this.isCompletingWash &&
|
|
(this.isCompletingWash.value !== undefined ? this.isCompletingWash.value : this.isCompletingWash)
|
|
);
|
|
},
|
|
isLastGuidedWashStep() {
|
|
return (
|
|
Array.isArray(this.normalizedGuidedWashFlowSteps) &&
|
|
this.normalizedGuidedWashFlowSteps.length > 0 &&
|
|
this.normalizedCurrentGuidedWashStep >= this.normalizedGuidedWashFlowSteps.length - 1
|
|
);
|
|
},
|
|
shouldShowProgressActions() {
|
|
return this.showProgressActions && this.showProgressActions.value !== undefined
|
|
? this.showProgressActions.value
|
|
: this.showProgressActions !== false;
|
|
},
|
|
isOpeningPropertyAccessGate() {
|
|
return !!(
|
|
this.openingPropertyAccessGate &&
|
|
(this.openingPropertyAccessGate.value !== undefined
|
|
? this.openingPropertyAccessGate.value
|
|
: this.openingPropertyAccessGate)
|
|
);
|
|
},
|
|
isOpeningPropertyExitGate() {
|
|
return !!(
|
|
this.openingPropertyExitGate &&
|
|
(this.openingPropertyExitGate.value !== undefined
|
|
? this.openingPropertyExitGate.value
|
|
: this.openingPropertyExitGate)
|
|
);
|
|
},
|
|
},
|
|
template: `
|
|
<div data-testid="self-serve-wash-progress">
|
|
<span data-testid="wash-progress-elapsed">
|
|
{{ elapsedText }}
|
|
</span>
|
|
<div
|
|
v-if="isFinishingWash && shouldShowProgressActions"
|
|
class="notification is-info is-light self-serve-finishing-screen"
|
|
data-testid="self-serve-finishing-wash"
|
|
>
|
|
<p class="title is-5 mb-0">{{ $t("self_wash.finishing_wash_exit_opening") }}</p>
|
|
</div>
|
|
<template v-if="!isFinishingWash">
|
|
<slot
|
|
v-if="shouldShowProgressActions"
|
|
name="guided-instructions"
|
|
:current-step="normalizedCurrentGuidedWashStep"
|
|
:steps="normalizedGuidedWashFlowSteps"
|
|
:is-last-step="isLastGuidedWashStep"
|
|
/>
|
|
<div
|
|
v-show="shouldShowProgressActions"
|
|
class="self-serve-bottom-actions"
|
|
data-testid="self-serve-bottom-actions"
|
|
>
|
|
<div class="self-serve-bottom-actions__row" data-testid="self-serve-session-bottom-actions">
|
|
<button
|
|
class="self-serve-bottom-actions__button"
|
|
data-testid="self-serve-nav-help"
|
|
@click.prevent="$emit('requestAssistance')"
|
|
>
|
|
self_wash.assistance
|
|
</button>
|
|
</div>
|
|
<div class="self-serve-bottom-actions__row" data-testid="self-serve-property-gate-actions">
|
|
<button
|
|
class="self-serve-bottom-actions__button"
|
|
data-testid="self-serve-nav-open-property-access-gate"
|
|
:disabled="isOpeningPropertyAccessGate"
|
|
@click.prevent="$emit('openPropertyAccessGate')"
|
|
>
|
|
self_wash.open_property_access_gate
|
|
</button>
|
|
<button
|
|
class="self-serve-bottom-actions__button"
|
|
data-testid="self-serve-nav-open-property-exit-gate"
|
|
:disabled="isOpeningPropertyExitGate"
|
|
@click.prevent="$emit('openPropertyExitGate')"
|
|
>
|
|
self_wash.open_property_exit_gate
|
|
</button>
|
|
</div>
|
|
<div class="self-serve-bottom-actions__row" data-testid="self-serve-guided-bottom-actions">
|
|
<button
|
|
class="self-serve-bottom-actions__button"
|
|
data-testid="self-serve-guided-prev"
|
|
:disabled="normalizedCurrentGuidedWashStep === 0"
|
|
@click.prevent="$emit('goPreviousGuidedWashStep')"
|
|
>
|
|
common.previous
|
|
</button>
|
|
<button
|
|
v-if="!isLastGuidedWashStep"
|
|
class="self-serve-bottom-actions__button"
|
|
data-testid="self-serve-guided-next"
|
|
@click.prevent="$emit('goNextGuidedWashStep')"
|
|
>
|
|
common.next
|
|
</button>
|
|
<button
|
|
v-else
|
|
class="self-serve-bottom-actions__button"
|
|
data-testid="self-serve-nav-complete"
|
|
:disabled="isFinishingWash"
|
|
@click.prevent="$emit('completeWash')"
|
|
>
|
|
common.done
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
</div>
|
|
`,
|
|
},
|
|
BSteps: BStepsStub,
|
|
BStepItem: BStepItemStub,
|
|
BButton: BButtonStub,
|
|
BMessage: {
|
|
template: "<div><slot /></div>",
|
|
},
|
|
};
|
|
|
|
describe("MyWashStart", () => {
|
|
let consoleWarnSpy;
|
|
|
|
afterEach(() => {
|
|
consoleWarnSpy?.mockRestore();
|
|
vi.clearAllTimers();
|
|
vi.useRealTimers();
|
|
localStorage.clear();
|
|
});
|
|
|
|
beforeEach(() => {
|
|
consoleWarnSpy = vi.spyOn(console, "warn").mockImplementation((...args) => {
|
|
const warning = args.map((entry) => String(entry)).join(" ");
|
|
if (warning.includes("[Vue warn]")) {
|
|
throw new Error(warning);
|
|
}
|
|
});
|
|
|
|
localStorage.clear();
|
|
mocks.nearestDepartment.value = {
|
|
id: 6,
|
|
name: "Roskilde",
|
|
address: "Industrivej 45, 4000 Roskilde",
|
|
self_serve_enabled: true,
|
|
lanes: [{ id: 7, name: "7", status: "AVAILABLE", products: [2], machine_available: true }],
|
|
};
|
|
mocks.guestDepartments.value = [mocks.nearestDepartment.value];
|
|
mocks.doesCurrentDepartmentSelectionHaveSelfServeEnabled.value = true;
|
|
mocks.answers.value = {};
|
|
mocks.completedTasks.value = {};
|
|
mocks.visibleQuestions.value = [{ id: 11, question: "Question 11" }];
|
|
mocks.activeTasks.value = [];
|
|
mocks.allowedServices.value = ["MACHINE"];
|
|
mocks.selfServeAllowed.value = false;
|
|
mocks.selfServeLane.value = { id: 7 };
|
|
mocks.selfServeSession.value = null;
|
|
mocks.resolvedVehicleTypeId.value = null;
|
|
mocks.conditions.value = [];
|
|
mocks.rules.value = [];
|
|
mocks.sessionCustomerNumber.value = 12345679;
|
|
mocks.selectedGrantCustomerNumber.value = null;
|
|
mocks.sessionSubuserId.value = null;
|
|
mocks.fetchDepartments.mockClear();
|
|
mocks.startAutoRefresh.mockClear();
|
|
mocks.stopAutoRefresh.mockClear();
|
|
mocks.restoreProgress.mockClear();
|
|
mocks.registerBeforeUnload.mockClear();
|
|
mocks.markDestroying.mockClear();
|
|
mocks.fetchCustomerVehicles.mockClear();
|
|
mocks.fetchVehicleTypeOptions.mockClear();
|
|
mocks.fetchSelfServeDataInternal.mockReset();
|
|
mocks.fetchSelfServeDataInternal.mockResolvedValue(undefined);
|
|
mocks.fetchWashSummary.mockClear();
|
|
mocks.saveProgress.mockClear();
|
|
mocks.clearProgress.mockClear();
|
|
mocks.startElapsedTimer.mockClear();
|
|
mocks.stopElapsedTimer.mockClear();
|
|
mocks.restoredProgressPayload = null;
|
|
mocks.syncVehicleAnswer.mockClear();
|
|
mocks.answerQuestion.mockClear();
|
|
mocks.setShowFooterInContent.mockClear();
|
|
mocks.addVehicle.mockClear();
|
|
mocks.sessionRequest.mockClear();
|
|
mocks.sessionRequest.mockResolvedValue(undefined);
|
|
SessionUser.request.mockClear();
|
|
mocks.handleConfirmNext.mockClear();
|
|
mocks.isNextButtonDisabled.mockClear();
|
|
mocks.isNextButtonDisabled.mockReturnValue(false);
|
|
mocks.onStartWash.mockClear();
|
|
mocks.onStopWash.mockReset();
|
|
mocks.onStopWash.mockResolvedValue(true);
|
|
mocks.openPropertyAccessGate.mockClear();
|
|
mocks.openPropertyExitGate.mockClear();
|
|
mocks.washSessionActionOptions = null;
|
|
});
|
|
|
|
it("loads initial wash context and tears down refresh state on unmount", async () => {
|
|
const wrapper = mountWithApp(MyWashStart, {
|
|
global: {
|
|
stubs: stubComponents,
|
|
},
|
|
});
|
|
|
|
await flushPromises();
|
|
|
|
expect(mocks.setShowFooterInContent).toHaveBeenCalledWith(false);
|
|
expect(mocks.fetchDepartments).toHaveBeenCalledTimes(1);
|
|
expect(mocks.startAutoRefresh).toHaveBeenCalledTimes(1);
|
|
expect(mocks.fetchCustomerVehicles).toHaveBeenCalledTimes(1);
|
|
expect(mocks.fetchVehicleTypeOptions).toHaveBeenCalledTimes(1);
|
|
expect(mocks.restoreProgress).toHaveBeenCalledTimes(1);
|
|
expect(mocks.registerBeforeUnload).toHaveBeenCalledTimes(1);
|
|
|
|
wrapper.unmount();
|
|
|
|
expect(mocks.stopAutoRefresh).toHaveBeenCalledTimes(1);
|
|
expect(mocks.markDestroying).toHaveBeenCalledTimes(1);
|
|
expect(mocks.setShowFooterInContent).toHaveBeenLastCalledWith(true);
|
|
});
|
|
|
|
it("wires child updates back into the self-serve runtime", async () => {
|
|
vi.useFakeTimers();
|
|
|
|
const wrapper = mountWithApp(MyWashStart, {
|
|
global: {
|
|
stubs: stubComponents,
|
|
},
|
|
});
|
|
|
|
await flushPromises();
|
|
await wrapper.get('[data-testid="emit-lane"]').trigger("click");
|
|
await wrapper.get('[data-testid="emit-registration"]').trigger("click");
|
|
await wrapper.get('[data-testid="emit-vehicle-type"]').trigger("click");
|
|
await nextTick();
|
|
await vi.advanceTimersByTimeAsync(300);
|
|
await flushPromises();
|
|
|
|
expect(mocks.fetchSelfServeDataInternal).toHaveBeenCalledWith(6, 2, 7, "AB12345");
|
|
|
|
await wrapper.get('[data-testid="emit-answer"]').trigger("click");
|
|
await flushPromises();
|
|
|
|
expect(mocks.answerQuestion).toHaveBeenCalledWith(11, true);
|
|
expect(mocks.syncVehicleAnswer).toHaveBeenCalledWith({
|
|
departmentId: 6,
|
|
laneId: 7,
|
|
customerNumber: 12345679,
|
|
reg: "AB12345",
|
|
questionId: 11,
|
|
value: true,
|
|
vehicleTypeId: 2,
|
|
});
|
|
});
|
|
|
|
it("debounces registration, lane, and vehicle type updates into one self-serve fetch", async () => {
|
|
vi.useFakeTimers();
|
|
|
|
const wrapper = mountWithApp(MyWashStart, {
|
|
global: {
|
|
stubs: stubComponents,
|
|
},
|
|
});
|
|
|
|
await flushPromises();
|
|
mocks.fetchSelfServeDataInternal.mockClear();
|
|
|
|
await wrapper.get('[data-testid="emit-registration"]').trigger("click");
|
|
await wrapper.get('[data-testid="emit-lane"]').trigger("click");
|
|
await wrapper.get('[data-testid="emit-vehicle-type"]').trigger("click");
|
|
await nextTick();
|
|
|
|
await vi.advanceTimersByTimeAsync(299);
|
|
await flushPromises();
|
|
expect(mocks.fetchSelfServeDataInternal).not.toHaveBeenCalled();
|
|
|
|
await vi.advanceTimersByTimeAsync(1);
|
|
await flushPromises();
|
|
|
|
expect(mocks.fetchSelfServeDataInternal).toHaveBeenCalledTimes(1);
|
|
expect(mocks.fetchSelfServeDataInternal).toHaveBeenCalledWith(6, 2, 7, "AB12345");
|
|
});
|
|
|
|
it("adopts the server-resolved vehicle type after an untyped eligibility lookup", async () => {
|
|
vi.useFakeTimers();
|
|
mocks.fetchSelfServeDataInternal.mockImplementation(async (_departmentId, vehicleTypeId) => {
|
|
if (!vehicleTypeId) {
|
|
mocks.resolvedVehicleTypeId.value = 2;
|
|
}
|
|
return { vehicle_type_id: 2 };
|
|
});
|
|
|
|
const wrapper = mountWithApp(MyWashStart, {
|
|
global: {
|
|
stubs: stubComponents,
|
|
},
|
|
});
|
|
|
|
await flushPromises();
|
|
mocks.fetchSelfServeDataInternal.mockClear();
|
|
|
|
await wrapper.get('[data-testid="emit-registration-unknown"]').trigger("click");
|
|
await nextTick();
|
|
await vi.advanceTimersByTimeAsync(300);
|
|
await flushPromises();
|
|
|
|
expect(mocks.fetchSelfServeDataInternal).toHaveBeenCalledWith(6, null, 7, "ZZ99999");
|
|
|
|
await nextTick();
|
|
await vi.advanceTimersByTimeAsync(300);
|
|
await flushPromises();
|
|
|
|
expect(mocks.fetchSelfServeDataInternal).toHaveBeenCalledWith(6, 2, 7, "ZZ99999");
|
|
});
|
|
|
|
it("treats the current allowed occupied lane as selectable", async () => {
|
|
mocks.nearestDepartment.value = {
|
|
...mocks.nearestDepartment.value,
|
|
lanes: [{ id: 7, name: "7", status: "OCCUPIED", products: [2], machine_available: true }],
|
|
};
|
|
mocks.guestDepartments.value = [mocks.nearestDepartment.value];
|
|
mocks.selfServeLane.value = { id: 7, status: "OCCUPIED", selfserve_enabled: true };
|
|
mocks.selfServeAllowed.value = true;
|
|
|
|
const wrapper = mountWithApp(MyWashStart, {
|
|
global: {
|
|
stubs: stubComponents,
|
|
},
|
|
});
|
|
|
|
await flushPromises();
|
|
|
|
expect(wrapper.get('[data-testid="lane-7-available"]').text()).toBe("true");
|
|
});
|
|
|
|
it("uses the allowed session lane when the lane payload is not populated yet", async () => {
|
|
mocks.nearestDepartment.value = {
|
|
...mocks.nearestDepartment.value,
|
|
lanes: [{ id: 7, name: "7", status: "OCCUPIED", products: [2], machine_available: true }],
|
|
};
|
|
mocks.guestDepartments.value = [mocks.nearestDepartment.value];
|
|
mocks.selfServeLane.value = null;
|
|
mocks.selfServeAllowed.value = false;
|
|
mocks.selfServeSession.value = { lane_id: 7, allowed: true };
|
|
|
|
const wrapper = mountWithApp(MyWashStart, {
|
|
global: {
|
|
stubs: stubComponents,
|
|
},
|
|
});
|
|
|
|
await flushPromises();
|
|
|
|
expect(wrapper.get('[data-testid="lane-7-available"]').text()).toBe("true");
|
|
});
|
|
|
|
it("hides machine tasks while manual wash is selected", async () => {
|
|
mocks.activeTasks.value = [
|
|
{ id: 31, task: "Machine checklist", services: ["MACHINE"] },
|
|
{ id: 32, task: "Manual bay prep", services: ["GATE"] },
|
|
{ id: 33, task: "General instruction", services: [] },
|
|
];
|
|
|
|
const wrapper = mountWithApp(MyWashStart, {
|
|
global: {
|
|
stubs: stubComponents,
|
|
},
|
|
});
|
|
|
|
await flushPromises();
|
|
|
|
expect(wrapper.find('[data-testid="rendered-task-31"]').exists()).toBe(false);
|
|
expect(wrapper.get('[data-testid="rendered-task-32"]').text()).toContain("Manual bay prep");
|
|
expect(wrapper.get('[data-testid="rendered-task-33"]').text()).toContain("General instruction");
|
|
expect(wrapper.find('[data-testid="tasks-dynamic-image-stub"]').exists()).toBe(false);
|
|
});
|
|
|
|
it("shows machine tasks after machine wash is selected", async () => {
|
|
mocks.activeTasks.value = [
|
|
{ id: 31, task: "Machine checklist", services: ["MACHINE"] },
|
|
{ id: 32, task: "Manual bay prep", services: ["GATE"] },
|
|
];
|
|
|
|
const wrapper = mountWithApp(MyWashStart, {
|
|
global: {
|
|
stubs: stubComponents,
|
|
},
|
|
});
|
|
|
|
await flushPromises();
|
|
expect(wrapper.find('[data-testid="rendered-task-31"]').exists()).toBe(false);
|
|
|
|
await wrapper.get('[data-testid="emit-lane"]').trigger("click");
|
|
await wrapper.get('[data-testid="emit-machine-wash"]').trigger("click");
|
|
await nextTick();
|
|
|
|
expect(wrapper.get('[data-testid="rendered-task-31"]').text()).toContain("Machine checklist");
|
|
expect(wrapper.get('[data-testid="rendered-task-32"]').text()).toContain("Manual bay prep");
|
|
expect(wrapper.find('[data-testid="tasks-dynamic-image-stub"]').exists()).toBe(true);
|
|
});
|
|
|
|
it("keeps machine tasks visible after start when a summary refresh lacks allowed services", async () => {
|
|
mocks.allowedServices.value = [];
|
|
mocks.activeTasks.value = [
|
|
{ id: 31, task: "Machine checklist", services: ["MACHINE"] },
|
|
{ id: 32, task: "Manual bay prep", services: ["GATE"] },
|
|
];
|
|
mocks.restoredProgressPayload = {
|
|
washInProgress: true,
|
|
washLaneId: 7,
|
|
washStartTime: Date.now() - 20_000,
|
|
licensePlateInput: "AB12345",
|
|
vehicleTypeSelect: 2,
|
|
radioWashType: "Machine",
|
|
radioLaneOption: 7,
|
|
customerNumberInput: 12345679,
|
|
isForcingNearestDepartment: false,
|
|
forceNearestDepartmentEvaluationId: 0,
|
|
answers: { 11: true },
|
|
completedTasks: {},
|
|
currentStep: 3,
|
|
};
|
|
|
|
const wrapper = mountWithApp(MyWashStart, {
|
|
global: {
|
|
stubs: stubComponents,
|
|
},
|
|
});
|
|
|
|
await flushPromises();
|
|
|
|
expect(wrapper.get('[data-testid="rendered-task-31"]').text()).toContain("Machine checklist");
|
|
expect(wrapper.get('[data-testid="rendered-task-32"]').text()).toContain("Manual bay prep");
|
|
expect(wrapper.find('[data-testid="tasks-dynamic-image-stub"]').exists()).toBe(true);
|
|
});
|
|
|
|
it("syncs only non-machine task ids to the backend when manual wash is selected", async () => {
|
|
mocks.activeTasks.value = [
|
|
{ id: 31, task: "Machine checklist", services: ["MACHINE"] },
|
|
{ id: 32, task: "Manual bay prep", services: ["GATE"] },
|
|
{ id: 33, task: "General instruction", services: [] },
|
|
];
|
|
|
|
mountWithApp(MyWashStart, {
|
|
global: {
|
|
stubs: stubComponents,
|
|
},
|
|
});
|
|
|
|
await flushPromises();
|
|
await mocks.washSessionActionOptions.updateLaneAllowedServices(7);
|
|
|
|
expect(mocks.updateLaneAllowedServices).toHaveBeenCalledWith(7, { taskIds: [32, 33] });
|
|
});
|
|
|
|
it("syncs machine task ids to the backend when machine wash is selected", async () => {
|
|
mocks.activeTasks.value = [
|
|
{ id: 31, task: "Machine checklist", services: ["MACHINE"] },
|
|
{ id: 32, task: "Manual bay prep", services: ["GATE"] },
|
|
];
|
|
|
|
const wrapper = mountWithApp(MyWashStart, {
|
|
global: {
|
|
stubs: stubComponents,
|
|
},
|
|
});
|
|
|
|
await flushPromises();
|
|
await wrapper.get('[data-testid="emit-lane"]').trigger("click");
|
|
await wrapper.get('[data-testid="emit-machine-wash"]').trigger("click");
|
|
await nextTick();
|
|
|
|
await mocks.washSessionActionOptions.updateLaneAllowedServices(7);
|
|
|
|
expect(mocks.updateLaneAllowedServices).toHaveBeenCalledWith(7, { taskIds: [31, 32] });
|
|
});
|
|
|
|
it("accepts another question answer while the previous answer sync is still pending", async () => {
|
|
let resolveFirstSync;
|
|
mocks.syncVehicleAnswer.mockImplementationOnce(
|
|
() =>
|
|
new Promise((resolve) => {
|
|
resolveFirstSync = resolve;
|
|
})
|
|
);
|
|
|
|
const wrapper = mountWithApp(MyWashStart, {
|
|
global: {
|
|
stubs: stubComponents,
|
|
},
|
|
});
|
|
|
|
await flushPromises();
|
|
await wrapper.get('[data-testid="emit-lane"]').trigger("click");
|
|
await wrapper.get('[data-testid="emit-registration"]').trigger("click");
|
|
await wrapper.get('[data-testid="emit-vehicle-type"]').trigger("click");
|
|
await nextTick();
|
|
|
|
await wrapper.get('[data-testid="emit-answer"]').trigger("click");
|
|
await nextTick();
|
|
await wrapper.get('[data-testid="emit-answer-21"]').trigger("click");
|
|
await flushPromises();
|
|
|
|
expect(mocks.answerQuestion).toHaveBeenCalledWith(11, true);
|
|
expect(mocks.answerQuestion).toHaveBeenCalledWith(21, false);
|
|
expect(mocks.syncVehicleAnswer).toHaveBeenCalledTimes(2);
|
|
expect(mocks.syncVehicleAnswer).toHaveBeenNthCalledWith(2, {
|
|
departmentId: 6,
|
|
laneId: 7,
|
|
customerNumber: 12345679,
|
|
reg: "AB12345",
|
|
questionId: 21,
|
|
value: false,
|
|
vehicleTypeId: 2,
|
|
});
|
|
|
|
resolveFirstSync?.();
|
|
await flushPromises();
|
|
});
|
|
|
|
it("surfaces failed question answer syncs and blocks question confirmation until retry succeeds", async () => {
|
|
mocks.syncVehicleAnswer.mockRejectedValueOnce(new Error("Question sync failed")).mockResolvedValueOnce(undefined);
|
|
|
|
const wrapper = mountWithApp(MyWashStart, {
|
|
global: {
|
|
stubs: stubComponents,
|
|
},
|
|
});
|
|
|
|
await flushPromises();
|
|
await wrapper.get('[data-testid="emit-lane"]').trigger("click");
|
|
await wrapper.get('[data-testid="emit-registration"]').trigger("click");
|
|
await wrapper.get('[data-testid="emit-vehicle-type"]').trigger("click");
|
|
await nextTick();
|
|
await wrapper.get('[data-testid="self-serve-nav-next"]').trigger("click");
|
|
await flushPromises();
|
|
|
|
await wrapper.get('[data-testid="emit-answer"]').trigger("click");
|
|
await flushPromises();
|
|
|
|
expect(wrapper.get('[data-testid="self-serve-question-sync-error"]').text()).toContain("Question sync failed");
|
|
|
|
const confirmButton = wrapper.get('[data-testid="self-serve-nav-confirm"]');
|
|
expect(confirmButton.attributes("disabled")).toBeDefined();
|
|
|
|
await confirmButton.trigger("click");
|
|
expect(mocks.handleConfirmNext).not.toHaveBeenCalled();
|
|
|
|
await wrapper.get('[data-testid="self-serve-question-sync-retry"]').trigger("click");
|
|
await flushPromises();
|
|
|
|
expect(mocks.syncVehicleAnswer).toHaveBeenCalledTimes(2);
|
|
expect(mocks.syncVehicleAnswer).toHaveBeenNthCalledWith(2, {
|
|
departmentId: 6,
|
|
laneId: 7,
|
|
customerNumber: 12345679,
|
|
reg: "AB12345",
|
|
questionId: 11,
|
|
value: true,
|
|
vehicleTypeId: 2,
|
|
});
|
|
expect(wrapper.find('[data-testid="self-serve-question-sync-error"]').exists()).toBe(false);
|
|
expect(wrapper.get('[data-testid="self-serve-nav-confirm"]').attributes("disabled")).toBeUndefined();
|
|
});
|
|
|
|
it("shows disabled warning without showing loading state when department self-serve is unavailable", async () => {
|
|
mocks.nearestDepartment.value = {
|
|
...mocks.nearestDepartment.value,
|
|
self_serve_enabled: false,
|
|
};
|
|
mocks.guestDepartments.value = [mocks.nearestDepartment.value];
|
|
mocks.doesCurrentDepartmentSelectionHaveSelfServeEnabled.value = false;
|
|
|
|
const wrapper = mountWithApp(MyWashStart, {
|
|
global: {
|
|
stubs: stubComponents,
|
|
},
|
|
});
|
|
|
|
await flushPromises();
|
|
|
|
expect(wrapper.find('[data-testid="self-serve-disabled-warning"]').exists()).toBe(true);
|
|
expect(wrapper.text()).not.toContain("self_wash.loading_data");
|
|
});
|
|
|
|
it("keeps the completed step visible when the department becomes unavailable after completion", async () => {
|
|
mocks.doesCurrentDepartmentSelectionHaveSelfServeEnabled.value = false;
|
|
mocks.restoredProgressPayload = {
|
|
washInProgress: false,
|
|
washLaneId: null,
|
|
washStartTime: null,
|
|
licensePlateInput: "AB12345",
|
|
vehicleTypeSelect: 2,
|
|
radioWashType: "Manual",
|
|
radioLaneOption: 7,
|
|
customerNumberInput: 12345679,
|
|
isForcingNearestDepartment: false,
|
|
forceNearestDepartmentEvaluationId: 0,
|
|
answers: {},
|
|
completedTasks: {},
|
|
currentStep: 5,
|
|
};
|
|
|
|
const wrapper = mountWithApp(MyWashStart, {
|
|
global: {
|
|
stubs: stubComponents,
|
|
},
|
|
});
|
|
|
|
await flushPromises();
|
|
|
|
expect(wrapper.find('[data-testid="self-serve-disabled-warning"]').exists()).toBe(false);
|
|
expect(wrapper.find('[data-testid="completed-step-stub"]').exists()).toBe(true);
|
|
});
|
|
|
|
it("shows a recently completed wash before startup refreshes finish", async () => {
|
|
localStorage.setItem(
|
|
"mywash_recent_completed_v1",
|
|
JSON.stringify({
|
|
laneId: 7,
|
|
reg: "AB12345",
|
|
customerNumber: 12345679,
|
|
completedAt: Date.now(),
|
|
})
|
|
);
|
|
mocks.fetchDepartments.mockReturnValueOnce(new Promise(() => {}));
|
|
|
|
const wrapper = mountWithApp(MyWashStart, {
|
|
global: {
|
|
stubs: stubComponents,
|
|
},
|
|
});
|
|
|
|
await nextTick();
|
|
|
|
expect(mocks.fetchDepartments).toHaveBeenCalledTimes(1);
|
|
expect(wrapper.find('[data-testid="self-serve-disabled-warning"]').exists()).toBe(false);
|
|
expect(wrapper.find('[data-testid="completed-step-stub"]').exists()).toBe(true);
|
|
|
|
wrapper.unmount();
|
|
});
|
|
|
|
it("picks up a recently completed wash marker written just after mount", async () => {
|
|
vi.useFakeTimers();
|
|
mocks.fetchDepartments.mockReturnValueOnce(new Promise(() => {}));
|
|
|
|
const wrapper = mountWithApp(MyWashStart, {
|
|
global: {
|
|
stubs: stubComponents,
|
|
},
|
|
});
|
|
|
|
await nextTick();
|
|
|
|
expect(wrapper.find('[data-testid="completed-step-stub"]').isVisible()).toBe(false);
|
|
|
|
localStorage.setItem(
|
|
"mywash_recent_completed_v1",
|
|
JSON.stringify({
|
|
laneId: 7,
|
|
reg: "AB12345",
|
|
customerNumber: 12345679,
|
|
completedAt: Date.now(),
|
|
})
|
|
);
|
|
await vi.advanceTimersByTimeAsync(250);
|
|
await nextTick();
|
|
|
|
expect(wrapper.find('[data-testid="self-serve-disabled-warning"]').exists()).toBe(false);
|
|
expect(wrapper.find('[data-testid="completed-step-stub"]').exists()).toBe(true);
|
|
|
|
wrapper.unmount();
|
|
});
|
|
|
|
it("restores a recently completed wash marker written after startup refreshes finish", async () => {
|
|
vi.useFakeTimers();
|
|
|
|
const wrapper = mountWithApp(MyWashStart, {
|
|
global: {
|
|
stubs: stubComponents,
|
|
},
|
|
});
|
|
|
|
await flushPromises();
|
|
await vi.advanceTimersByTimeAsync(5500);
|
|
await nextTick();
|
|
|
|
expect(wrapper.find('[data-testid="completed-step-stub"]').isVisible()).toBe(false);
|
|
|
|
localStorage.setItem(
|
|
"mywash_recent_completed_v1",
|
|
JSON.stringify({
|
|
laneId: 7,
|
|
reg: "AB12345",
|
|
customerNumber: 12345679,
|
|
completedAt: Date.now(),
|
|
})
|
|
);
|
|
await vi.advanceTimersByTimeAsync(1000);
|
|
await nextTick();
|
|
|
|
expect(wrapper.find('[data-testid="self-serve-disabled-warning"]').exists()).toBe(false);
|
|
expect(wrapper.find('[data-testid="completed-step-stub"]').exists()).toBe(true);
|
|
expect(wrapper.vm.currentStep).toBe(5);
|
|
expect(mocks.clearProgress).toHaveBeenCalled();
|
|
|
|
wrapper.unmount();
|
|
});
|
|
|
|
it("does not fetch self-serve eligibility after a recent completion marker is restored", async () => {
|
|
vi.useFakeTimers();
|
|
|
|
const wrapper = mountWithApp(MyWashStart, {
|
|
global: {
|
|
stubs: stubComponents,
|
|
},
|
|
});
|
|
|
|
await flushPromises();
|
|
mocks.fetchSelfServeDataInternal.mockClear();
|
|
|
|
localStorage.setItem(
|
|
"mywash_recent_completed_v1",
|
|
JSON.stringify({
|
|
laneId: 7,
|
|
reg: "AB12345",
|
|
customerNumber: 12345679,
|
|
completedAt: Date.now(),
|
|
})
|
|
);
|
|
await vi.advanceTimersByTimeAsync(1000);
|
|
await nextTick();
|
|
|
|
wrapper.vm.licensePlateInput = "CD98765";
|
|
wrapper.vm.vehicleTypeSelect = 2;
|
|
await nextTick();
|
|
await vi.advanceTimersByTimeAsync(400);
|
|
|
|
expect(wrapper.vm.currentStep).toBe(5);
|
|
expect(mocks.fetchSelfServeDataInternal).not.toHaveBeenCalled();
|
|
|
|
wrapper.unmount();
|
|
});
|
|
|
|
it("does not let a late lane refresh move a completed wash back to questions", async () => {
|
|
mocks.doesCurrentDepartmentSelectionHaveSelfServeEnabled.value = false;
|
|
mocks.restoredProgressPayload = {
|
|
washInProgress: true,
|
|
washLaneId: 7,
|
|
washStartTime: Date.now() - 20_000,
|
|
licensePlateInput: "AB12345",
|
|
vehicleTypeSelect: 2,
|
|
radioWashType: "Manual",
|
|
radioLaneOption: 7,
|
|
customerNumberInput: 12345679,
|
|
isForcingNearestDepartment: false,
|
|
forceNearestDepartmentEvaluationId: 0,
|
|
answers: {},
|
|
completedTasks: {},
|
|
currentStep: 4,
|
|
};
|
|
|
|
const pendingFetchResolvers = [];
|
|
mocks.fetchSelfServeDataInternal.mockImplementation(
|
|
() =>
|
|
new Promise((resolve) => {
|
|
pendingFetchResolvers.push(resolve);
|
|
})
|
|
);
|
|
|
|
const wrapper = mountWithApp(MyWashStart, {
|
|
global: {
|
|
stubs: stubComponents,
|
|
},
|
|
});
|
|
|
|
await flushPromises();
|
|
|
|
const guidedActions = wrapper
|
|
.get('[data-testid="self-serve-bottom-actions"]')
|
|
.get('[data-testid="self-serve-guided-bottom-actions"]');
|
|
|
|
for (let index = 0; index < 5; index += 1) {
|
|
await guidedActions.get('[data-testid="self-serve-guided-next"]').trigger("click");
|
|
}
|
|
|
|
await guidedActions.get('[data-testid="self-serve-nav-complete"]').trigger("click");
|
|
await new Promise((resolve) => setTimeout(resolve, 260));
|
|
await flushPromises();
|
|
|
|
expect(wrapper.find('[data-testid="completed-step-stub"]').exists()).toBe(true);
|
|
|
|
pendingFetchResolvers.forEach((resolve) => resolve(null));
|
|
await flushPromises();
|
|
await nextTick();
|
|
|
|
expect(wrapper.find('[data-testid="self-serve-disabled-warning"]').exists()).toBe(false);
|
|
expect(wrapper.find('[data-testid="completed-step-stub"]').exists()).toBe(true);
|
|
});
|
|
|
|
it("adds an unknown registration as customer vehicle before continuing from vehicle step", async () => {
|
|
const wrapper = mountWithApp(MyWashStart, {
|
|
global: {
|
|
stubs: stubComponents,
|
|
},
|
|
});
|
|
|
|
await flushPromises();
|
|
await wrapper.get('[data-testid="emit-lane"]').trigger("click");
|
|
await wrapper.get('[data-testid="emit-registration-unknown"]').trigger("click");
|
|
await wrapper.get('[data-testid="emit-vehicle-type"]').trigger("click");
|
|
await nextTick();
|
|
|
|
await wrapper.get('[data-testid="self-serve-nav-next"]').trigger("click");
|
|
await flushPromises();
|
|
|
|
expect(mocks.addVehicle).toHaveBeenCalledWith(2, "ZZ99999", false, 12345679);
|
|
expect(mocks.fetchCustomerVehicles.mock.calls.length).toBeGreaterThan(1);
|
|
expect(mocks.fetchSelfServeDataInternal).toHaveBeenCalledWith(6, 2, 7, "ZZ99999");
|
|
});
|
|
|
|
it("uses an effective lane when moving from vehicle to questions without manual lane selection", async () => {
|
|
const wrapper = mountWithApp(MyWashStart, {
|
|
global: {
|
|
stubs: stubComponents,
|
|
},
|
|
});
|
|
|
|
await flushPromises();
|
|
await wrapper.get('[data-testid="emit-registration"]').trigger("click");
|
|
await wrapper.get('[data-testid="emit-vehicle-type"]').trigger("click");
|
|
await nextTick();
|
|
|
|
await wrapper.get('[data-testid="self-serve-nav-next"]').trigger("click");
|
|
await flushPromises();
|
|
|
|
expect(mocks.fetchSelfServeDataInternal).toHaveBeenCalledWith(6, 2, 7, "AB12345");
|
|
});
|
|
|
|
it("falls back to available lane when restored lane is stale for the selected department", async () => {
|
|
vi.useFakeTimers();
|
|
|
|
mocks.restoredProgressPayload = {
|
|
washInProgress: false,
|
|
washLaneId: null,
|
|
washStartTime: null,
|
|
licensePlateInput: "AB12345",
|
|
vehicleTypeSelect: 2,
|
|
radioWashType: "Manual",
|
|
radioLaneOption: 999,
|
|
customerNumberInput: 12345679,
|
|
isForcingNearestDepartment: false,
|
|
forceNearestDepartmentEvaluationId: 0,
|
|
answers: {},
|
|
completedTasks: {},
|
|
currentStep: 1,
|
|
};
|
|
|
|
const wrapper = mountWithApp(MyWashStart, {
|
|
global: {
|
|
stubs: stubComponents,
|
|
},
|
|
});
|
|
|
|
await flushPromises();
|
|
await vi.advanceTimersByTimeAsync(300);
|
|
await flushPromises();
|
|
|
|
expect(mocks.fetchSelfServeDataInternal).toHaveBeenCalledWith(6, 2, 7, "AB12345");
|
|
|
|
wrapper.unmount();
|
|
});
|
|
|
|
it("overrides a restored customer number with the authenticated customer's number", async () => {
|
|
mocks.restoredProgressPayload = {
|
|
washInProgress: false,
|
|
washLaneId: null,
|
|
washStartTime: null,
|
|
licensePlateInput: "AB12345",
|
|
vehicleTypeSelect: 2,
|
|
radioWashType: "Manual",
|
|
radioLaneOption: 7,
|
|
customerNumberInput: 777,
|
|
isForcingNearestDepartment: false,
|
|
forceNearestDepartmentEvaluationId: 0,
|
|
answers: {},
|
|
completedTasks: {},
|
|
currentStep: 1,
|
|
};
|
|
|
|
const wrapper = mountWithApp(MyWashStart, {
|
|
global: {
|
|
stubs: stubComponents,
|
|
},
|
|
});
|
|
|
|
await flushPromises();
|
|
await wrapper.get('[data-testid="emit-answer"]').trigger("click");
|
|
await flushPromises();
|
|
|
|
expect(mocks.syncVehicleAnswer).toHaveBeenCalledWith({
|
|
departmentId: 6,
|
|
laneId: 7,
|
|
customerNumber: 12345679,
|
|
reg: "AB12345",
|
|
questionId: 11,
|
|
value: true,
|
|
vehicleTypeId: 2,
|
|
});
|
|
|
|
wrapper.unmount();
|
|
});
|
|
|
|
it("does not auto-start wash when only the first visible question is answered", async () => {
|
|
const wrapper = mountWithApp(MyWashStart, {
|
|
global: {
|
|
stubs: stubComponents,
|
|
},
|
|
});
|
|
|
|
await flushPromises();
|
|
await wrapper.get('[data-testid="emit-lane"]').trigger("click");
|
|
await wrapper.get('[data-testid="emit-registration"]').trigger("click");
|
|
await wrapper.get('[data-testid="emit-vehicle-type"]').trigger("click");
|
|
await nextTick();
|
|
await wrapper.get('[data-testid="self-serve-nav-next"]').trigger("click");
|
|
await flushPromises();
|
|
|
|
expect(wrapper.get('[data-testid="self-serve-nav-confirm"]').text()).toContain("common.next");
|
|
|
|
await wrapper.get('[data-testid="emit-answer"]').trigger("click");
|
|
await flushPromises();
|
|
|
|
expect(mocks.onStartWash).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it("skips directly to lane selection when no questions are visible after vehicle setup", async () => {
|
|
mocks.visibleQuestions.value = [];
|
|
|
|
const wrapper = mountWithApp(MyWashStart, {
|
|
global: {
|
|
stubs: stubComponents,
|
|
},
|
|
});
|
|
|
|
await flushPromises();
|
|
await wrapper.get('[data-testid="emit-registration"]').trigger("click");
|
|
await wrapper.get('[data-testid="emit-vehicle-type"]').trigger("click");
|
|
await nextTick();
|
|
|
|
await wrapper.get('[data-testid="self-serve-nav-next"]').trigger("click");
|
|
await flushPromises();
|
|
|
|
expect(mocks.fetchSelfServeDataInternal).toHaveBeenCalledWith(6, 2, 7, "AB12345");
|
|
expect(wrapper.get('[data-testid="self-serve-nav-confirm"]').text()).toContain("self_wash.confirm_and_start");
|
|
expect(mocks.onStartWash).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it("returns restored prewash progress to answered question review when questions exist", async () => {
|
|
mocks.answers.value = { 11: true };
|
|
mocks.restoredProgressPayload = {
|
|
washInProgress: false,
|
|
washLaneId: null,
|
|
washStartTime: null,
|
|
licensePlateInput: "AB12345",
|
|
vehicleTypeSelect: 2,
|
|
radioWashType: "Manual",
|
|
radioLaneOption: 7,
|
|
customerNumberInput: 12345679,
|
|
isForcingNearestDepartment: false,
|
|
forceNearestDepartmentEvaluationId: 0,
|
|
answers: { 11: true },
|
|
completedTasks: {},
|
|
currentStep: 2,
|
|
};
|
|
|
|
const wrapper = mountWithApp(MyWashStart, {
|
|
global: {
|
|
stubs: stubComponents,
|
|
},
|
|
});
|
|
|
|
await flushPromises();
|
|
await nextTick();
|
|
|
|
const prewashActions = wrapper.get('[data-testid="self-serve-prewash-bottom-actions"]');
|
|
expect(prewashActions.classes()).toContain("self-serve-prewash-actions--question-review");
|
|
expect(wrapper.get('[data-testid="self-serve-nav-confirm"]').text()).toContain("common.next");
|
|
expect(mocks.onStartWash).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it("places lane setup navigation in the pre-wash bottom action row", async () => {
|
|
mocks.visibleQuestions.value = [];
|
|
mocks.restoredProgressPayload = {
|
|
washInProgress: false,
|
|
washLaneId: null,
|
|
washStartTime: null,
|
|
licensePlateInput: "AB12345",
|
|
vehicleTypeSelect: 2,
|
|
radioWashType: "Manual",
|
|
radioLaneOption: 7,
|
|
customerNumberInput: 12345679,
|
|
isForcingNearestDepartment: false,
|
|
forceNearestDepartmentEvaluationId: 0,
|
|
answers: {},
|
|
completedTasks: {},
|
|
currentStep: 2,
|
|
};
|
|
|
|
const wrapper = mountWithApp(MyWashStart, {
|
|
global: {
|
|
stubs: stubComponents,
|
|
},
|
|
});
|
|
|
|
await flushPromises();
|
|
|
|
const prewashActions = wrapper.get('[data-testid="self-serve-prewash-bottom-actions"]');
|
|
const navigationRow = prewashActions.get('[data-testid="self-serve-prewash-navigation-actions"]');
|
|
const previousButton = navigationRow.get('[data-testid="self-serve-nav-previous"]');
|
|
const confirmButton = navigationRow.get('[data-testid="self-serve-nav-confirm"]');
|
|
const visibleActionIds = Array.from(navigationRow.element.children)
|
|
.filter((element) => !(element.getAttribute("style") || "").includes("display: none"))
|
|
.map((element) => element.getAttribute("data-testid"));
|
|
|
|
expect(prewashActions.classes()).toContain("self-serve-prewash-actions");
|
|
expect(navigationRow.classes()).toContain("self-serve-bottom-actions__row--prewash");
|
|
expect(previousButton.classes()).toContain("self-serve-bottom-actions__button");
|
|
expect(confirmButton.classes()).toContain("self-serve-bottom-actions__button");
|
|
expect(visibleActionIds).toEqual(["self-serve-nav-previous", "self-serve-nav-confirm"]);
|
|
});
|
|
|
|
it("does not restore a delayed server wash after the user edits start form input", async () => {
|
|
vi.useFakeTimers();
|
|
mocks.restoredProgressPayload = {
|
|
washInProgress: false,
|
|
washLaneId: null,
|
|
washStartTime: null,
|
|
licensePlateInput: "AB12345",
|
|
vehicleTypeSelect: 2,
|
|
radioWashType: "Manual",
|
|
radioLaneOption: 7,
|
|
customerNumberInput: 12345679,
|
|
isForcingNearestDepartment: false,
|
|
forceNearestDepartmentEvaluationId: 0,
|
|
answers: {},
|
|
completedTasks: {},
|
|
currentStep: 0,
|
|
};
|
|
|
|
const wrapper = mountWithApp(MyWashStart, {
|
|
global: {
|
|
stubs: stubComponents,
|
|
},
|
|
});
|
|
|
|
await flushPromises();
|
|
await wrapper.get('[data-testid="emit-registration-unknown"]').trigger("click");
|
|
await vi.advanceTimersByTimeAsync(1600);
|
|
await flushPromises();
|
|
|
|
expect(mocks.sessionRequest).not.toHaveBeenCalledWith(
|
|
"/modules/self-serve/lane/wash/in-progress",
|
|
"GET",
|
|
expect.anything()
|
|
);
|
|
|
|
wrapper.unmount();
|
|
});
|
|
|
|
it("does not call SessionUser.request for delayed server restore after unmount", async () => {
|
|
vi.useFakeTimers();
|
|
mocks.restoredProgressPayload = {
|
|
washInProgress: false,
|
|
washLaneId: null,
|
|
washStartTime: null,
|
|
licensePlateInput: "AB12345",
|
|
vehicleTypeSelect: 2,
|
|
radioWashType: "Manual",
|
|
radioLaneOption: 7,
|
|
customerNumberInput: 12345679,
|
|
isForcingNearestDepartment: false,
|
|
forceNearestDepartmentEvaluationId: 0,
|
|
answers: {},
|
|
completedTasks: {},
|
|
currentStep: 0,
|
|
};
|
|
|
|
const wrapper = mountWithApp(MyWashStart, {
|
|
global: {
|
|
stubs: stubComponents,
|
|
},
|
|
});
|
|
|
|
await flushPromises();
|
|
expect(SessionUser.request).not.toHaveBeenCalled();
|
|
|
|
wrapper.unmount();
|
|
await vi.advanceTimersByTimeAsync(1600);
|
|
await flushPromises();
|
|
|
|
expect(SessionUser.request).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it("retries server active wash restore when the authenticated customer number arrives after mount", async () => {
|
|
mocks.sessionCustomerNumber.value = null;
|
|
mocks.sessionRequest.mockImplementation(async (path, method, payload) => {
|
|
if (path === "/modules/self-serve/lane/wash/my-active-wash" && method === "GET") {
|
|
return {
|
|
data: {
|
|
data: {
|
|
lane_id: 7,
|
|
in_progress: true,
|
|
session: {
|
|
id: 805,
|
|
reg: "AB12345",
|
|
customer_number: 12345679,
|
|
vehicle_type_id: 2,
|
|
machine_relay_enabled: false,
|
|
wash_started_at: "2026-04-28 10:15:00",
|
|
},
|
|
customer: { customer_number: 12345679 },
|
|
vehicle: { reg: "AB12345", type: 2 },
|
|
},
|
|
},
|
|
};
|
|
}
|
|
|
|
return {
|
|
data: {
|
|
data: {
|
|
lane_id: payload?.lane_id,
|
|
in_progress: false,
|
|
session: null,
|
|
customer: null,
|
|
vehicle: null,
|
|
},
|
|
},
|
|
};
|
|
});
|
|
|
|
const wrapper = mountWithApp(MyWashStart, {
|
|
global: {
|
|
stubs: stubComponents,
|
|
},
|
|
});
|
|
|
|
await flushPromises();
|
|
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
await flushPromises();
|
|
|
|
expect(mocks.sessionRequest).not.toHaveBeenCalledWith(
|
|
"/modules/self-serve/lane/wash/in-progress",
|
|
"GET",
|
|
expect.anything()
|
|
);
|
|
|
|
mocks.sessionCustomerNumber.value = 12345679;
|
|
await nextTick();
|
|
await flushPromises();
|
|
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
await flushPromises();
|
|
|
|
expect(mocks.sessionRequest).toHaveBeenCalledWith("/modules/self-serve/lane/wash/my-active-wash", "GET");
|
|
expect(mocks.fetchWashSummary).toHaveBeenCalledWith({ session_id: 805, vehicle_type: 2 }, false);
|
|
expect(mocks.startElapsedTimer).toHaveBeenCalled();
|
|
expect(
|
|
wrapper.get('[data-testid="self-serve-nav-open-property-access-gate"]').attributes("style") || ""
|
|
).not.toContain("display: none");
|
|
|
|
wrapper.unmount();
|
|
});
|
|
|
|
it("does not restore another driver's active wash under the same customer number", async () => {
|
|
localStorage.setItem("is_subuser", "true");
|
|
localStorage.setItem("selected_customer_number", "12345679");
|
|
mocks.selectedGrantCustomerNumber.value = 12345679;
|
|
mocks.sessionSubuserId.value = 501;
|
|
mocks.sessionRequest.mockImplementation(async (path, method, payload) => {
|
|
if (path === "/modules/self-serve/lane/wash/my-active-wash" && method === "GET") {
|
|
return {
|
|
data: {
|
|
data: {
|
|
lane_id: 7,
|
|
in_progress: true,
|
|
session: {
|
|
id: 906,
|
|
reg: "AB12345",
|
|
customer_number: 12345679,
|
|
subuser_id: 502,
|
|
vehicle_type_id: 2,
|
|
machine_relay_enabled: true,
|
|
machine_start_triggered: true,
|
|
wash_started_at: "2026-04-28 10:15:00",
|
|
},
|
|
subuser: { id: 502, name: "Other Driver" },
|
|
customer: { customer_number: 12345679 },
|
|
vehicle: { reg: "AB12345", type: 2 },
|
|
},
|
|
},
|
|
};
|
|
}
|
|
|
|
return {
|
|
data: {
|
|
data: {
|
|
lane_id: payload?.lane_id,
|
|
in_progress: false,
|
|
session: null,
|
|
customer: null,
|
|
vehicle: null,
|
|
},
|
|
},
|
|
};
|
|
});
|
|
|
|
const wrapper = mountWithApp(MyWashStart, {
|
|
global: {
|
|
stubs: stubComponents,
|
|
},
|
|
});
|
|
|
|
await flushPromises();
|
|
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
await flushPromises();
|
|
|
|
expect(mocks.sessionRequest).toHaveBeenCalledWith("/modules/self-serve/lane/wash/my-active-wash", "GET");
|
|
expect(mocks.fetchWashSummary).not.toHaveBeenCalledWith({ session_id: 906, vehicle_type: 2 }, false);
|
|
expect(mocks.startElapsedTimer).not.toHaveBeenCalled();
|
|
expect(mocks.saveProgress).not.toHaveBeenCalledWith("serverActiveWash");
|
|
expect(wrapper.get('[data-testid="self-serve-bottom-actions"]').attributes("style") || "").toContain(
|
|
"display: none"
|
|
);
|
|
|
|
wrapper.unmount();
|
|
});
|
|
|
|
it("wires property gate buttons in the in-progress step", async () => {
|
|
mocks.restoredProgressPayload = {
|
|
washInProgress: true,
|
|
washLaneId: 7,
|
|
washStartTime: Date.now() - 20_000,
|
|
licensePlateInput: "AB12345",
|
|
vehicleTypeSelect: 2,
|
|
radioWashType: "Manual",
|
|
radioLaneOption: 7,
|
|
customerNumberInput: 12345679,
|
|
isForcingNearestDepartment: false,
|
|
forceNearestDepartmentEvaluationId: 0,
|
|
answers: {},
|
|
completedTasks: {},
|
|
currentStep: 4,
|
|
};
|
|
|
|
const wrapper = mountWithApp(MyWashStart, {
|
|
global: {
|
|
stubs: stubComponents,
|
|
},
|
|
});
|
|
|
|
await flushPromises();
|
|
|
|
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");
|
|
await new Promise((resolve) => setTimeout(resolve, 260));
|
|
await flushPromises();
|
|
|
|
expect(mocks.onStopWash).toHaveBeenCalledWith(7);
|
|
expect(mocks.openPropertyExitGate).toHaveBeenLastCalledWith(7, { suppressAlert: true });
|
|
});
|
|
|
|
it("resets guided wash steps after completing so the next wash starts from the first instruction", async () => {
|
|
mocks.restoredProgressPayload = {
|
|
washInProgress: true,
|
|
washLaneId: 7,
|
|
washStartTime: Date.now() - 20_000,
|
|
licensePlateInput: "AB12345",
|
|
vehicleTypeSelect: 2,
|
|
radioWashType: "Manual",
|
|
radioLaneOption: 7,
|
|
customerNumberInput: 12345679,
|
|
isForcingNearestDepartment: false,
|
|
forceNearestDepartmentEvaluationId: 0,
|
|
answers: {},
|
|
completedTasks: {},
|
|
currentStep: 4,
|
|
};
|
|
|
|
const wrapper = mountWithApp(MyWashStart, {
|
|
global: {
|
|
stubs: stubComponents,
|
|
},
|
|
});
|
|
|
|
await flushPromises();
|
|
|
|
const guidedStepText = () => wrapper.get('[data-testid="guided-step-stub"]').text();
|
|
const guidedActions = wrapper
|
|
.get('[data-testid="self-serve-bottom-actions"]')
|
|
.get('[data-testid="self-serve-guided-bottom-actions"]');
|
|
|
|
expect(guidedStepText()).toBe("0");
|
|
|
|
for (let index = 0; index < 5; index += 1) {
|
|
await guidedActions.get('[data-testid="self-serve-guided-next"]').trigger("click");
|
|
}
|
|
|
|
expect(guidedStepText()).toBe("5");
|
|
|
|
await guidedActions.get('[data-testid="self-serve-nav-complete"]').trigger("click");
|
|
await new Promise((resolve) => setTimeout(resolve, 260));
|
|
await flushPromises();
|
|
|
|
expect(mocks.onStopWash).toHaveBeenCalledWith(7);
|
|
await vi.waitFor(() => {
|
|
expect(wrapper.find('[data-testid="self-serve-nav-close"]').exists()).toBe(true);
|
|
});
|
|
|
|
wrapper.vm.currentStep = 0;
|
|
await nextTick();
|
|
|
|
expect(wrapper.find('[data-testid="completed-step-stub"]').exists()).toBe(true);
|
|
|
|
await wrapper.get('[data-testid="self-serve-nav-close"]').trigger("click");
|
|
await nextTick();
|
|
|
|
expect(wrapper.vm.currentGuidedWashStep).toBe(0);
|
|
expect(mocks.clearProgress).toHaveBeenCalled();
|
|
});
|
|
|
|
it("shows a finishing screen while completing and then opens the property exit gate silently", async () => {
|
|
mocks.restoredProgressPayload = {
|
|
washInProgress: true,
|
|
washLaneId: 7,
|
|
washStartTime: Date.now() - 20_000,
|
|
licensePlateInput: "AB12345",
|
|
vehicleTypeSelect: 2,
|
|
radioWashType: "Manual",
|
|
radioLaneOption: 7,
|
|
customerNumberInput: 12345679,
|
|
isForcingNearestDepartment: false,
|
|
forceNearestDepartmentEvaluationId: 0,
|
|
answers: {},
|
|
completedTasks: {},
|
|
currentStep: 4,
|
|
};
|
|
let resolveStop;
|
|
mocks.onStopWash.mockReturnValueOnce(
|
|
new Promise((resolve) => {
|
|
resolveStop = resolve;
|
|
})
|
|
);
|
|
|
|
const wrapper = mountWithApp(MyWashStart, {
|
|
global: {
|
|
stubs: stubComponents,
|
|
},
|
|
messages: {
|
|
en: {
|
|
common: {
|
|
done: "Done",
|
|
next: "Next",
|
|
previous: "Previous",
|
|
},
|
|
self_wash: {
|
|
finishing_wash_exit_opening: "Afslutter vask, porten åbnes automatisk",
|
|
},
|
|
user_dashboard: {
|
|
wash: {
|
|
title: "Wash",
|
|
subtitle: "Wash",
|
|
},
|
|
},
|
|
},
|
|
},
|
|
});
|
|
|
|
await flushPromises();
|
|
const guidedActions = wrapper
|
|
.get('[data-testid="self-serve-bottom-actions"]')
|
|
.get('[data-testid="self-serve-guided-bottom-actions"]');
|
|
|
|
for (let index = 0; index < 5; index += 1) {
|
|
await guidedActions.get('[data-testid="self-serve-guided-next"]').trigger("click");
|
|
}
|
|
|
|
await guidedActions.get('[data-testid="self-serve-nav-complete"]').trigger("click");
|
|
await nextTick();
|
|
|
|
expect(JSON.parse(localStorage.getItem(stoppingWashStorageKey))).toMatchObject({
|
|
laneId: 7,
|
|
reg: "AB12345",
|
|
customerNumber: 12345679,
|
|
});
|
|
expect(wrapper.get('[data-testid="self-serve-finishing-wash"]').text()).toContain(
|
|
"Afslutter vask, porten åbnes automatisk"
|
|
);
|
|
expect(mocks.onStopWash).toHaveBeenCalledWith(7);
|
|
expect(mocks.openPropertyExitGate).not.toHaveBeenCalled();
|
|
|
|
resolveStop(true);
|
|
await new Promise((resolve) => setTimeout(resolve, 260));
|
|
await flushPromises();
|
|
|
|
expect(mocks.openPropertyExitGate).toHaveBeenCalledWith(7, { suppressAlert: true });
|
|
expect(localStorage.getItem(stoppingWashStorageKey)).toBeNull();
|
|
});
|
|
|
|
it("restores the finishing screen after refresh when a stopping marker matches active progress", async () => {
|
|
mocks.restoredProgressPayload = {
|
|
washInProgress: true,
|
|
washLaneId: 7,
|
|
washStartTime: Date.now() - 20_000,
|
|
licensePlateInput: "AB12345",
|
|
vehicleTypeSelect: 2,
|
|
radioWashType: "Manual",
|
|
radioLaneOption: 7,
|
|
customerNumberInput: 12345679,
|
|
isForcingNearestDepartment: false,
|
|
forceNearestDepartmentEvaluationId: 0,
|
|
answers: {},
|
|
completedTasks: {},
|
|
currentStep: 4,
|
|
};
|
|
localStorage.setItem(
|
|
stoppingWashStorageKey,
|
|
JSON.stringify({
|
|
laneId: 7,
|
|
reg: "AB12345",
|
|
customerNumber: 12345679,
|
|
startedAt: Date.now(),
|
|
})
|
|
);
|
|
|
|
const wrapper = mountWithApp(MyWashStart, {
|
|
global: {
|
|
stubs: stubComponents,
|
|
},
|
|
messages: {
|
|
en: {
|
|
self_wash: {
|
|
finishing_wash_exit_opening: "Afslutter vask, porten åbnes automatisk",
|
|
},
|
|
user_dashboard: {
|
|
wash: {
|
|
title: "Wash",
|
|
subtitle: "Wash",
|
|
},
|
|
},
|
|
},
|
|
},
|
|
});
|
|
|
|
await flushPromises();
|
|
|
|
expect(wrapper.get('[data-testid="self-serve-finishing-wash"]').text()).toContain(
|
|
"Afslutter vask, porten åbnes automatisk"
|
|
);
|
|
expect(wrapper.find('[data-testid="self-serve-bottom-actions"]').exists()).toBe(false);
|
|
expect(localStorage.getItem(stoppingWashStorageKey)).not.toBeNull();
|
|
});
|
|
|
|
it("ignores and clears an expired stopping marker during active progress restore", async () => {
|
|
mocks.restoredProgressPayload = {
|
|
washInProgress: true,
|
|
washLaneId: 7,
|
|
washStartTime: Date.now() - 20_000,
|
|
licensePlateInput: "AB12345",
|
|
vehicleTypeSelect: 2,
|
|
radioWashType: "Manual",
|
|
radioLaneOption: 7,
|
|
customerNumberInput: 12345679,
|
|
isForcingNearestDepartment: false,
|
|
forceNearestDepartmentEvaluationId: 0,
|
|
answers: {},
|
|
completedTasks: {},
|
|
currentStep: 4,
|
|
};
|
|
localStorage.setItem(
|
|
stoppingWashStorageKey,
|
|
JSON.stringify({
|
|
laneId: 7,
|
|
reg: "AB12345",
|
|
customerNumber: 12345679,
|
|
startedAt: Date.now() - 3 * 60 * 1000,
|
|
})
|
|
);
|
|
|
|
const wrapper = mountWithApp(MyWashStart, {
|
|
global: {
|
|
stubs: stubComponents,
|
|
},
|
|
});
|
|
|
|
await flushPromises();
|
|
|
|
expect(wrapper.find('[data-testid="self-serve-finishing-wash"]').exists()).toBe(false);
|
|
expect(localStorage.getItem(stoppingWashStorageKey)).toBeNull();
|
|
});
|
|
|
|
it("keeps the guided wash in progress when stop fails", async () => {
|
|
vi.useFakeTimers();
|
|
mocks.restoredProgressPayload = {
|
|
washInProgress: true,
|
|
washLaneId: 7,
|
|
washStartTime: Date.now() - 20_000,
|
|
licensePlateInput: "AB12345",
|
|
vehicleTypeSelect: 2,
|
|
radioWashType: "Manual",
|
|
radioLaneOption: 7,
|
|
customerNumberInput: 12345679,
|
|
isForcingNearestDepartment: false,
|
|
forceNearestDepartmentEvaluationId: 0,
|
|
answers: {},
|
|
completedTasks: {},
|
|
currentStep: 4,
|
|
};
|
|
mocks.onStopWash.mockResolvedValue(false);
|
|
|
|
const wrapper = mountWithApp(MyWashStart, {
|
|
global: {
|
|
stubs: stubComponents,
|
|
},
|
|
});
|
|
|
|
await flushPromises();
|
|
|
|
const bottomActions = wrapper.get('[data-testid="self-serve-bottom-actions"]');
|
|
const guidedActions = bottomActions.get('[data-testid="self-serve-guided-bottom-actions"]');
|
|
|
|
for (let index = 0; index < 5; index += 1) {
|
|
await guidedActions.get('[data-testid="self-serve-guided-next"]').trigger("click");
|
|
}
|
|
|
|
await guidedActions.get('[data-testid="self-serve-nav-complete"]').trigger("click");
|
|
await vi.advanceTimersByTimeAsync(4000);
|
|
await flushPromises();
|
|
|
|
expect(mocks.onStopWash).toHaveBeenCalledWith(7);
|
|
expect(wrapper.get('[data-testid="guided-step-stub"]').text()).toBe("5");
|
|
expect(bottomActions.isVisible()).toBe(true);
|
|
});
|
|
|
|
it("shows completed when stop fails but the server confirms the wash is no longer in progress", async () => {
|
|
mocks.restoredProgressPayload = {
|
|
washInProgress: true,
|
|
washLaneId: 7,
|
|
washStartTime: Date.now() - 20_000,
|
|
licensePlateInput: "AB12345",
|
|
vehicleTypeSelect: 2,
|
|
radioWashType: "Manual",
|
|
radioLaneOption: 7,
|
|
customerNumberInput: 12345679,
|
|
isForcingNearestDepartment: false,
|
|
forceNearestDepartmentEvaluationId: 0,
|
|
answers: {},
|
|
completedTasks: {},
|
|
currentStep: 4,
|
|
};
|
|
let wrapper;
|
|
mocks.onStopWash.mockImplementation(async () => {
|
|
wrapper.vm.washActionError = "Network Error";
|
|
return false;
|
|
});
|
|
mocks.sessionRequest.mockImplementation(async (path, method, payload) => {
|
|
if (path === "/modules/self-serve/lane/wash/in-progress" && method === "GET" && payload?.lane_id === 7) {
|
|
return {
|
|
data: {
|
|
data: {
|
|
lane_id: 7,
|
|
in_progress: false,
|
|
session: null,
|
|
customer: null,
|
|
vehicle: null,
|
|
},
|
|
},
|
|
};
|
|
}
|
|
|
|
return undefined;
|
|
});
|
|
|
|
wrapper = mountWithApp(MyWashStart, {
|
|
global: {
|
|
stubs: stubComponents,
|
|
},
|
|
});
|
|
|
|
await flushPromises();
|
|
|
|
const guidedActions = wrapper
|
|
.get('[data-testid="self-serve-bottom-actions"]')
|
|
.get('[data-testid="self-serve-guided-bottom-actions"]');
|
|
|
|
for (let index = 0; index < 5; index += 1) {
|
|
await guidedActions.get('[data-testid="self-serve-guided-next"]').trigger("click");
|
|
}
|
|
|
|
await guidedActions.get('[data-testid="self-serve-nav-complete"]').trigger("click");
|
|
await new Promise((resolve) => setTimeout(resolve, 260));
|
|
await flushPromises();
|
|
|
|
expect(mocks.onStopWash).toHaveBeenCalledWith(7);
|
|
expect(mocks.sessionRequest).toHaveBeenCalledWith("/modules/self-serve/lane/wash/in-progress", "GET", {
|
|
lane_id: 7,
|
|
});
|
|
expect(mocks.stopElapsedTimer).toHaveBeenCalled();
|
|
expect(mocks.clearProgress).toHaveBeenCalled();
|
|
expect(mocks.openPropertyExitGate).toHaveBeenLastCalledWith(7, { suppressAlert: true });
|
|
expect(wrapper.vm.washActionError).toBe("Network Error");
|
|
expect(wrapper.find('[data-testid="completed-step-stub"]').exists()).toBe(true);
|
|
});
|
|
|
|
it("restores an active wash from the server when local progress is missing", async () => {
|
|
mocks.nearestDepartment.value = {
|
|
id: 6,
|
|
name: "Roskilde",
|
|
address: "Industrivej 45, 4000 Roskilde",
|
|
self_serve_enabled: true,
|
|
lanes: [{ id: 7, name: "7", status: "AVAILABLE", products: [2], machine_available: true }],
|
|
};
|
|
const odense = {
|
|
id: 2,
|
|
name: "Odense",
|
|
address: "Beta 2",
|
|
self_serve_enabled: true,
|
|
lanes: [{ id: 9, name: "9", status: "OCCUPIED", products: [2], machine_available: true }],
|
|
};
|
|
mocks.guestDepartments.value = [mocks.nearestDepartment.value, odense];
|
|
mocks.sessionRequest.mockImplementation(async (path, method, payload) => {
|
|
if (
|
|
(path === "/modules/self-serve/lane/wash/my-active-wash" && method === "GET") ||
|
|
(path === "/modules/self-serve/lane/wash/in-progress" && method === "GET" && payload?.lane_id === 9)
|
|
) {
|
|
return {
|
|
data: {
|
|
data: {
|
|
lane_id: 9,
|
|
in_progress: true,
|
|
session: {
|
|
id: 704,
|
|
reg: "CD67890",
|
|
customer_number: 12345679,
|
|
vehicle_type_id: 2,
|
|
machine_relay_enabled: true,
|
|
wash_started_at: "2026-04-28 10:15:00",
|
|
},
|
|
customer: { customer_number: 12345679 },
|
|
vehicle: { reg: "CD67890", type: 2 },
|
|
},
|
|
},
|
|
};
|
|
}
|
|
|
|
return {
|
|
data: {
|
|
data: {
|
|
lane_id: payload?.lane_id,
|
|
in_progress: false,
|
|
session: null,
|
|
customer: null,
|
|
vehicle: null,
|
|
},
|
|
},
|
|
};
|
|
});
|
|
|
|
const wrapper = mountWithApp(MyWashStart, {
|
|
global: {
|
|
stubs: stubComponents,
|
|
},
|
|
});
|
|
|
|
await flushPromises();
|
|
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
await flushPromises();
|
|
|
|
expect(mocks.sessionRequest).toHaveBeenCalledWith("/modules/self-serve/lane/wash/my-active-wash", "GET");
|
|
expect(mocks.sessionRequest).not.toHaveBeenCalledWith("/modules/self-serve/lane/wash/in-progress", "GET", {
|
|
lane_id: 7,
|
|
});
|
|
expect(mocks.fetchWashSummary).toHaveBeenCalledWith({ session_id: 704, vehicle_type: 2 }, false);
|
|
expect(mocks.startElapsedTimer).toHaveBeenCalled();
|
|
expect(mocks.saveProgress).toHaveBeenCalledWith("serverActiveWash");
|
|
|
|
const accessGateButton = wrapper.get('[data-testid="self-serve-nav-open-property-access-gate"]');
|
|
expect(accessGateButton.attributes("style") || "").not.toContain("display: none");
|
|
await accessGateButton.trigger("click");
|
|
expect(mocks.openPropertyAccessGate).toHaveBeenCalledWith(9);
|
|
});
|
|
|
|
it("does not restore a preview-only ready session as an active wash", async () => {
|
|
mocks.sessionRequest.mockImplementation(async (path, method, payload) => {
|
|
if (path === "/modules/self-serve/lane/wash/my-active-wash" && method === "GET") {
|
|
return {
|
|
data: {
|
|
data: {
|
|
lane_id: 7,
|
|
in_progress: true,
|
|
status: "READY_FOR_MACHINE_START",
|
|
session: {
|
|
id: 901,
|
|
status: "READY_FOR_MACHINE_START",
|
|
reg: "AB12345",
|
|
customer_number: 12345679,
|
|
vehicle_type_id: 2,
|
|
machine_relay_enabled: false,
|
|
machine_start_triggered: false,
|
|
machine_relay_enabled_at: null,
|
|
machine_start_triggered_at: null,
|
|
wash_started_at: null,
|
|
},
|
|
customer: { customer_number: 12345679 },
|
|
vehicle: { reg: "AB12345", type: 2 },
|
|
},
|
|
},
|
|
};
|
|
}
|
|
|
|
return {
|
|
data: {
|
|
data: {
|
|
lane_id: payload?.lane_id,
|
|
in_progress: false,
|
|
session: null,
|
|
customer: null,
|
|
vehicle: null,
|
|
},
|
|
},
|
|
};
|
|
});
|
|
|
|
const wrapper = mountWithApp(MyWashStart, {
|
|
global: {
|
|
stubs: stubComponents,
|
|
},
|
|
});
|
|
|
|
await flushPromises();
|
|
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
await flushPromises();
|
|
|
|
expect(mocks.sessionRequest).toHaveBeenCalledWith("/modules/self-serve/lane/wash/my-active-wash", "GET");
|
|
expect(mocks.fetchWashSummary).not.toHaveBeenCalled();
|
|
expect(mocks.startElapsedTimer).not.toHaveBeenCalled();
|
|
expect(mocks.saveProgress).not.toHaveBeenCalledWith("serverActiveWash");
|
|
expect(wrapper.get('[data-testid="self-serve-bottom-actions"]').attributes("style") || "").toContain(
|
|
"display: none"
|
|
);
|
|
});
|
|
|
|
it("does not restore a recently completed wash from the server on revisit", async () => {
|
|
localStorage.setItem(
|
|
"mywash_recent_completed_v1",
|
|
JSON.stringify({
|
|
laneId: 7,
|
|
reg: "AB12345",
|
|
customerNumber: 12345679,
|
|
completedAt: Date.now(),
|
|
})
|
|
);
|
|
mocks.sessionRequest.mockImplementation(async (path, method, payload) => {
|
|
if (path === "/modules/self-serve/lane/wash/my-active-wash" && method === "GET") {
|
|
return {
|
|
data: {
|
|
data: {
|
|
lane_id: 7,
|
|
in_progress: true,
|
|
session: {
|
|
id: 805,
|
|
reg: "AB12345",
|
|
customer_number: 12345679,
|
|
vehicle_type_id: 2,
|
|
machine_relay_enabled: false,
|
|
wash_started_at: "2026-04-28 10:15:00",
|
|
},
|
|
customer: { customer_number: 12345679 },
|
|
vehicle: { reg: "AB12345", type: 2 },
|
|
},
|
|
},
|
|
};
|
|
}
|
|
|
|
return {
|
|
data: {
|
|
data: {
|
|
lane_id: payload?.lane_id,
|
|
in_progress: false,
|
|
session: null,
|
|
customer: null,
|
|
vehicle: null,
|
|
},
|
|
},
|
|
};
|
|
});
|
|
|
|
const wrapper = mountWithApp(MyWashStart, {
|
|
global: {
|
|
stubs: stubComponents,
|
|
},
|
|
});
|
|
|
|
await flushPromises();
|
|
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
await flushPromises();
|
|
|
|
expect(mocks.sessionRequest).toHaveBeenCalledWith("/modules/self-serve/lane/wash/my-active-wash", "GET");
|
|
expect(mocks.fetchWashSummary).not.toHaveBeenCalled();
|
|
expect(mocks.startElapsedTimer).not.toHaveBeenCalled();
|
|
expect(mocks.saveProgress).not.toHaveBeenCalledWith("serverActiveWash");
|
|
expect(wrapper.find('[data-testid="completed-step-stub"]').exists()).toBe(true);
|
|
});
|
|
|
|
it("keeps restored in-progress state when active wash refresh cannot verify authenticated ownership before completion", async () => {
|
|
mocks.restoredProgressPayload = {
|
|
washInProgress: true,
|
|
washLaneId: 7,
|
|
washStartTime: Date.now() - 30_000,
|
|
licensePlateInput: "AB12345",
|
|
vehicleTypeSelect: 2,
|
|
radioWashType: "Manual",
|
|
radioLaneOption: 7,
|
|
customerNumberInput: 12345679,
|
|
isForcingNearestDepartment: false,
|
|
forceNearestDepartmentEvaluationId: 0,
|
|
answers: {},
|
|
completedTasks: {},
|
|
currentStep: 4,
|
|
};
|
|
mocks.sessionRequest.mockImplementation(async (path, method, payload) => {
|
|
if (path === "/modules/self-serve/lane/wash/in-progress" && method === "GET" && payload?.lane_id === 7) {
|
|
return {
|
|
data: {
|
|
data: {
|
|
lane_id: 7,
|
|
in_progress: true,
|
|
session: {
|
|
id: 909,
|
|
reg: "VICTIM42",
|
|
customer_number: null,
|
|
vehicle_type_id: 2,
|
|
machine_relay_enabled: true,
|
|
wash_started_at: "2026-04-28 10:15:00",
|
|
},
|
|
customer: {},
|
|
vehicle: { reg: "VICTIM42", type: 2 },
|
|
},
|
|
},
|
|
};
|
|
}
|
|
|
|
return undefined;
|
|
});
|
|
|
|
const wrapper = mountWithApp(MyWashStart, {
|
|
global: {
|
|
stubs: stubComponents,
|
|
},
|
|
});
|
|
|
|
await flushPromises();
|
|
await nextTick();
|
|
await flushPromises();
|
|
|
|
expect(mocks.sessionRequest).toHaveBeenCalledWith("/modules/self-serve/lane/wash/in-progress", "GET", {
|
|
lane_id: 7,
|
|
});
|
|
expect(mocks.fetchWashSummary).not.toHaveBeenCalledWith({ session_id: 909, vehicle_type: 2 }, false);
|
|
expect(mocks.saveProgress).not.toHaveBeenCalledWith("serverActiveWashRefresh");
|
|
expect(mocks.stopElapsedTimer).not.toHaveBeenCalled();
|
|
expect(mocks.clearProgress).not.toHaveBeenCalled();
|
|
expect(wrapper.get('[data-testid="self-serve-bottom-actions"]').attributes("style") || "").not.toContain(
|
|
"display: none"
|
|
);
|
|
|
|
wrapper.unmount();
|
|
});
|
|
|
|
it("keeps restored in-progress state when active wash refresh says the lane is no longer in progress before completion", async () => {
|
|
mocks.restoredProgressPayload = {
|
|
washInProgress: true,
|
|
washLaneId: 7,
|
|
washStartTime: Date.now() - 30_000,
|
|
licensePlateInput: "AB12345",
|
|
vehicleTypeSelect: 2,
|
|
radioWashType: "Manual",
|
|
radioLaneOption: 7,
|
|
customerNumberInput: 12345679,
|
|
isForcingNearestDepartment: false,
|
|
forceNearestDepartmentEvaluationId: 0,
|
|
answers: {},
|
|
completedTasks: {},
|
|
currentStep: 4,
|
|
};
|
|
mocks.sessionRequest.mockImplementation(async (path, method, payload) => {
|
|
if (path === "/modules/self-serve/lane/wash/in-progress" && method === "GET" && payload?.lane_id === 7) {
|
|
return {
|
|
data: {
|
|
data: {
|
|
lane_id: 7,
|
|
in_progress: false,
|
|
session: null,
|
|
customer: null,
|
|
vehicle: null,
|
|
},
|
|
},
|
|
};
|
|
}
|
|
|
|
return undefined;
|
|
});
|
|
|
|
const wrapper = mountWithApp(MyWashStart, {
|
|
global: {
|
|
stubs: stubComponents,
|
|
},
|
|
});
|
|
|
|
await flushPromises();
|
|
await nextTick();
|
|
await flushPromises();
|
|
|
|
expect(mocks.sessionRequest).toHaveBeenCalledWith("/modules/self-serve/lane/wash/in-progress", "GET", {
|
|
lane_id: 7,
|
|
});
|
|
expect(mocks.stopElapsedTimer).not.toHaveBeenCalled();
|
|
expect(mocks.clearProgress).not.toHaveBeenCalled();
|
|
expect(mocks.fetchWashSummary).not.toHaveBeenCalled();
|
|
expect(wrapper.get('[data-testid="self-serve-bottom-actions"]').attributes("style") || "").not.toContain(
|
|
"display: none"
|
|
);
|
|
|
|
wrapper.unmount();
|
|
});
|
|
});
|