Files
pleno-vue/tests/unit/my-wash-start.spec.js
T
Jeppe Bundgaard b39b458f4f Add .prettierrc.json and refactor test files for improved formatting consistency:
- Introduced `.prettierrc.json` to enforce consistent code formatting across the project.
- Updated unit and e2e test files to address formatting issues, improve readability, and ensure alignment with the new Prettier configuration.
2026-04-13 09:13:27 +02:00

538 lines
17 KiB
JavaScript

// @vitest-environment jsdom
import { flushPromises } from "@vue/test-utils";
import { nextTick } from "vue";
import { beforeEach, describe, expect, it, vi } from "vitest";
const mocks = vi.hoisted(() => {
const asMockRef = (value) => ({ value, __v_isRef: true });
const nearestDepartment = {
value: {
id: 1,
name: "Copenhagen",
address: "Main street 1",
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"] };
return {
nearestDepartment,
guestDepartments,
doesCurrentDepartmentSelectionHaveSelfServeEnabled,
answers,
completedTasks,
visibleQuestions,
activeTasks,
conditions,
rules,
allowedServices,
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(),
};
});
vi.mock("@/components/session/token/SessionUser.vue", () => ({
SessionUser: {
user: {
customer_number: { value: 12345 },
},
canAccessSuperUser: () => true,
request: vi.fn(async (...args) => {
mocks.sessionRequest(...args);
return { 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,
},
},
},
},
}));
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 },
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", () => ({
useSelfServeLogic: () => ({
loading: { value: false },
lane: { value: { id: 7 } },
questions: { value: [] },
conditions: mocks.conditions,
rules: mocks.rules,
tasks: { value: [] },
answers: mocks.answers,
completedTasks: mocks.completedTasks,
allowedServices: mocks.allowedServices,
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: () => ({
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),
}),
};
});
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-vehicle-type" @click="$emit('select-vehicle-type', { id: 2, name: 'Truck' })">vehicle</button>
</div>
`,
},
SelfServeQuestionsStep: {
emits: ["answer-question"],
template: "<button data-testid='emit-answer' @click=\"$emit('answer-question', 11, true)\">answer</button>",
},
SelfServeLaneStep: {
emits: ["update:selected-lane-id"],
template: "<button data-testid='emit-lane' @click=\"$emit('update:selected-lane-id', 7)\">lane</button>",
},
SelfServeTasksStep: {
template: "<div data-testid='tasks-step-stub' />",
},
SelfServeGuidedInstructions: {
template: "<div data-testid='guided-step-stub' />",
},
SelfServeCompletedStep: {
template: "<div data-testid='completed-step-stub' />",
},
BSteps: BStepsStub,
BStepItem: BStepItemStub,
BButton: BButtonStub,
BMessage: {
template: "<div><slot /></div>",
},
};
describe("MyWashStart", () => {
beforeEach(() => {
mocks.nearestDepartment.value = {
id: 1,
name: "Copenhagen",
address: "Main street 1",
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.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.mockClear();
mocks.restoredProgressPayload = null;
mocks.syncVehicleAnswer.mockClear();
mocks.answerQuestion.mockClear();
mocks.setShowFooterInContent.mockClear();
mocks.addVehicle.mockClear();
mocks.sessionRequest.mockClear();
mocks.openPropertyAccessGate.mockClear();
mocks.openPropertyExitGate.mockClear();
});
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 () => {
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 flushPromises();
expect(mocks.fetchSelfServeDataInternal).toHaveBeenCalledWith(1, 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: 1,
laneId: 7,
customerNumber: 12345,
reg: "AB12345",
questionId: 11,
value: true,
});
});
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("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, 12345);
expect(mocks.fetchCustomerVehicles.mock.calls.length).toBeGreaterThan(1);
expect(mocks.fetchSelfServeDataInternal).toHaveBeenCalledWith(1, 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(1, 2, 7, "AB12345");
});
it("falls back to available lane when restored lane is stale for the selected department", async () => {
mocks.restoredProgressPayload = {
washInProgress: false,
washLaneId: null,
washStartTime: null,
licensePlateInput: "AB12345",
vehicleTypeSelect: 2,
radioWashType: "Manual",
radioLaneOption: 999,
customerNumberInput: 12345,
isForcingNearestDepartment: false,
forceNearestDepartmentEvaluationId: 0,
answers: {},
completedTasks: {},
currentStep: 1,
};
const wrapper = mountWithApp(MyWashStart, {
global: {
stubs: stubComponents,
},
});
await flushPromises();
expect(mocks.fetchSelfServeDataInternal).toHaveBeenCalledWith(1, 2, 7, "AB12345");
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();
await wrapper.get('[data-testid="emit-answer"]').trigger("click");
await flushPromises();
expect(mocks.onStartWash).not.toHaveBeenCalled();
});
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: 12345,
isForcingNearestDepartment: false,
forceNearestDepartmentEvaluationId: 0,
answers: {},
completedTasks: {},
currentStep: 4,
};
const wrapper = mountWithApp(MyWashStart, {
global: {
stubs: stubComponents,
},
});
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");
expect(mocks.openPropertyAccessGate).toHaveBeenCalledWith(7);
expect(mocks.openPropertyExitGate).toHaveBeenCalledWith(7);
});
});