- 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.
538 lines
17 KiB
JavaScript
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);
|
|
});
|
|
});
|