540 lines
18 KiB
JavaScript
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);
|
|
});
|
|
});
|