Files
pleno-vue/tests/unit/pos-department-step-1.spec.js
T

443 lines
14 KiB
JavaScript

// @vitest-environment jsdom
import { defineComponent } from "vue";
import { flushPromises } from "@vue/test-utils";
import { beforeEach, describe, expect, it, vi } from "vitest";
import { mountWithApp } from "./helpers/mountWithApp.js";
function buildContext(overrides = {}) {
return {
reg1: "",
reg2: "",
bookingMatches: [],
selectedBookingId: null,
skippedBooking: false,
requiresBookingSelection: false,
bookingResolution: "none",
committed: false,
source: "initial",
...overrides,
bookingMatches: Array.isArray(overrides.bookingMatches)
? overrides.bookingMatches.map((booking) => ({ ...booking }))
: [],
};
}
const posProcessState = vi.hoisted(() => ({
customerName: null,
orderId: null,
customerId: null,
step: null,
departmentId: null,
currentStep: null,
nextStep: vi.fn(),
clearCache: vi.fn(),
copyLastWashItemsToCurrentOrder: vi.fn(),
preflightHandler: null,
}));
const sessionState = vi.hoisted(() => ({
request: vi.fn(),
redirectToDepartment: vi.fn(),
}));
const selectVehicleFormState = vi.hoisted(() => ({
commitContext: null,
finalizedContext: null,
bookingSelectionContext: null,
skipBookingContext: null,
refreshContext: null,
finalizeDesktopStep1Context: vi.fn(),
applyBookingChoice: vi.fn(),
skipBookingChoice: vi.fn(),
refreshOrderBookingMatches: vi.fn(),
}));
vi.mock("@/components/shop/POSDepartmentProcess.vue", async () => {
const { ref } = await import("vue");
posProcessState.customerName = ref("");
posProcessState.orderId = ref(null);
posProcessState.customerId = ref(null);
posProcessState.step = ref(1);
posProcessState.departmentId = ref(12);
posProcessState.currentStep = ref(1);
return {
customer_name: posProcessState.customerName,
isCustomerSelected: () => Boolean(posProcessState.customerName.value),
department_id: posProcessState.departmentId,
order_id: posProcessState.orderId,
customer_id: posProcessState.customerId,
step: posProcessState.step,
getDepartment: () => 12,
getCurrentStep: () => posProcessState.currentStep.value,
clearCache: posProcessState.clearCache,
nextStep: posProcessState.nextStep,
copyLastWashItemsToCurrentOrder: posProcessState.copyLastWashItemsToCurrentOrder,
setDesktopStep1PreflightHandler: (handler = null) => {
posProcessState.preflightHandler = handler;
},
clearDesktopStep1PreflightHandler: (handler = null) => {
if (handler === null || posProcessState.preflightHandler === handler) {
posProcessState.preflightHandler = null;
}
},
};
});
vi.mock("@/components/session/token/SessionUser.vue", () => ({
SessionUser: {
request: sessionState.request,
functions: {
redirectTo: {
department: sessionState.redirectToDepartment,
},
},
objects: {
orders: {
meta: {
endpoint: "/orders",
},
columns: {
reg_1: {
label: "Number plate",
},
reference: {
label: "Reference",
},
},
},
global: {
language: {
possible_duplicates: "Possible duplicates",
},
},
},
},
}));
vi.mock("@/views/dashboards/departmentDashboard/modules/Pos/posRouteState.js", () => ({
parsePosRouteSearch: vi.fn(() => ({
orderId: null,
customerId: null,
step: null,
})),
}));
vi.mock("@/components/displays/department/pos/utils/orderBookingDisplay.js", () => ({
getOrderBookingReferenceValue: vi.fn((booking) => booking?.reference || booking?.reference_number || ""),
getOrderBookingServiceText: vi.fn(() => "Service"),
}));
import PosDepartmentStep1 from "@/components/displays/department/pos/steps/PosDepartmentStep1.vue";
const SlotStub = defineComponent({
template: "<div><slot /></div>",
});
const ElementTabsBoxStub = defineComponent({
template: `
<div>
<slot />
<slot name="customer" />
<slot name="license_plates" />
</div>
`,
});
const NextStepErrorStub = defineComponent({
template: '<div data-testid="pos-next-step-error-stub"></div>',
});
const SelectVehicleFormPOSStub = defineComponent({
emits: ["update:desktopStep1Context", "commit:desktopStep1"],
setup(_, { emit, expose }) {
const finalizeDesktopStep1Context = (...args) => selectVehicleFormState.finalizeDesktopStep1Context(...args);
const applyBookingChoice = (...args) => selectVehicleFormState.applyBookingChoice(...args);
const skipBookingChoice = (...args) => selectVehicleFormState.skipBookingChoice(...args);
const refreshOrderBookingMatches = (...args) => selectVehicleFormState.refreshOrderBookingMatches(...args);
expose({
finalizeDesktopStep1Context,
applyBookingChoice,
skipBookingChoice,
refreshOrderBookingMatches,
});
return {
emitCommit() {
emit("commit:desktopStep1", buildContext(selectVehicleFormState.commitContext));
},
};
},
template: `
<div>
<button type="button" data-testid="emit-step1-commit" @click="emitCommit">Commit</button>
</div>
`,
});
const PosDesktopOrderBookingSelectorModalStub = defineComponent({
props: {
isActive: {
type: Boolean,
default: false,
},
bookings: {
type: Array,
default: () => [],
},
},
emits: ["select", "skip", "refresh-bookings"],
methods: {
emitSelect() {
this.$emit("select", this.bookings[0] || null);
},
},
template: `
<div v-if="isActive" data-testid="pos-desktop-order-booking-modal">
<button type="button" data-testid="pos-desktop-order-booking-use-first" @click="emitSelect">Select</button>
</div>
`,
});
function mountStepOne() {
return mountWithApp(PosDepartmentStep1, {
messages: {
en: {
common: {
order: "Order",
},
admin: {
pos: {
license_plates: "License plates",
customer: "Customer",
not_found: "Not found",
order: "Order",
show_details: "Show details",
show_order_details: "Show order details",
hide_order_details: "Hide order details",
duplicate_order_warning: "Potential duplicates found.",
warning: "Warning",
order_booking_selector: {
title: "Select booking",
help_text: "Choose booking",
},
},
},
},
},
global: {
stubs: {
WhiteBox: SlotStub,
ButtonsBox: SlotStub,
ElementTabsBox: ElementTabsBoxStub,
PosSelectedCustomer: SlotStub,
PosLastScannedLicensePlatesV2: SlotStub,
NextStepError: NextStepErrorStub,
NextStep: defineComponent({
template: '<button type="button" data-testid="pos-next-step-stub">Next</button>',
}),
Cancel: defineComponent({
template: '<button type="button" data-testid="pos-cancel-stub">Cancel</button>',
}),
SelectVehicleFormPOS: SelectVehicleFormPOSStub,
PosDesktopOrderBookingSelectorModal: PosDesktopOrderBookingSelectorModalStub,
},
},
});
}
describe("PosDepartmentStep1 duplicate warning", () => {
beforeEach(() => {
posProcessState.customerName.value = "";
posProcessState.orderId.value = null;
posProcessState.customerId.value = null;
posProcessState.step.value = 1;
posProcessState.departmentId.value = 12;
posProcessState.currentStep.value = 1;
posProcessState.nextStep.mockReset();
posProcessState.clearCache.mockReset();
posProcessState.copyLastWashItemsToCurrentOrder.mockReset();
posProcessState.preflightHandler = null;
sessionState.request.mockReset();
sessionState.redirectToDepartment.mockReset();
selectVehicleFormState.commitContext = buildContext({
reg1: "AB12345",
committed: true,
source: "commit",
});
selectVehicleFormState.finalizedContext = buildContext({
reg1: "AB12345",
committed: true,
source: "commit",
});
selectVehicleFormState.bookingSelectionContext = buildContext({
reg1: "AB12345",
committed: true,
selectedBookingId: 8130,
bookingResolution: "selected",
source: "booking_selection",
});
selectVehicleFormState.skipBookingContext = buildContext({
reg1: "AB12345",
committed: true,
skippedBooking: true,
bookingResolution: "skipped",
source: "booking_skip",
});
selectVehicleFormState.refreshContext = buildContext({
reg1: "AB12345",
committed: false,
source: "booking_refresh",
});
selectVehicleFormState.finalizeDesktopStep1Context.mockReset();
selectVehicleFormState.finalizeDesktopStep1Context.mockImplementation(async () =>
buildContext(selectVehicleFormState.finalizedContext)
);
selectVehicleFormState.applyBookingChoice.mockReset();
selectVehicleFormState.applyBookingChoice.mockImplementation(async () =>
buildContext(selectVehicleFormState.bookingSelectionContext)
);
selectVehicleFormState.skipBookingChoice.mockReset();
selectVehicleFormState.skipBookingChoice.mockImplementation(async () =>
buildContext(selectVehicleFormState.skipBookingContext)
);
selectVehicleFormState.refreshOrderBookingMatches.mockReset();
selectVehicleFormState.refreshOrderBookingMatches.mockImplementation(async () =>
buildContext(selectVehicleFormState.refreshContext)
);
});
it("renders the inline warning and expands duplicate details after a committed duplicate match", async () => {
sessionState.request.mockResolvedValue({
data: {
data: [
{
id: 9206,
reg_1: "AB12345",
reference: "DUP-DETAILS",
customer_id: 12345679,
created_at: "2026-04-14T10:00:00.000Z",
},
],
},
});
const wrapper = mountStepOne();
await wrapper.get('[data-testid="emit-step1-commit"]').trigger("click");
await flushPromises();
expect(wrapper.get('[data-testid="pos-desktop-duplicate-warning-inline"]').text()).toContain(
"Potential duplicates found."
);
expect(wrapper.find('[data-testid="pos-desktop-duplicate-warning-details-inline"]').exists()).toBe(false);
await wrapper.get('[data-testid="pos-desktop-duplicate-warning-toggle-details"]').trigger("click");
await flushPromises();
expect(wrapper.get('[data-testid="pos-desktop-duplicate-warning-details-inline"]').text()).toContain("Order #9206");
await wrapper.get('[data-testid="pos-desktop-duplicate-order-open-9206"]').trigger("click");
expect(sessionState.redirectToDepartment).toHaveBeenCalledWith(12, "/modules/pos/orders/9206");
});
it("keeps booking selection ahead of the inline duplicate warning until booking resolution completes", async () => {
sessionState.request.mockResolvedValue({
data: {
data: [
{
id: 9207,
reg_1: "AB12345",
reference: "DUP-BOOKING",
customer_id: 12345679,
created_at: "2026-04-14T10:00:00.000Z",
},
],
},
});
const booking = {
id: 8130,
customer_name: "Pleno Logistics",
reg_1: "AB12345",
reference: "BOOKING-REF",
};
selectVehicleFormState.commitContext = buildContext({
reg1: "AB12345",
committed: true,
requiresBookingSelection: true,
bookingResolution: "selection_required",
bookingMatches: [booking],
source: "commit",
});
selectVehicleFormState.finalizedContext = buildContext(selectVehicleFormState.commitContext);
selectVehicleFormState.bookingSelectionContext = buildContext({
reg1: "AB12345",
committed: true,
selectedBookingId: booking.id,
bookingResolution: "selected",
bookingMatches: [booking],
source: "booking_selection",
});
selectVehicleFormState.finalizeDesktopStep1Context.mockReset();
selectVehicleFormState.finalizeDesktopStep1Context
.mockResolvedValueOnce(buildContext(selectVehicleFormState.commitContext))
.mockResolvedValue(buildContext(selectVehicleFormState.bookingSelectionContext));
const wrapper = mountStepOne();
await wrapper.get('[data-testid="emit-step1-commit"]').trigger("click");
await flushPromises();
expect(wrapper.get('[data-testid="pos-desktop-order-booking-modal"]').exists()).toBe(true);
expect(wrapper.find('[data-testid="pos-desktop-duplicate-warning-inline"]').exists()).toBe(false);
await wrapper.get('[data-testid="pos-desktop-order-booking-use-first"]').trigger("click");
await flushPromises();
expect(wrapper.find('[data-testid="pos-desktop-order-booking-modal"]').exists()).toBe(false);
expect(wrapper.get('[data-testid="pos-desktop-duplicate-warning-inline"]').exists()).toBe(true);
expect(selectVehicleFormState.applyBookingChoice).toHaveBeenCalledWith(booking, {
source: "booking_selection",
committed: true,
});
});
it("does not block desktop preflight when duplicates are found", async () => {
sessionState.request.mockResolvedValue({
data: {
data: [
{
id: 9208,
reg_1: "AB12345",
reference: "DUP-PREFLIGHT",
customer_id: 12345679,
created_at: "2026-04-14T10:00:00.000Z",
},
],
},
});
selectVehicleFormState.finalizedContext = buildContext({
reg1: "AB12345",
committed: true,
source: "next",
});
const wrapper = mountStepOne();
await flushPromises();
await expect(posProcessState.preflightHandler({ reason: "next" })).resolves.toBe(true);
await flushPromises();
expect(wrapper.get('[data-testid="pos-desktop-duplicate-warning-inline"]').exists()).toBe(true);
});
});