Files
pleno-vue/tests/unit/select-vehicle-form-pos.spec.js
T

540 lines
18 KiB
JavaScript

// @vitest-environment jsdom
import { nextTick, ref } from "vue";
import { flushPromises } from "@vue/test-utils";
import { beforeEach, describe, expect, it, vi } from "vitest";
import { mountWithApp } from "./helpers/mountWithApp.js";
globalThis.ResizeObserver =
globalThis.ResizeObserver ||
class ResizeObserver {
observe() {}
unobserve() {}
disconnect() {}
};
const posState = vi.hoisted(() => ({
customerId: null,
departmentId: null,
notes: null,
customerName: null,
reference: null,
reg1: null,
reg2: null,
reg3: null,
customerAttributes: null,
orderNotes: null,
orderPo: null,
orderId: null,
selectedOrderBookingId: null,
selectedOrderBookingPlate: null,
skippedBookingPlate: null,
searchAndSelectCustomer: vi.fn(),
selectCustomer: vi.fn(),
setSelectedOrderBookingSelection: vi.fn(),
clearSelectedOrderBookingSelection: vi.fn(),
skipSelectedOrderBookingSelection: vi.fn(),
isSelectedOrderBookingSkippedForPlate: vi.fn(() => false),
doesVehiclePlateRequireBookingSelection: vi.fn(() => false),
getSelectedVehiclePlateBooking: vi.fn(() => null),
loadPendingBookings: vi.fn(async () => []),
ensureVehiclePlateBookingsLoaded: vi.fn(async () => []),
getCurrentStep: vi.fn(() => 1),
customerRequiresReferenceNumber: vi.fn(() => false),
isBlankPosMetadataValue: vi.fn((value) => String(value ?? "").trim() === ""),
}));
const normalizeSkippedBookingPlate = (value) =>
String(value ?? "")
.replace(/\s/g, "")
.toUpperCase();
vi.mock("@/components/shop/POSDepartmentProcess.vue", async () => {
posState.customerId = ref(null);
posState.departmentId = ref(12);
posState.notes = ref([]);
posState.customerName = ref("");
posState.reference = ref("");
posState.reg1 = ref("AB12345");
posState.reg2 = ref("");
posState.reg3 = ref("");
posState.customerAttributes = ref([]);
posState.orderNotes = ref("");
posState.orderPo = ref("");
posState.orderId = ref(null);
posState.selectedOrderBookingId = ref(null);
posState.selectedOrderBookingPlate = ref("");
posState.skippedBookingPlate = ref("");
posState.selectCustomer.mockImplementation((customer) => {
posState.customerId.value = customer?.customerNumber ?? null;
posState.customerName.value = customer?.name ?? "";
});
return {
customer_id: posState.customerId,
department_id: posState.departmentId,
notes: posState.notes,
isCustomerBarred: vi.fn(() => false),
customer_name: posState.customerName,
reference: posState.reference,
reg_1: posState.reg1,
reg_2: posState.reg2,
reg_3: posState.reg3,
order_notes: posState.orderNotes,
order_po: posState.orderPo,
order_id: posState.orderId,
searchAndSelectCustomer: posState.searchAndSelectCustomer,
selectCustomer: posState.selectCustomer,
isCustomerSelected: () => Boolean(posState.customerId.value),
customer_attributes: posState.customerAttributes,
selectedOrderBookingId: posState.selectedOrderBookingId,
selectedOrderBookingPlate: posState.selectedOrderBookingPlate,
setSelectedOrderBookingSelection: posState.setSelectedOrderBookingSelection,
clearSelectedOrderBookingSelection: posState.clearSelectedOrderBookingSelection,
skipSelectedOrderBookingSelection: posState.skipSelectedOrderBookingSelection,
isSelectedOrderBookingSkippedForPlate: posState.isSelectedOrderBookingSkippedForPlate,
doesVehiclePlateRequireBookingSelection: posState.doesVehiclePlateRequireBookingSelection,
getSelectedVehiclePlateBooking: posState.getSelectedVehiclePlateBooking,
loadPendingBookings: posState.loadPendingBookings,
ensureVehiclePlateBookingsLoaded: posState.ensureVehiclePlateBookingsLoaded,
getCurrentStep: posState.getCurrentStep,
customerRequiresReferenceNumber: posState.customerRequiresReferenceNumber,
isBlankPosMetadataValue: posState.isBlankPosMetadataValue,
};
});
vi.mock("@/components/session/token/SessionUser.vue", () => ({
SessionUser: {
adminUser: false,
objects: {
orders: {
columns: {
reg_1: { label: "Nummerplade" },
reg_2: { label: "Reg. 2" },
reg_3: { label: "Reg. 3" },
reference: { label: "Reference" },
},
},
global: {
language: {
customer: "Kunde",
select: "Vælg",
other: "Andet",
no_data: "Ingen data",
last_wash: "Sidste vask",
copy: "Kopier",
have: "Har",
have_not: "Har ikke",
quantity: "Antal",
show: "Vis",
hide: "Skjul",
},
},
vehicles: {
meta: {
labels: {
single: "Køretøj",
},
},
columns: {
wash_subscription: {
label: "Vaskeabonnement",
},
},
},
products: {
meta: {
labels: {
single: "Produkt",
},
},
functions: {
getProductName: vi.fn(() => "Forvogn"),
},
},
},
},
}));
import SelectVehicleFormPOS from "@/components/forms/department/pos/SelectVehicleFormPOS.vue";
const linkedVehicle = {
id: 7002,
reg: "EC21235",
customer_id: 12345679,
type: 53,
status: "verified",
wash_subscription: false,
addons: { list: [] },
last_order_id: null,
};
const unlinkedVehicle = {
id: 7001,
reg: "AB12345",
customer_id: null,
type: 53,
status: "verified",
wash_subscription: false,
addons: { list: [] },
last_order_id: null,
};
const bookedVehicle = {
id: 7003,
reg: "AB12345",
customer_id: 12345679,
type: 53,
status: "booked",
booking_id: 8104,
wash_subscription: false,
addons: { list: [] },
last_order_id: 54518,
};
const bookingWithItems = {
id: 8104,
customer_number: 12345679,
customer_name: "(TEST) Pleno Vognmandsforretning",
reg_1: "AB12345",
reference: "BOOKING-SERVICE-REF",
items: [
{
id: 53,
name: "Booking Forvogn",
quantity: 1,
addons: [
{
id: 64,
name: "Booking Safety Seal",
quantity: 2,
},
],
},
],
};
const LicensePlateReg1InputStub = {
name: "LicensePlateReg1Input",
emits: ["update:vehicleObject", "update:focus", "update:bookingObject", "update:bookingMatches"],
methods: {
emitUnlinkedVehicle() {
this.$emit("update:vehicleObject", { ...unlinkedVehicle });
},
emitLinkedVehicle() {
this.$emit("update:vehicleObject", { ...linkedVehicle });
},
emitLinkedVehicleTwice() {
this.$emit("update:vehicleObject", { ...linkedVehicle });
this.$emit("update:vehicleObject", { ...linkedVehicle });
},
emitBookedVehicle() {
this.$emit("update:vehicleObject", { ...bookedVehicle });
},
emitLinkedBookingMatchesTwice() {
this.$emit("update:bookingMatches", [{ ...bookingWithItems }]);
this.$emit("update:bookingMatches", [{ ...bookingWithItems }]);
},
},
template: `
<div>
<button
type="button"
data-testid="emit-unlinked-vehicle"
@click="emitUnlinkedVehicle"
>
Emit unlinked vehicle
</button>
<button
type="button"
data-testid="emit-linked-vehicle"
@click="emitLinkedVehicle"
>
Emit linked vehicle
</button>
<button
type="button"
data-testid="emit-linked-vehicle-twice"
@click="emitLinkedVehicleTwice"
>
Emit linked vehicle twice
</button>
<button
type="button"
data-testid="emit-booked-vehicle"
@click="emitBookedVehicle"
>
Emit booked vehicle
</button>
<button
type="button"
data-testid="emit-linked-booking-matches-twice"
@click="emitLinkedBookingMatchesTwice"
>
Emit linked booking matches twice
</button>
</div>
`,
};
const ExpandableContentBoxStub = {
name: "ExpandableContentBox",
props: {
expanded: {
type: Boolean,
default: false,
},
},
emits: ["update:expanded"],
template: `
<div data-testid="expandable-content-box-stub">
<slot v-if="expanded" name="expandedContent" />
<button
type="button"
data-testid="expandable-toggle"
@click="$emit('update:expanded', !expanded)"
>
Toggle
</button>
</div>
`,
};
const CustomerSearchFieldPosStub = {
name: "CustomerSearchFieldPos",
props: {
showCardPaymentButton: {
type: Boolean,
default: true,
},
},
template: `
<div data-testid="customer-search-field-pos-stub">
<button
type="button"
data-testid="customer-search-new-customer-stub"
>
Ny kunde
</button>
<button
v-if="showCardPaymentButton"
type="button"
data-testid="customer-search-card-payment-stub"
>
Vælg direkte betaling med betalingskort
</button>
</div>
`,
};
const SimpleStub = {
template: "<div />",
};
function mountForm() {
return mountWithApp(SelectVehicleFormPOS, {
global: {
stubs: {
CustomerSearchFieldPos: CustomerSearchFieldPosStub,
customerSearchFieldPos: CustomerSearchFieldPosStub,
ExpandableContentBox: ExpandableContentBoxStub,
LicensePlateReg1Input: LicensePlateReg1InputStub,
LicensePlateInput: SimpleStub,
PosNotes: SimpleStub,
OrderContentTable: SimpleStub,
OrderItemsTable: SimpleStub,
VehicleCustomerSuggestionsPos: SimpleStub,
DefaultObjectSelector: SimpleStub,
PosDesktopLastWashCard: {
template: '<div data-testid="pos-desktop-last-wash"></div>',
},
},
},
});
}
describe("SelectVehicleFormPOS", () => {
beforeEach(() => {
posState.customerId.value = null;
posState.departmentId.value = 12;
posState.notes.value = [];
posState.customerName.value = "";
posState.reference.value = "";
posState.reg1.value = "AB12345";
posState.reg2.value = "";
posState.reg3.value = "";
posState.customerAttributes.value = [];
posState.orderNotes.value = "";
posState.orderPo.value = "";
posState.orderId.value = null;
posState.selectedOrderBookingId.value = null;
posState.selectedOrderBookingPlate.value = "";
posState.skippedBookingPlate.value = "";
posState.searchAndSelectCustomer.mockClear();
posState.selectCustomer.mockClear();
posState.setSelectedOrderBookingSelection.mockClear();
posState.clearSelectedOrderBookingSelection.mockClear();
posState.skipSelectedOrderBookingSelection.mockClear();
posState.clearSelectedOrderBookingSelection.mockImplementation((options = {}) => {
if (options?.clearSkippedPlate !== false) {
posState.skippedBookingPlate.value = "";
}
});
posState.skipSelectedOrderBookingSelection.mockImplementation((plate) => {
posState.skippedBookingPlate.value = normalizeSkippedBookingPlate(plate);
});
posState.isSelectedOrderBookingSkippedForPlate.mockReset();
posState.isSelectedOrderBookingSkippedForPlate.mockImplementation(
(plate) =>
normalizeSkippedBookingPlate(plate) !== "" &&
normalizeSkippedBookingPlate(plate) === posState.skippedBookingPlate.value
);
posState.doesVehiclePlateRequireBookingSelection.mockReset();
posState.doesVehiclePlateRequireBookingSelection.mockReturnValue(false);
posState.getSelectedVehiclePlateBooking.mockReset();
posState.getSelectedVehiclePlateBooking.mockReturnValue(null);
posState.loadPendingBookings.mockClear();
posState.ensureVehiclePlateBookingsLoaded.mockReset();
posState.ensureVehiclePlateBookingsLoaded.mockResolvedValue([]);
posState.customerRequiresReferenceNumber.mockReset();
posState.customerRequiresReferenceNumber.mockReturnValue(false);
posState.isBlankPosMetadataValue.mockReset();
posState.isBlankPosMetadataValue.mockImplementation((value) => String(value ?? "").trim() === "");
});
it("shows a single inline customer selector before a vehicle is linked", () => {
const wrapper = mountForm();
expect(wrapper.findAll('[data-testid="customer-search-field-pos-stub"]')).toHaveLength(1);
expect(wrapper.findAll('[data-testid="customer-search-new-customer-stub"]')).toHaveLength(1);
expect(wrapper.findAll('[data-testid="customer-search-card-payment-stub"]')).toHaveLength(1);
expect(wrapper.find('[data-testid="expandable-content-box-stub"]').exists()).toBe(false);
});
it("keeps a single inline customer selector when an unlinked vehicle card is expanded", async () => {
const wrapper = mountForm();
await wrapper.get('[data-testid="emit-unlinked-vehicle"]').trigger("click");
await nextTick();
expect(wrapper.findAll('[data-testid="customer-search-field-pos-stub"]')).toHaveLength(1);
expect(wrapper.findAll('[data-testid="customer-search-new-customer-stub"]')).toHaveLength(1);
expect(wrapper.find('[data-testid="expandable-content-box-stub"]').exists()).toBe(true);
await wrapper.get('[data-testid="expandable-toggle"]').trigger("click");
await nextTick();
expect(wrapper.findAll('[data-testid="customer-search-field-pos-stub"]')).toHaveLength(1);
expect(wrapper.findAll('[data-testid="customer-search-new-customer-stub"]')).toHaveLength(1);
expect(wrapper.findAll('[data-testid="customer-search-card-payment-stub"]')).toHaveLength(1);
});
it("moves customer selection into the expanded other section for linked vehicles", async () => {
const wrapper = mountForm();
await wrapper.get('[data-testid="emit-linked-vehicle"]').trigger("click");
await nextTick();
expect(wrapper.findAll('[data-testid="customer-search-field-pos-stub"]')).toHaveLength(0);
await wrapper.get('[data-testid="expandable-toggle"]').trigger("click");
await nextTick();
expect(wrapper.findAll('[data-testid="customer-search-field-pos-stub"]')).toHaveLength(2);
expect(wrapper.findAll('[data-testid="customer-search-new-customer-stub"]')).toHaveLength(2);
expect(wrapper.findAll('[data-testid="customer-search-card-payment-stub"]')).toHaveLength(2);
});
it("ignores repeated identical linked vehicle emissions", async () => {
const wrapper = mountForm();
await wrapper.get('[data-testid="emit-linked-vehicle-twice"]').trigger("click");
await nextTick();
expect(wrapper.findAll('[data-testid="customer-search-field-pos-stub"]')).toHaveLength(0);
expect(posState.searchAndSelectCustomer).not.toHaveBeenCalled();
});
it("does not re-apply identical booking matches", async () => {
const wrapper = mountForm();
await wrapper.get('[data-testid="emit-linked-booking-matches-twice"]').trigger("click");
await flushPromises();
expect(posState.setSelectedOrderBookingSelection).toHaveBeenCalledTimes(1);
expect(posState.searchAndSelectCustomer).toHaveBeenCalledTimes(1);
expect(posState.searchAndSelectCustomer).toHaveBeenCalledWith(12345679);
});
it("hides a booked desktop vehicle summary after continuing without booking", async () => {
const wrapper = mountForm();
await wrapper.get('[data-testid="emit-booked-vehicle"]').trigger("click");
await nextTick();
expect(wrapper.find('[data-testid="pos-desktop-vehicle-summary"]').exists()).toBe(true);
await wrapper.vm.skipBookingChoice({
committed: false,
});
await nextTick();
expect(posState.skipSelectedOrderBookingSelection).toHaveBeenCalledWith("AB12345");
expect(wrapper.find('[data-testid="pos-desktop-vehicle-summary"]').exists()).toBe(false);
});
it("keeps normal desktop vehicle summaries visible after a skipped booking plate is reselected", async () => {
const wrapper = mountForm();
await wrapper.get('[data-testid="emit-booked-vehicle"]').trigger("click");
await nextTick();
await wrapper.vm.skipBookingChoice({
committed: false,
});
await nextTick();
expect(wrapper.find('[data-testid="pos-desktop-vehicle-summary"]').exists()).toBe(false);
await wrapper.get('[data-testid="emit-unlinked-vehicle"]').trigger("click");
await nextTick();
expect(wrapper.find('[data-testid="pos-desktop-vehicle-summary"]').exists()).toBe(true);
});
it("shows selected booking services and hides desktop last wash", async () => {
const wrapper = mountForm();
await wrapper.get('[data-testid="emit-booked-vehicle"]').trigger("click");
await nextTick();
expect(wrapper.find('[data-testid="pos-desktop-last-wash"]').exists()).toBe(true);
await wrapper.vm.applyBookingChoice(bookingWithItems, {
committed: false,
});
await nextTick();
const vehicleSummary = wrapper.get('[data-testid="pos-desktop-vehicle-summary"]');
expect(vehicleSummary.text()).toContain("Booking Forvogn");
expect(vehicleSummary.text()).toContain("Booking Safety Seal");
expect(vehicleSummary.text()).not.toContain("Ingen abonnement- eller tilvalgsdata");
expect(wrapper.find('[data-testid="pos-desktop-last-wash"]').exists()).toBe(false);
});
it("renders the reference autocomplete with the legacy input id and warning state", async () => {
posState.customerRequiresReferenceNumber.mockReturnValue(true);
const wrapper = mountForm();
await nextTick();
await nextTick();
const referenceControl = wrapper.get('[data-testid="pos-desktop-step-1-reference-control"]');
const referenceInput = wrapper.get("#reference");
expect(wrapper.get('[data-testid="pos-reference-autocomplete"]').exists()).toBe(true);
expect(referenceControl.attributes("data-warning-state")).toBe("danger");
expect(referenceInput.attributes("aria-invalid")).toBe("true");
expect(wrapper.get('[data-testid="pos-desktop-step-1-reference-warning-icon"]').exists()).toBe(true);
});
});