// @vitest-environment jsdom
import { mount } from "@vue/test-utils";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
class ResizeObserver {
observe() {}
unobserve() {}
disconnect() {}
}
global.ResizeObserver = ResizeObserver;
import ActionSettingsWheelButton from "@/components/displays/buttons/ActionSettingsWheelButton.vue";
import { SessionUser } from "@/components/session/token/SessionUser.vue";
import { CUSTOMER_RULE_DEFINITIONS } from "@/features/customer/customerRuleRegistry.js";
import { createTestI18n } from "./helpers/mountWithApp.js";
const swalFireMock = vi.hoisted(() => vi.fn(() => Promise.resolve({ isConfirmed: true, value: "Needs review" })));
const getUserIdMock = vi.hoisted(() => vi.fn(() => Promise.resolve({ data: { data: { user_id: 777 } } })));
const listCustomerAttributesMock = vi.hoisted(() => vi.fn(() => Promise.resolve({ data: { data: [] } })));
const createCustomerAttributeMock = vi.hoisted(() => vi.fn(() => Promise.resolve({ data: { data: null } })));
const deleteCustomerAttributeMock = vi.hoisted(() => vi.fn(() => Promise.resolve({ data: { data: null } })));
vi.mock("sweetalert2", () => ({
default: {
fire: swalFireMock,
},
}));
vi.mock("@/features/customer/customerAttributeService.js", () => ({
buildCustomerAttributeTargetPayload: (target = {}) => {
const userId = Number.parseInt(String(target.userId ?? target.user_id ?? ""), 10);
if (Number.isInteger(userId) && userId > 0) {
return { user_id: userId };
}
const customerNumber = Number.parseInt(String(target.customerNumber ?? target.customer_number ?? ""), 10);
if (Number.isInteger(customerNumber) && customerNumber > 0) {
return { customer_number: customerNumber };
}
return {};
},
createCustomerAttribute: createCustomerAttributeMock,
deleteCustomerAttribute: deleteCustomerAttributeMock,
extractCustomerAttributesData: (response) => response?.data?.data ?? response?.data ?? response ?? [],
listCustomerAttributes: listCustomerAttributesMock,
}));
vi.mock("@/components/session/token/SessionUser.vue", () => ({
SessionUser: {
adminUser: {
customers: {
fromCustomerNumber: {
getUserId: getUserIdMock,
},
},
},
request: vi.fn(() => Promise.resolve()),
canAccessAdmin: vi.fn(() => false),
canAccessSuperUser: vi.fn(() => true),
canAccessUser: vi.fn(() => false),
canAccessCustomerFeature: vi.fn(() => true),
hasPermission: vi.fn(() => true),
functions: {
ucFirst: (value) => value,
parseErrorMessage: () => "error",
device: {
isMobile: vi.fn(() => false),
},
redirectTo: {
department: vi.fn(),
superUser: vi.fn(),
user: vi.fn(),
},
},
superUser: {
intimidate: {
intimidateUser: vi.fn(() => Promise.resolve()),
getImpersonationToken: vi.fn(() => Promise.resolve("token")),
showImpersonationQRCode: vi.fn(() => Promise.resolve({ value: "qr" })),
},
},
objects: {
order_bookings: {
meta: {
labels: {
single: "Booking",
multiple: "Bookings",
},
},
showEditObjectFieldForm: vi.fn(() => Promise.resolve()),
functions: {
resendBookingCompletionConfirmation: vi.fn(() => Promise.resolve()),
resendBookingConfirmation: vi.fn(() => Promise.resolve()),
showDeleteConfirmationModal: vi.fn(() => Promise.resolve()),
},
},
department_lanes: {
meta: {
labels: {
single: "Lane",
},
},
functions: {
forceEnableMachine: vi.fn(() => Promise.resolve()),
forceDisableMachine: vi.fn(() => Promise.resolve()),
},
},
department_gates: {
showCreateObjectForm: vi.fn(() => Promise.resolve()),
},
department_relays: {
showCreateObjectForm: vi.fn(() => Promise.resolve()),
},
orders: {
meta: {
labels: {
single: "Order",
},
},
functions: {
fetchAttachments: vi.fn(() => Promise.resolve([])),
assignDraftCustomer: vi.fn(() => Promise.resolve()),
get_department_id: vi.fn(() => Promise.resolve(1)),
removeAttachment: vi.fn(() => Promise.resolve({ success: true })),
resendWashCertificate: vi.fn(() => Promise.resolve()),
showAttachWashCertificateForm: vi.fn(() => Promise.resolve()),
showChangeCustomerForm: vi.fn(() => Promise.resolve()),
showChangeInvoiceCollectionForm: vi.fn(() => Promise.resolve()),
showDeleteConfirmationModal: vi.fn(() => Promise.resolve()),
downloadAttachment: vi.fn(() => Promise.resolve()),
},
},
collectedOrderInvoices: {
meta: {
title: "Invoices",
},
add: vi.fn(() => Promise.resolve({ data: { data: { id: 901 } } })),
functions: {
download: vi.fn(() => Promise.resolve()),
},
},
vehicles: {
meta: {
labels: {
single: "Vehicle",
multiple: "Vehicles",
},
},
functions: {
showDeleteObjectForm: vi.fn(() => Promise.resolve()),
},
},
global: {
language: {
customer: "Customer",
},
},
subuser_grants: {
functions: {
showPermissionEditForm: vi.fn(() => Promise.resolve()),
},
delete: vi.fn(() => Promise.resolve()),
},
},
},
}));
const CUSTOMER_RULE_TRANSLATION_KEYS = CUSTOMER_RULE_DEFINITIONS.flatMap((rule) => [
rule.labelKey,
rule.descriptionKey,
]);
const SETTINGS_WHEEL_TRANSLATION_KEYS = [
"admin.pos.settings_wheel.associate_order",
"admin.pos.settings_wheel.accept_self_serve_wash",
"admin.pos.settings_wheel.accept_self_serve_wash_confirm",
"admin.pos.settings_wheel.accept_self_serve_wash_success",
"admin.pos.settings_wheel.attach_wash_certificate",
"admin.pos.settings_wheel.attached_files",
"admin.pos.settings_wheel.booking",
"admin.pos.settings_wheel.change_association",
"admin.pos.settings_wheel.change_customer",
"admin.pos.settings_wheel.change_invoice_collection",
"admin.pos.settings_wheel.change_order",
"admin.pos.settings_wheel.change_password",
"admin.pos.settings_wheel.close",
"admin.pos.settings_wheel.copy_link",
"admin.pos.settings_wheel.delete_booking",
"admin.pos.settings_wheel.delete_order",
"admin.pos.settings_wheel.delete_user",
"admin.pos.settings_wheel.delete_vehicle",
"admin.pos.settings_wheel.download_invoice",
"admin.pos.settings_wheel.edit_permissions",
"admin.pos.settings_wheel.email_notifications_section",
"admin.pos.settings_wheel.enter_password",
"admin.pos.settings_wheel.error",
"admin.pos.settings_wheel.error_changing_password",
"admin.pos.settings_wheel.error_downloading_attachment",
"admin.pos.settings_wheel.link_copied",
"admin.pos.settings_wheel.login_as_customer",
"admin.pos.settings_wheel.login_as_user",
"admin.pos.settings_wheel.login_as_user_qr",
"admin.pos.settings_wheel.manage_vehicle",
"admin.pos.settings_wheel.mark_as_completed",
"admin.pos.settings_wheel.no_actions_defined",
"admin.pos.settings_wheel.customer_section",
"admin.pos.settings_wheel.gates_section",
"admin.pos.settings_wheel.open_attached_file",
"admin.pos.settings_wheel.open_fleet_landing",
"admin.pos.settings_wheel.open_gateways_tab",
"admin.pos.settings_wheel.open_gates_tab",
"admin.pos.settings_wheel.open_hardware_workspace_lanes",
"admin.pos.settings_wheel.open_legacy_gates",
"admin.pos.settings_wheel.open_legacy_relays",
"admin.pos.settings_wheel.open_legacy_self_serve",
"admin.pos.settings_wheel.open_primary_gateway",
"admin.pos.settings_wheel.rules_section",
"admin.pos.settings_wheel.open_relays_tab",
"admin.pos.settings_wheel.open_studio",
"admin.pos.settings_wheel.password_changed",
"admin.pos.settings_wheel.password_changed_text",
"admin.pos.settings_wheel.please_enter_password",
"admin.pos.settings_wheel.relays_section",
"admin.pos.settings_wheel.resend_booking_completion_confirmation",
"admin.pos.settings_wheel.resend_booking_completion_confirmation_error",
"admin.pos.settings_wheel.resend_booking_completion_confirmation_success",
"admin.pos.settings_wheel.resend_booking_confirmation",
"admin.pos.settings_wheel.resend_booking_confirmation_error",
"admin.pos.settings_wheel.resend_booking_confirmation_success",
"admin.pos.settings_wheel.resend_wash_certificate",
"admin.pos.settings_wheel.resend_wash_certificate_error",
"admin.pos.settings_wheel.resend_wash_certificate_success",
"admin.pos.settings_wheel.scan_qr_to_login",
"admin.pos.settings_wheel.self_serve_customer",
"admin.pos.settings_wheel.self_serve_driver",
"admin.pos.settings_wheel.self_serve_elapsed",
"admin.pos.settings_wheel.self_serve_wash_attachment",
"admin.pos.settings_wheel.self_serve_wash_attachment_for_customer",
"admin.pos.settings_wheel.self_serve_studio_section",
"admin.pos.settings_wheel.shortcuts_section",
"admin.pos.settings_wheel.shortcut_orders",
"admin.pos.settings_wheel.shortcut_other",
"admin.pos.settings_wheel.shortcut_overview",
"admin.pos.settings_wheel.shortcut_pricing",
"admin.pos.settings_wheel.shortcut_vehicles",
"admin.pos.settings_wheel.show_customer",
"admin.pos.settings_wheel.show_qr_code",
"admin.pos.settings_wheel.add_gate",
"admin.pos.settings_wheel.add_flag",
"admin.pos.settings_wheel.add_flag_customer",
"admin.pos.settings_wheel.add_flag_invoice_collection",
"admin.pos.settings_wheel.add_flag_order",
"admin.pos.settings_wheel.add_flag_order_customer",
"admin.pos.settings_wheel.add_flag_order_item",
"admin.pos.settings_wheel.add_flag_order_item_notes",
"admin.pos.settings_wheel.add_flag_order_item_price",
"admin.pos.settings_wheel.add_flag_order_item_quantity",
"admin.pos.settings_wheel.add_flag_order_item_reference",
"admin.pos.settings_wheel.add_flag_order_notes",
"admin.pos.settings_wheel.add_flag_order_po",
"admin.pos.settings_wheel.add_flag_order_reference",
"admin.pos.settings_wheel.add_flag_reason_placeholder",
"admin.pos.settings_wheel.add_flag_reason_required",
"admin.pos.settings_wheel.add_flag_target",
"admin.pos.settings_wheel.add_relay",
"admin.pos.settings_wheel.flags_section",
"admin.pos.settings_wheel.gateways_section",
"admin.pos.settings_wheel.view_booking_new_tab",
"admin.pos.settings_wheel.view_customer_new_tab",
"admin.pos.settings_wheel.view_invoice_collection_new_tab",
"admin.pos.settings_wheel.view_lane_new_tab",
"admin.pos.settings_wheel.view_lane_setup_new_tab",
"admin.pos.settings_wheel.view_order_new_tab",
"admin.pos.settings_wheel.view_vehicle_new_tab",
"admin.pos.attachments_no_preview",
"admin.pos.attachments_office_preview_unavailable",
"admin.pos.drafts_assignment.error",
"admin.pos.drafts_assignment.invoice_collection_empty",
"admin.pos.drafts_assignment.new_collection_description",
"admin.pos.drafts_assignment.new_collection_name",
"common.cancel",
"global.delete",
"global.download",
"global.loading",
"global.preview",
"global.print",
"common.confirm",
"pos.license_plate",
"superuser.department_lane.force_disable_machine",
"superuser.department_lane.force_enable_machine",
...CUSTOMER_RULE_TRANSLATION_KEYS,
];
const createKeyValueMessages = (keys) => {
const enMessages = {};
for (const key of keys) {
const parts = key.split(".");
let cursor = enMessages;
for (let i = 0; i < parts.length; i += 1) {
const part = parts[i];
const isLeaf = i === parts.length - 1;
if (isLeaf) {
cursor[part] = key;
} else {
cursor[part] = cursor[part] ?? {};
cursor = cursor[part];
}
}
}
return { en: enMessages };
};
const i18n = createTestI18n(createKeyValueMessages(SETTINGS_WHEEL_TRANSLATION_KEYS));
const flushMicrotasks = async () => {
await Promise.resolve();
await Promise.resolve();
};
const flushDropdownLayout = async () => {
for (let index = 0; index < 8; index += 1) {
await Promise.resolve();
}
};
const createRect = ({ left = 0, top = 0, width = 0, height = 0 }) => ({
x: left,
y: top,
top,
left,
right: left + width,
bottom: top + height,
width,
height,
toJSON() {
return this;
},
});
const setDesktopFlyoutViewport = () => {
Object.defineProperty(window, "innerWidth", {
configurable: true,
writable: true,
value: 1900,
});
Object.defineProperty(window, "innerHeight", {
configurable: true,
writable: true,
value: 900,
});
window.matchMedia = vi.fn().mockImplementation(() => ({
matches: true,
media: "",
onchange: null,
addListener: vi.fn(),
removeListener: vi.fn(),
addEventListener: vi.fn(),
removeEventListener: vi.fn(),
dispatchEvent: vi.fn(),
}));
};
const setCompactViewport = () => {
Object.defineProperty(window, "innerWidth", {
configurable: true,
writable: true,
value: 1280,
});
Object.defineProperty(window, "innerHeight", {
configurable: true,
writable: true,
value: 900,
});
window.matchMedia = vi.fn().mockImplementation(() => ({
matches: false,
media: "",
onchange: null,
addListener: vi.fn(),
removeListener: vi.fn(),
addEventListener: vi.fn(),
removeEventListener: vi.fn(),
dispatchEvent: vi.fn(),
}));
};
const mockMenuGeometry = () =>
vi.spyOn(HTMLElement.prototype, "getBoundingClientRect").mockImplementation(function getBoundingClientRectMock() {
if (this.classList?.contains("dropdown-trigger")) {
return {
x: 1750,
y: 96,
top: 96,
left: 1750,
right: 1850,
bottom: 136,
width: 100,
height: 40,
toJSON() {
return this;
},
};
}
return {
x: 1320,
y: 136,
top: 136,
left: 1320,
right: 1850,
bottom: 560,
width: 530,
height: 424,
toJSON() {
return this;
},
};
});
const mockDropdownPlacementGeometry = ({
contentHeight = 180,
contentWidth = 240,
triggerHeight = 40,
triggerLeft = 880,
triggerTop = 520,
triggerWidth = 80,
} = {}) => {
let currentContentHeight = contentHeight;
const originalScrollHeightDescriptor = Object.getOwnPropertyDescriptor(HTMLElement.prototype, "scrollHeight");
Object.defineProperty(HTMLElement.prototype, "scrollHeight", {
configurable: true,
get() {
if (this.classList?.contains("dropdown-content")) {
return currentContentHeight;
}
if (originalScrollHeightDescriptor?.get) {
return originalScrollHeightDescriptor.get.call(this);
}
return 0;
},
});
const rectSpy = vi.spyOn(HTMLElement.prototype, "getBoundingClientRect").mockImplementation(function rectMock() {
if (this.classList?.contains("dropdown-trigger")) {
return createRect({
left: triggerLeft,
top: triggerTop,
width: triggerWidth,
height: triggerHeight,
});
}
if (this.classList?.contains("dropdown-content")) {
const dropdown = this.closest(".dropdown");
const menu = this.closest(".dropdown-menu");
const fixedTop = Number.parseFloat(menu?.style?.top || "");
const maxHeight = Number.parseFloat(this.style?.maxHeight || "");
const height = Number.isFinite(maxHeight) ? Math.min(currentContentHeight, maxHeight) : currentContentHeight;
const top = Number.isFinite(fixedTop)
? fixedTop
: dropdown?.classList.contains("is-up")
? triggerTop - height
: triggerTop + triggerHeight;
return createRect({
left: triggerLeft + triggerWidth - contentWidth,
top,
width: contentWidth,
height,
});
}
if (this.classList?.contains("action-wheel-clip-host")) {
return createRect({
left: 0,
top: 480,
width: 1280,
height: 120,
});
}
return createRect({
left: 0,
top: 0,
width: 1280,
height: 900,
});
});
return {
restore() {
rectSpy.mockRestore();
if (originalScrollHeightDescriptor) {
Object.defineProperty(HTMLElement.prototype, "scrollHeight", originalScrollHeightDescriptor);
} else {
delete HTMLElement.prototype.scrollHeight;
}
},
setContentHeight(nextHeight) {
currentContentHeight = nextHeight;
},
};
};
const mountDesktopFlyoutButton = (props = {}) => {
setDesktopFlyoutViewport();
return mount(ActionSettingsWheelButton, {
props: {
order_id: 42,
refreshFunction: vi.fn(() => Promise.resolve()),
...props,
},
slots: {
actions: "",
},
global: {
plugins: [i18n],
stubs: {
CustomerModal: { template: "
" },
},
},
});
};
const mountPositionedFlatDropdownButton = (options = {}) => {
setCompactViewport();
return mount(ActionSettingsWheelButton, {
props: {
order_id: null,
refreshFunction: vi.fn(() => Promise.resolve()),
...(options.props || {}),
},
slots: {
actions:
options.actions || '',
},
attachTo: options.attachTo,
global: {
plugins: [i18n],
stubs: {
CustomerModal: { template: "" },
},
},
});
};
const mountFlatDropdownButton = (props = {}) => {
setCompactViewport();
return mount(ActionSettingsWheelButton, {
props: {
order_id: 42,
refreshFunction: vi.fn(() => Promise.resolve()),
...props,
},
slots: {
actions: "",
},
global: {
plugins: [i18n],
stubs: {
CustomerModal: { template: "" },
},
},
});
};
const originalFetch = global.fetch;
const originalCreateObjectURL = global.URL.createObjectURL;
const originalRevokeObjectURL = global.URL.revokeObjectURL;
const originalMatchMedia = window.matchMedia;
describe("ActionSettingsWheelButton", () => {
beforeEach(() => {
getUserIdMock.mockClear();
listCustomerAttributesMock.mockReset();
listCustomerAttributesMock.mockResolvedValue({
data: {
data: [{ id: 1, customer_number: 123456, attribute: "invoiceAllOrdersIndividually" }],
},
});
createCustomerAttributeMock.mockReset();
createCustomerAttributeMock.mockResolvedValue({ data: { data: null } });
deleteCustomerAttributeMock.mockReset();
deleteCustomerAttributeMock.mockResolvedValue({ data: { data: null } });
swalFireMock.mockReset();
swalFireMock.mockResolvedValue({ isConfirmed: true, value: "Needs review" });
SessionUser.request.mockReset();
SessionUser.request.mockResolvedValue({ data: { data: { summary: { primary_gateway: { id: 701 } } } } });
SessionUser.canAccessCustomerFeature.mockReset();
SessionUser.canAccessCustomerFeature.mockReturnValue(true);
SessionUser.hasPermission.mockReset();
SessionUser.hasPermission.mockImplementation((permission) =>
["add_customer_attribute", "delete_customer_attribute", "list_customer_attributes", "superuser"].includes(
permission
)
);
SessionUser.objects.orders.functions.showChangeInvoiceCollectionForm.mockClear();
SessionUser.objects.orders.functions.fetchAttachments.mockReset();
SessionUser.objects.orders.functions.fetchAttachments.mockResolvedValue([]);
SessionUser.objects.orders.functions.assignDraftCustomer.mockReset();
SessionUser.objects.orders.functions.assignDraftCustomer.mockResolvedValue({ data: { data: null } });
SessionUser.objects.orders.functions.downloadAttachment.mockReset();
SessionUser.objects.orders.functions.downloadAttachment.mockResolvedValue("https://cdn.example.test/attachment");
SessionUser.objects.orders.functions.removeAttachment.mockReset();
SessionUser.objects.orders.functions.removeAttachment.mockResolvedValue({ success: true });
SessionUser.objects.orders.functions.resendWashCertificate.mockReset();
SessionUser.objects.orders.functions.resendWashCertificate.mockResolvedValue({});
SessionUser.objects.order_bookings.functions.resendBookingConfirmation.mockReset();
SessionUser.objects.order_bookings.functions.resendBookingConfirmation.mockResolvedValue({});
SessionUser.objects.order_bookings.functions.resendBookingCompletionConfirmation.mockReset();
SessionUser.objects.order_bookings.functions.resendBookingCompletionConfirmation.mockResolvedValue({});
SessionUser.objects.department_gates.showCreateObjectForm.mockClear();
SessionUser.objects.department_relays.showCreateObjectForm.mockClear();
SessionUser.objects.collectedOrderInvoices.add.mockReset();
SessionUser.objects.collectedOrderInvoices.add.mockResolvedValue({ data: { data: { id: 901 } } });
SessionUser.objects.vehicles.functions.showDeleteObjectForm.mockReset();
SessionUser.objects.vehicles.functions.showDeleteObjectForm.mockResolvedValue({});
SessionUser.functions.redirectTo.department.mockClear();
SessionUser.functions.redirectTo.superUser.mockClear();
global.fetch = vi.fn(() =>
Promise.resolve({
ok: true,
blob: () => Promise.resolve(new Blob(["preview"], { type: "application/pdf" })),
})
);
global.URL.createObjectURL = vi.fn((blob) => `blob:${blob.type}:${Date.now()}`);
global.URL.revokeObjectURL = vi.fn();
});
afterEach(() => {
global.fetch = originalFetch;
global.URL.createObjectURL = originalCreateObjectURL;
global.URL.revokeObjectURL = originalRevokeObjectURL;
window.matchMedia = originalMatchMedia;
vi.restoreAllMocks();
});
it("marks the trigger as active while its dropdown is open", async () => {
const wrapper = mountFlatDropdownButton();
await flushMicrotasks();
const trigger = wrapper.get(".action-settings-wheel-trigger");
expect(trigger.classes()).not.toContain("action-settings-wheel-trigger--active");
expect(trigger.attributes("aria-expanded")).toBe("false");
await trigger.trigger("click");
await flushMicrotasks();
expect(trigger.classes()).toContain("action-settings-wheel-trigger--active");
expect(trigger.attributes("aria-expanded")).toBe("true");
wrapper.unmount();
});
it("keeps only the latest opened action settings wheel active", async () => {
setCompactViewport();
const wrapper = mount(
{
components: { ActionSettingsWheelButton },
template: `
`,
},
{
global: {
plugins: [i18n],
stubs: {
CustomerModal: { template: "" },
},
},
}
);
await flushMicrotasks();
const triggers = wrapper.findAll(".action-settings-wheel-trigger");
await triggers[0].trigger("click");
await flushMicrotasks();
expect(triggers[0].attributes("aria-expanded")).toBe("true");
expect(triggers[0].classes()).toContain("action-settings-wheel-trigger--active");
await triggers[1].trigger("click");
await flushMicrotasks();
expect(triggers[0].attributes("aria-expanded")).toBe("false");
expect(triggers[0].classes()).not.toContain("action-settings-wheel-trigger--active");
expect(triggers[1].attributes("aria-expanded")).toBe("true");
expect(triggers[1].classes()).toContain("action-settings-wheel-trigger--active");
wrapper.unmount();
});
it("uses the same vertical side when the dropdown is reopened with unchanged geometry", async () => {
const geometry = mockDropdownPlacementGeometry({
contentHeight: 420,
triggerTop: 520,
});
const wrapper = mountPositionedFlatDropdownButton();
await flushDropdownLayout();
const trigger = wrapper.get(".action-settings-wheel-trigger");
await trigger.trigger("click");
await flushDropdownLayout();
expect(wrapper.get(".dropdown").classes()).toContain("is-up");
await trigger.trigger("click");
await flushDropdownLayout();
expect(wrapper.get(".dropdown").classes()).not.toContain("is-active");
await trigger.trigger("click");
await flushDropdownLayout();
expect(wrapper.get(".dropdown").classes()).toContain("is-up");
wrapper.unmount();
geometry.restore();
});
it("keeps the initially selected side when the menu grows after opening", async () => {
const geometry = mockDropdownPlacementGeometry({
contentHeight: 120,
triggerTop: 520,
});
const wrapper = mountPositionedFlatDropdownButton();
await flushDropdownLayout();
await wrapper.get(".action-settings-wheel-trigger").trigger("click");
await flushDropdownLayout();
expect(wrapper.get(".dropdown").classes()).not.toContain("is-up");
geometry.setContentHeight(420);
window.dispatchEvent(new Event("resize"));
await flushDropdownLayout();
expect(wrapper.get(".dropdown").classes()).not.toContain("is-up");
expect(wrapper.get(".dropdown-content").element.style.maxHeight).toBe("328px");
wrapper.unmount();
geometry.restore();
});
it("keeps fixed-position fallback on the chosen side and clears stale content top", async () => {
const host = document.createElement("div");
host.className = "action-wheel-clip-host";
host.style.overflow = "hidden";
document.body.appendChild(host);
const geometry = mockDropdownPlacementGeometry({
contentHeight: 420,
triggerTop: 520,
});
const wrapper = mountPositionedFlatDropdownButton({ attachTo: host });
await flushDropdownLayout();
await wrapper.get(".action-settings-wheel-trigger").trigger("click");
await flushDropdownLayout();
const dropdownContent = wrapper.get(".dropdown-content").element;
const dropdownMenu = wrapper.get(".dropdown-menu").element;
expect(wrapper.get(".dropdown").classes()).toContain("is-up");
expect(dropdownMenu.style.position).toBe("fixed");
expect(dropdownMenu.style.top).toBe("100px");
expect(dropdownContent.style.top).toBe("");
dropdownContent.style.top = "24px";
window.dispatchEvent(new Event("resize"));
await flushDropdownLayout();
expect(wrapper.get(".dropdown").classes()).toContain("is-up");
expect(dropdownMenu.style.position).toBe("fixed");
expect(dropdownMenu.style.top).toBe("100px");
expect(dropdownContent.style.top).toBe("");
wrapper.unmount();
geometry.restore();
host.remove();
});
it("resolves customer user id and reloads customer rules only when the target changes in direct mode", async () => {
const wrapper = mount(ActionSettingsWheelButton, {
props: {
displayActionsDirectly: true,
customer_number: 123456,
order_id: null,
},
global: {
plugins: [i18n],
stubs: {
ActionSettingsWheelItem: { template: "" },
ActionSettingsWheelItemLabel: { template: "" },
CustomerModal: { template: "" },
},
},
});
await flushMicrotasks();
expect(getUserIdMock).toHaveBeenCalledTimes(1);
expect(getUserIdMock).toHaveBeenLastCalledWith(123456);
expect(listCustomerAttributesMock).toHaveBeenCalledTimes(1);
expect(listCustomerAttributesMock).toHaveBeenLastCalledWith({ customer_number: 123456 });
await wrapper.setProps({ icon: "fas fa-ellipsis-v" });
await flushMicrotasks();
expect(getUserIdMock).toHaveBeenCalledTimes(1);
expect(listCustomerAttributesMock).toHaveBeenCalledTimes(1);
await wrapper.setProps({ customer_number: 654321 });
await flushMicrotasks();
expect(getUserIdMock).toHaveBeenCalledTimes(2);
expect(getUserIdMock).toHaveBeenLastCalledWith(654321);
expect(listCustomerAttributesMock).toHaveBeenCalledTimes(2);
expect(listCustomerAttributesMock).toHaveBeenLastCalledWith({ customer_number: 654321 });
});
it("does not perform customer lookup when explicit user id is provided and uses the user target for rules", async () => {
mount(ActionSettingsWheelButton, {
props: {
displayActionsDirectly: true,
user_id: 77,
customer_number: null,
},
global: {
plugins: [i18n],
stubs: {
ActionSettingsWheelItem: { template: "" },
ActionSettingsWheelItemLabel: { template: "" },
CustomerModal: { template: "" },
},
},
});
await flushMicrotasks();
expect(getUserIdMock).not.toHaveBeenCalled();
expect(listCustomerAttributesMock).toHaveBeenCalledTimes(1);
expect(listCustomerAttributesMock).toHaveBeenCalledWith({ user_id: 77 });
});
it("lazy loads customer rules when a flat dropdown opens and toggles them by customer number", async () => {
const wrapper = mountFlatDropdownButton({
order_id: null,
customer_number: 123456,
reg_1: null,
reg_2: null,
});
await flushMicrotasks();
expect(getUserIdMock).toHaveBeenCalledTimes(1);
expect(listCustomerAttributesMock).not.toHaveBeenCalled();
await wrapper.find(".dropdown-trigger button").trigger("click");
await flushMicrotasks();
await flushMicrotasks();
expect(listCustomerAttributesMock).toHaveBeenCalledTimes(1);
expect(listCustomerAttributesMock).toHaveBeenCalledWith({ customer_number: 123456 });
expect(wrapper.text()).toContain("admin.pos.settings_wheel.customer_section");
expect(wrapper.text()).toContain("admin.pos.settings_wheel.rules_section");
expect(wrapper.text()).toContain("admin.pos.settings_wheel.shortcuts_section");
const ruleToggle = wrapper.get(
'[data-testid="action-settings-wheel-toggle-customer-rule-invoiceAllOrdersIndividually"]'
);
expect(ruleToggle.attributes("aria-pressed")).toBe("true");
await ruleToggle.trigger("click");
await flushMicrotasks();
expect(deleteCustomerAttributeMock).toHaveBeenCalledTimes(1);
expect(deleteCustomerAttributeMock).toHaveBeenCalledWith(
{ customer_number: 123456 },
"invoiceAllOrdersIndividually"
);
expect(
wrapper
.get('[data-testid="action-settings-wheel-toggle-customer-rule-invoiceAllOrdersIndividually"]')
.attributes("aria-pressed")
).toBe("false");
});
it("renders customer, rules, and shortcuts sections in direct mode and enables disabled rules", async () => {
listCustomerAttributesMock.mockResolvedValueOnce({ data: { data: [] } });
const wrapper = mount(ActionSettingsWheelButton, {
props: {
displayActionsDirectly: true,
order_id: null,
customer_number: 123456,
reg_1: null,
reg_2: null,
},
global: {
plugins: [i18n],
stubs: {
CustomerModal: { template: "" },
},
},
});
await flushMicrotasks();
await flushMicrotasks();
expect(wrapper.text()).toContain("admin.pos.settings_wheel.customer_section");
expect(wrapper.text()).toContain("admin.pos.settings_wheel.rules_section");
expect(wrapper.text()).toContain("admin.pos.settings_wheel.shortcuts_section");
expect(wrapper.text()).toContain("admin.pos.settings_wheel.shortcut_overview");
expect(wrapper.text()).toContain("admin.pos.settings_wheel.shortcut_orders");
const ruleToggle = wrapper.get(
'[data-testid="action-settings-wheel-toggle-customer-rule-invoiceAllOrdersIndividually"]'
);
expect(ruleToggle.attributes("aria-pressed")).toBe("false");
await ruleToggle.trigger("click");
await flushMicrotasks();
expect(createCustomerAttributeMock).toHaveBeenCalledTimes(1);
expect(createCustomerAttributeMock).toHaveBeenCalledWith(
{ customer_number: 123456 },
"invoiceAllOrdersIndividually"
);
expect(
wrapper
.get('[data-testid="action-settings-wheel-toggle-customer-rule-invoiceAllOrdersIndividually"]')
.attributes("aria-pressed")
).toBe("true");
});
it("renders customer, rules, and shortcuts sections in desktop flyout mode", async () => {
const geometrySpy = mockMenuGeometry();
const wrapper = mountDesktopFlyoutButton({
order_id: null,
customer_number: 123456,
reg_1: null,
reg_2: null,
});
await flushMicrotasks();
await wrapper.find(".dropdown-trigger button").trigger("click");
await flushMicrotasks();
await flushMicrotasks();
expect(wrapper.find('[data-testid="action-settings-wheel-section-customer"]').exists()).toBe(true);
expect(wrapper.find('[data-testid="action-settings-wheel-section-rules"]').exists()).toBe(true);
expect(wrapper.find('[data-testid="action-settings-wheel-section-shortcuts"]').exists()).toBe(true);
await wrapper.get('[data-testid="action-settings-wheel-section-rules"]').trigger("mouseenter");
await flushMicrotasks();
const ruleToggle = wrapper.get(
'[data-testid="action-settings-wheel-toggle-customer-rule-invoiceAllOrdersIndividually"]'
);
expect(ruleToggle.attributes("aria-pressed")).toBe("true");
await ruleToggle.trigger("click");
await flushMicrotasks();
expect(deleteCustomerAttributeMock).toHaveBeenCalledWith(
{ customer_number: 123456 },
"invoiceAllOrdersIndividually"
);
geometrySpy.mockRestore();
});
it("passes metadata appearance to label rows in external menu sections", async () => {
const wrapper = mountFlatDropdownButton({
order_id: null,
menuSections: [
{
key: "driver-details",
label: "Details",
items: [
{
key: "driver-id",
type: "label",
icon: "fas fa-hashtag",
label: "Driver ID",
detailValue: "51",
appearance: "metadata",
testId: "driver-id-row",
},
],
},
],
});
await wrapper.find(".dropdown-trigger button").trigger("click");
await flushMicrotasks();
const row = wrapper.get('[data-testid="driver-id-row"]');
expect(row.classes()).toContain("dropdown-item-label--metadata");
expect(row.text()).toContain("Driver ID");
expect(row.text()).toContain("51");
});
it("does not render standalone booking completion for unlinked bookings", async () => {
const wrapper = mountFlatDropdownButton({
order_id: null,
order_booking_id: 123,
department_id: 1,
customer_number: 123456,
});
await flushMicrotasks();
await wrapper.find(".dropdown-trigger button").trigger("click");
await flushMicrotasks();
expect(wrapper.text()).toContain("admin.pos.settings_wheel.booking");
expect(wrapper.text()).toContain("admin.pos.settings_wheel.view_booking_new_tab");
expect(wrapper.text()).not.toContain("admin.pos.settings_wheel.mark_as_completed");
});
it("hides email notifications when the order has no booking or wash certificate", async () => {
const wrapper = mountFlatDropdownButton({
order_id: 42,
order_booking_id: null,
});
await flushMicrotasks();
await wrapper.find(".dropdown-trigger button").trigger("click");
await flushMicrotasks();
expect(wrapper.text()).not.toContain("admin.pos.settings_wheel.email_notifications_section");
expect(wrapper.text()).not.toContain("admin.pos.settings_wheel.resend_booking_confirmation");
expect(wrapper.text()).not.toContain("admin.pos.settings_wheel.resend_wash_certificate");
});
it("shows booking email notification actions for booking-only orders", async () => {
const wrapper = mountFlatDropdownButton({
order_id: 42,
order_booking_id: 123,
department_id: 1,
});
await flushMicrotasks();
await wrapper.find(".dropdown-trigger button").trigger("click");
await flushMicrotasks();
expect(wrapper.text()).toContain("admin.pos.settings_wheel.email_notifications_section");
expect(wrapper.text()).toContain("admin.pos.settings_wheel.resend_booking_confirmation");
expect(wrapper.text()).not.toContain("admin.pos.settings_wheel.resend_booking_completion_confirmation");
expect(wrapper.text()).not.toContain("admin.pos.settings_wheel.resend_wash_certificate");
const button = wrapper
.findAll("button.dropdown-item-action")
.find((buttonWrapper) => buttonWrapper.text().includes("admin.pos.settings_wheel.resend_booking_confirmation"));
expect(button).toBeTruthy();
await button.trigger("click");
await flushMicrotasks();
expect(SessionUser.objects.order_bookings.functions.resendBookingConfirmation).toHaveBeenCalledWith(123);
expect(swalFireMock).toHaveBeenCalledWith(
expect.objectContaining({
title: "admin.pos.settings_wheel.resend_booking_confirmation_success",
icon: "success",
})
);
});
it("shows booking completion email action when a booking has a wash certificate", async () => {
SessionUser.objects.orders.functions.fetchAttachments.mockResolvedValue([
{
id: 301,
content: {
document: "wash_certificate_42.pdf",
other: "WASH_CERTIFICATE",
},
},
]);
const wrapper = mountFlatDropdownButton({
order_id: 42,
order_booking_id: 123,
department_id: 1,
});
await flushMicrotasks();
await wrapper.find(".dropdown-trigger button").trigger("click");
await flushMicrotasks();
expect(wrapper.text()).toContain("admin.pos.settings_wheel.email_notifications_section");
expect(wrapper.text()).toContain("admin.pos.settings_wheel.resend_booking_confirmation");
expect(wrapper.text()).toContain("admin.pos.settings_wheel.resend_booking_completion_confirmation");
expect(wrapper.text()).not.toContain("admin.pos.settings_wheel.resend_wash_certificate");
const button = wrapper
.findAll("button.dropdown-item-action")
.find((buttonWrapper) =>
buttonWrapper.text().includes("admin.pos.settings_wheel.resend_booking_completion_confirmation")
);
expect(button).toBeTruthy();
await button.trigger("click");
await flushMicrotasks();
expect(SessionUser.objects.order_bookings.functions.resendBookingCompletionConfirmation).toHaveBeenCalledWith(123);
expect(swalFireMock).toHaveBeenCalledWith(
expect.objectContaining({
title: "admin.pos.settings_wheel.resend_booking_completion_confirmation_success",
icon: "success",
})
);
});
it("shows wash certificate email action when an order has a certificate without booking", async () => {
SessionUser.objects.orders.functions.fetchAttachments.mockResolvedValue([
{
id: 302,
content: {
document: "wash_certificate_42_302.pdf",
other: "wash_certificate_42_302.pdf",
},
},
]);
const wrapper = mountFlatDropdownButton({
order_id: 42,
order_booking_id: null,
});
await flushMicrotasks();
await wrapper.find(".dropdown-trigger button").trigger("click");
await flushMicrotasks();
expect(wrapper.text()).toContain("admin.pos.settings_wheel.email_notifications_section");
expect(wrapper.text()).not.toContain("admin.pos.settings_wheel.resend_booking_confirmation");
expect(wrapper.text()).not.toContain("admin.pos.settings_wheel.resend_booking_completion_confirmation");
expect(wrapper.text()).toContain("admin.pos.settings_wheel.resend_wash_certificate");
const button = wrapper
.findAll("button.dropdown-item-action")
.find((buttonWrapper) => buttonWrapper.text().includes("admin.pos.settings_wheel.resend_wash_certificate"));
expect(button).toBeTruthy();
await button.trigger("click");
await flushMicrotasks();
expect(SessionUser.objects.orders.functions.resendWashCertificate).toHaveBeenCalledWith(42);
expect(swalFireMock).toHaveBeenCalledWith(
expect.objectContaining({
title: "admin.pos.settings_wheel.resend_wash_certificate_success",
icon: "success",
})
);
});
it("routes 'change invoice collection' action without opening a new tab", async () => {
const windowOpenSpy = vi.spyOn(window, "open").mockImplementation(() => null);
const wrapper = mount(ActionSettingsWheelButton, {
props: {
order_id: 42,
invoice_collection_id: 991,
},
slots: {
actions: "",
},
global: {
plugins: [i18n],
stubs: {
CustomerModal: { template: "" },
},
},
});
await flushMicrotasks();
await wrapper.find(".dropdown-trigger button").trigger("click");
const buttons = wrapper.findAll("button.dropdown-item-action");
const changeInvoiceCollectionButton = buttons.find((buttonWrapper) =>
buttonWrapper.text().includes("admin.pos.settings_wheel.change_invoice_collection")
);
expect(changeInvoiceCollectionButton).toBeTruthy();
await changeInvoiceCollectionButton.trigger("click");
await flushMicrotasks();
expect(SessionUser.objects.orders.functions.showChangeInvoiceCollectionForm).toHaveBeenCalledTimes(1);
expect(SessionUser.objects.orders.functions.showChangeInvoiceCollectionForm).toHaveBeenCalledWith(
42,
expect.any(Function)
);
expect(windowOpenSpy).not.toHaveBeenCalled();
windowOpenSpy.mockRestore();
});
it("routes 'view invoice collection in new tab' action to window.open only", async () => {
const windowOpenSpy = vi.spyOn(window, "open").mockImplementation(() => null);
const wrapper = mount(ActionSettingsWheelButton, {
props: {
order_id: 42,
invoice_collection_id: 777,
},
slots: {
actions: "",
},
global: {
plugins: [i18n],
stubs: {
CustomerModal: { template: "" },
},
},
});
await flushMicrotasks();
SessionUser.objects.orders.functions.showChangeInvoiceCollectionForm.mockClear();
await wrapper.find(".dropdown-trigger button").trigger("click");
const buttons = wrapper.findAll("button.dropdown-item-action");
const viewInvoiceCollectionButton = buttons.find((buttonWrapper) =>
buttonWrapper.text().includes("admin.pos.settings_wheel.view_invoice_collection_new_tab")
);
expect(viewInvoiceCollectionButton).toBeTruthy();
await viewInvoiceCollectionButton.trigger("click");
await flushMicrotasks();
expect(windowOpenSpy).toHaveBeenCalledWith("/superuser/invoices/777", "_blank");
expect(SessionUser.objects.orders.functions.showChangeInvoiceCollectionForm).not.toHaveBeenCalled();
windowOpenSpy.mockRestore();
});
it("groups multiple vehicle registrations into desktop flyout categories with nested actions", async () => {
SessionUser.request.mockImplementation((endpoint, method, params) => {
if (endpoint === "/vehicles/search") {
return Promise.resolve({
data: {
data: [{ id: params.search === "AB12345" ? 501 : 502, reg: params.search }],
},
});
}
return Promise.resolve({ data: { data: { summary: { primary_gateway: { id: 701 } } } } });
});
const geometrySpy = mockMenuGeometry();
const wrapper = mountDesktopFlyoutButton({
reg_1: "AB12345",
reg_2: "CD67890",
reg_3: "AB12345",
});
await flushMicrotasks();
await wrapper.find(".dropdown-trigger button").trigger("click");
await flushMicrotasks();
await flushMicrotasks();
await wrapper.get('[data-testid="action-settings-wheel-section-vehicle"]').trigger("mouseenter");
await flushMicrotasks();
const vehicleSubmenu = wrapper.get('[data-testid="action-settings-wheel-submenu-vehicle"]');
expect(vehicleSubmenu.text()).toContain("AB12345");
expect(vehicleSubmenu.text()).toContain("CD67890");
expect(vehicleSubmenu.findAll('[data-testid^="action-settings-wheel-vehicle-"]')).toHaveLength(2);
await wrapper.get('[data-testid="action-settings-wheel-vehicle-ab12345"]').trigger("mouseenter");
await flushMicrotasks();
const nestedPanel = wrapper.get('[data-testid="action-settings-wheel-vehicle-ab12345-panel"]');
expect(nestedPanel.text()).toContain("AB12345");
expect(nestedPanel.text()).toContain("admin.pos.settings_wheel.manage_vehicle");
expect(nestedPanel.text()).toContain("admin.pos.settings_wheel.view_vehicle_new_tab");
expect(nestedPanel.text()).toContain("admin.pos.settings_wheel.delete_vehicle");
wrapper.unmount();
geometrySpy.mockRestore();
});
it("routes vehicle manage/open actions and deletes the resolved vehicle id", async () => {
const refreshFunction = vi.fn(() => Promise.resolve());
SessionUser.request.mockImplementation((endpoint, method, params) => {
if (endpoint === "/vehicles/search") {
return Promise.resolve({
data: {
data: [{ id: 501, reg: params.search, customer_id: 123456 }],
},
});
}
return Promise.resolve({ data: { data: { summary: { primary_gateway: { id: 701 } } } } });
});
const wrapper = mountFlatDropdownButton({
refreshFunction,
reg_1: " AB12345 ",
});
await flushMicrotasks();
await wrapper.find(".dropdown-trigger button").trigger("click");
await flushMicrotasks();
await flushMicrotasks();
await wrapper.get('[data-testid="action-settings-wheel-vehicle-manage-ab12345"]').trigger("click");
await flushMicrotasks();
expect(SessionUser.functions.redirectTo.superUser).toHaveBeenCalledWith("/vehicles/AB12345", false);
await wrapper.find(".dropdown-trigger button").trigger("click");
await flushMicrotasks();
await wrapper.get('[data-testid="action-settings-wheel-vehicle-open-ab12345"]').trigger("click");
await flushMicrotasks();
expect(SessionUser.functions.redirectTo.superUser).toHaveBeenCalledWith("/vehicles/AB12345", true);
await wrapper.find(".dropdown-trigger button").trigger("click");
await flushMicrotasks();
await wrapper.get('[data-testid="action-settings-wheel-vehicle-delete-ab12345"]').trigger("click");
await flushMicrotasks();
expect(SessionUser.objects.vehicles.functions.showDeleteObjectForm).toHaveBeenCalledWith(501, expect.any(Function));
const [, onAfterSubmit] = SessionUser.objects.vehicles.functions.showDeleteObjectForm.mock.calls[0];
await onAfterSubmit();
expect(refreshFunction).toHaveBeenCalledTimes(1);
});
it("builds attachment flyout rows, auto-renders previews on hover, and opens the full preview action explicitly", async () => {
SessionUser.objects.orders.functions.fetchAttachments.mockResolvedValue([
{
id: 301,
content: {
document: "safety-seal.pdf",
other: "safety-seal.pdf",
},
},
{
id: 302,
content: {
image: "truck.jpg",
other: "truck.jpg",
},
},
]);
SessionUser.objects.orders.functions.downloadAttachment.mockImplementation((orderId, attachmentId) =>
Promise.resolve(`https://cdn.example.test/orders/${orderId}/attachments/${attachmentId}`)
);
global.fetch = vi.fn((url) =>
Promise.resolve({
ok: true,
blob: () =>
Promise.resolve(
new Blob(["preview"], {
type: String(url).includes("/301") ? "application/pdf" : "image/png",
})
),
})
);
const geometrySpy = mockMenuGeometry();
const windowOpenSpy = vi.spyOn(window, "open").mockImplementation(() => null);
const wrapper = mountDesktopFlyoutButton();
await flushMicrotasks();
await wrapper.find(".dropdown-trigger button").trigger("click");
await flushMicrotasks();
const attachmentsSection = wrapper.get('[data-testid="action-settings-wheel-section-attachments"]');
await attachmentsSection.trigger("mouseenter");
await flushMicrotasks();
const pdfRow = wrapper.get('[data-testid="action-settings-wheel-attachment-row-301"]');
const imageRow = wrapper.get('[data-testid="action-settings-wheel-attachment-row-302"]');
expect(pdfRow.text()).toContain("safety-seal.pdf");
expect(imageRow.text()).toContain("truck.jpg");
await pdfRow.trigger("mouseenter");
await flushMicrotasks();
await flushMicrotasks();
const previewPanel = wrapper.get('[data-testid="action-settings-wheel-attachment-panel"]');
expect(previewPanel.find("iframe").exists()).toBe(true);
expect(previewPanel.find("img").exists()).toBe(false);
expect(SessionUser.objects.orders.functions.downloadAttachment).toHaveBeenCalledWith(42, 301, false);
expect(global.fetch).toHaveBeenCalledTimes(1);
expect(wrapper.find('[data-testid="action-settings-wheel-attachment-action-preview-301"]').exists()).toBe(true);
expect(wrapper.find('[data-testid="action-settings-wheel-attachment-action-download-301"]').exists()).toBe(true);
expect(wrapper.find('[data-testid="action-settings-wheel-attachment-action-print-301"]').exists()).toBe(true);
expect(wrapper.find('[data-testid="action-settings-wheel-attachment-action-delete-301"]').exists()).toBe(true);
await wrapper.get('[data-testid="action-settings-wheel-attachment-action-preview-301"]').trigger("click");
await flushMicrotasks();
expect(windowOpenSpy).toHaveBeenCalledTimes(1);
await imageRow.trigger("focus");
await flushMicrotasks();
await flushMicrotasks();
expect(wrapper.get('[data-testid="action-settings-wheel-attachment-panel"]').find("img").exists()).toBe(true);
expect(global.fetch).toHaveBeenCalledTimes(2);
wrapper.unmount();
windowOpenSpy.mockRestore();
geometrySpy.mockRestore();
});
it("renders self-serve wash object attachments as readable text", async () => {
SessionUser.objects.orders.functions.fetchAttachments.mockResolvedValue([
{
id: 401,
content: {
image: null,
document: null,
other: {
type: "SELF_SERVE_WASH",
customer_number: 123456,
subuser_id: 77,
subuser: {
name: "Driver One",
username: "driver-one",
},
license_plate: "AB12345",
elapsed_wash_time_seconds: 601,
},
},
},
]);
const geometrySpy = mockMenuGeometry();
const wrapper = mountDesktopFlyoutButton({
customer_number: 999999,
});
await flushMicrotasks();
await wrapper.find(".dropdown-trigger button").trigger("click");
await flushMicrotasks();
await wrapper.get('[data-testid="action-settings-wheel-section-attachments"]').trigger("mouseenter");
await flushMicrotasks();
const attachmentRow = wrapper.get('[data-testid="action-settings-wheel-attachment-row-401"]');
expect(attachmentRow.text()).toContain("admin.pos.settings_wheel.self_serve_wash_attachment_for_customer");
expect(attachmentRow.text()).not.toContain("[object Object]");
await attachmentRow.trigger("mouseenter");
await flushMicrotasks();
const previewText = wrapper.get('[data-testid="action-settings-wheel-attachment-panel"]').text();
expect(previewText).toContain("admin.pos.settings_wheel.self_serve_wash_attachment");
expect(previewText).toContain("admin.pos.settings_wheel.self_serve_customer: #123456");
expect(previewText).toContain("admin.pos.settings_wheel.self_serve_driver: Driver One");
expect(previewText).toContain("pos.license_plate: AB12345");
expect(previewText).toContain("admin.pos.settings_wheel.self_serve_elapsed: 11 min");
expect(previewText).not.toContain("[object Object]");
wrapper.unmount();
geometrySpy.mockRestore();
});
it("accepts self-serve wash drafts by assigning the original customer and collection", async () => {
SessionUser.objects.orders.functions.fetchAttachments.mockResolvedValue([
{
id: 402,
content: {
other: {
type: "SELF_SERVE_WASH",
customer_number: 123456,
draft_customer_number: 999999,
subuser_id: 77,
},
},
},
]);
SessionUser.request.mockResolvedValueOnce({
data: {
data: [{ id: 888, closed_at: null }],
},
});
const refreshFunction = vi.fn(() => Promise.resolve());
const wrapper = mountFlatDropdownButton({
order_id: 42,
customer_number: 999999,
department_id: 5,
refreshFunction,
});
await flushMicrotasks();
await wrapper.find(".dropdown-trigger button").trigger("click");
await flushMicrotasks();
const acceptButton = wrapper
.findAll("button.dropdown-item-action")
.find((buttonWrapper) => buttonWrapper.text().includes("admin.pos.settings_wheel.accept_self_serve_wash"));
expect(acceptButton).toBeTruthy();
await acceptButton.trigger("click");
await flushMicrotasks();
await flushMicrotasks();
expect(SessionUser.request).toHaveBeenCalledWith("/collected-invoices", "GET", {
page: 1,
limit: 100,
order: "closed_at:asc",
filters: "customer_number:123456,booked_invoice_id:is_null",
});
expect(SessionUser.objects.collectedOrderInvoices.add).not.toHaveBeenCalled();
expect(SessionUser.objects.orders.functions.assignDraftCustomer).toHaveBeenCalledWith({
order_id: 42,
customer_id: 123456,
invoice_collection_id: 888,
department_id: 5,
recalculate_prices: true,
});
expect(refreshFunction).toHaveBeenCalledTimes(1);
expect(swalFireMock).toHaveBeenCalledWith(
expect.objectContaining({
title: "admin.pos.settings_wheel.accept_self_serve_wash",
icon: "question",
})
);
expect(swalFireMock).toHaveBeenCalledWith(
expect.objectContaining({
title: "admin.pos.settings_wheel.accept_self_serve_wash_success",
icon: "success",
})
);
wrapper.unmount();
});
it("deletes attachments, refreshes the list, and clears preview object URLs on close", async () => {
const attachmentsBeforeDelete = [
{
id: 301,
content: {
document: "safety-seal.pdf",
other: "safety-seal.pdf",
},
},
];
SessionUser.objects.orders.functions.fetchAttachments
.mockResolvedValueOnce(attachmentsBeforeDelete)
.mockResolvedValueOnce([]);
SessionUser.objects.orders.functions.downloadAttachment.mockResolvedValue(
"https://cdn.example.test/orders/42/attachments/301"
);
const refreshFunction = vi.fn(() => Promise.resolve());
const geometrySpy = mockMenuGeometry();
const wrapper = mountDesktopFlyoutButton({ refreshFunction });
await flushMicrotasks();
await wrapper.find(".dropdown-trigger button").trigger("click");
await flushMicrotasks();
await wrapper.get('[data-testid="action-settings-wheel-section-attachments"]').trigger("mouseenter");
await flushMicrotasks();
await wrapper.get('[data-testid="action-settings-wheel-attachment-row-301"]').trigger("mouseenter");
await flushMicrotasks();
await flushMicrotasks();
expect(global.URL.createObjectURL).toHaveBeenCalledTimes(1);
await wrapper.get('[data-testid="action-settings-wheel-attachment-action-delete-301"]').trigger("click");
await flushMicrotasks();
expect(SessionUser.objects.orders.functions.removeAttachment).toHaveBeenCalledTimes(1);
expect(SessionUser.objects.orders.functions.removeAttachment).toHaveBeenCalledWith(42, 301);
expect(refreshFunction).toHaveBeenCalledTimes(1);
expect(SessionUser.objects.orders.functions.fetchAttachments).toHaveBeenCalledTimes(2);
expect(wrapper.find('[data-testid="action-settings-wheel-section-attachments"]').exists()).toBe(false);
await wrapper.find(".dropdown-trigger button").trigger("click");
await flushMicrotasks();
expect(global.URL.revokeObjectURL).toHaveBeenCalledTimes(1);
wrapper.unmount();
geometrySpy.mockRestore();
});
it("prints document attachments from the resolved preview source instead of a blank wrapper window", async () => {
SessionUser.objects.orders.functions.fetchAttachments.mockResolvedValue([
{
id: 301,
content: {
document: "safety-seal.pdf",
other: "safety-seal.pdf",
},
},
]);
SessionUser.objects.orders.functions.downloadAttachment.mockResolvedValue(
"https://cdn.example.test/orders/42/attachments/301"
);
const printWindow = {
focus: vi.fn(),
print: vi.fn(),
addEventListener: vi.fn(),
};
const windowOpenSpy = vi.spyOn(window, "open").mockImplementation(() => printWindow);
const geometrySpy = mockMenuGeometry();
const wrapper = mountDesktopFlyoutButton();
await flushMicrotasks();
await wrapper.find(".dropdown-trigger button").trigger("click");
await flushMicrotasks();
await wrapper.get('[data-testid="action-settings-wheel-section-attachments"]').trigger("mouseenter");
await flushMicrotasks();
await wrapper.get('[data-testid="action-settings-wheel-attachment-row-301"]').trigger("mouseenter");
await flushMicrotasks();
await flushMicrotasks();
await wrapper.get('[data-testid="action-settings-wheel-attachment-action-print-301"]').trigger("click");
await flushMicrotasks();
expect(windowOpenSpy).toHaveBeenCalledWith(expect.stringMatching(/^blob:/), "_blank", "noopener,noreferrer");
expect(printWindow.addEventListener).toHaveBeenCalledWith("load", expect.any(Function), { once: true });
windowOpenSpy.mockRestore();
geometrySpy.mockRestore();
});
it("adds department hardware sections for a lane and routes the new actions correctly", async () => {
const geometrySpy = mockMenuGeometry();
const refreshFunction = vi.fn(() => Promise.resolve());
const wrapper = mountDesktopFlyoutButton({
order_id: null,
department_id: 1,
department_lane_id: 8,
refreshFunction,
});
await flushMicrotasks();
await wrapper.find(".dropdown-trigger button").trigger("click");
await flushMicrotasks();
const sectionLabels = wrapper
.findAll(".action-settings-wheel-section-trigger__label")
.map((section) => section.text())
.filter(Boolean);
expect(sectionLabels).toEqual([
"Lane",
"admin.pos.settings_wheel.self_serve_studio_section",
"admin.pos.settings_wheel.gates_section",
"admin.pos.settings_wheel.relays_section",
"admin.pos.settings_wheel.gateways_section",
]);
const reopenDropdown = async () => {
if (!wrapper.find('[data-testid="action-settings-wheel-sections"]').exists()) {
await wrapper.find(".dropdown-trigger button").trigger("click");
await flushMicrotasks();
}
};
const getActionButton = async (label) => {
await reopenDropdown();
const button = wrapper
.findAll("button.dropdown-item-action")
.find((buttonWrapper) => buttonWrapper.text().includes(label));
expect(button).toBeTruthy();
return button;
};
await (await getActionButton("admin.pos.settings_wheel.open_studio")).trigger("click");
expect(SessionUser.functions.redirectTo.department).toHaveBeenCalledWith(1, "modules/self-serve/studio", true);
await (await getActionButton("admin.pos.settings_wheel.add_gate")).trigger("click");
expect(SessionUser.objects.department_gates.showCreateObjectForm).toHaveBeenCalledWith(expect.any(Function), {
department: 1,
});
await (await getActionButton("admin.pos.settings_wheel.add_relay")).trigger("click");
expect(SessionUser.objects.department_relays.showCreateObjectForm).toHaveBeenCalledWith(expect.any(Function), {
department: 1,
});
await (await getActionButton("admin.pos.settings_wheel.open_primary_gateway")).trigger("click");
await flushMicrotasks();
expect(SessionUser.request).toHaveBeenCalledWith("/modules/edge-gateways/workspace/departments/1", "GET");
expect(SessionUser.functions.redirectTo.superUser).toHaveBeenCalledWith(
"/selfserve/edge-agents/701/overview",
true
);
wrapper.unmount();
geometrySpy.mockRestore();
});
it("creates invoice period flags for supported order and order item targets", async () => {
SessionUser.hasPermission.mockImplementation((permission) =>
[
"add_customer_attribute",
"delete_customer_attribute",
"list_customer_attributes",
"add_invoice_period_flag",
"superuser",
].includes(permission)
);
SessionUser.request.mockResolvedValueOnce({
data: {
data: {
id: 901,
source: "manual",
target_type: "order",
target_id: 42,
reason: "Needs review",
},
},
});
const wrapper = mountFlatDropdownButton({
order_id: 42,
customer_number: 123456,
flagTarget: {
target_type: "order_item",
target_id: 77,
},
});
await flushMicrotasks();
await wrapper.find(".dropdown-trigger button").trigger("click");
await flushMicrotasks();
expect(wrapper.text()).toContain("admin.pos.settings_wheel.flags_section");
expect(wrapper.text()).toContain("admin.pos.settings_wheel.add_flag_order");
expect(wrapper.text()).toContain("admin.pos.settings_wheel.add_flag_order_item_price");
expect(wrapper.text()).toContain("admin.pos.settings_wheel.add_flag_customer");
const addOrderFlagButton = wrapper
.findAll("button.dropdown-item-action")
.find((buttonWrapper) => buttonWrapper.text().includes("admin.pos.settings_wheel.add_flag_order"));
expect(addOrderFlagButton).toBeTruthy();
await addOrderFlagButton.trigger("click");
await flushMicrotasks();
expect(swalFireMock).toHaveBeenCalledWith(
expect.objectContaining({
input: "textarea",
inputPlaceholder: "admin.pos.settings_wheel.add_flag_reason_placeholder",
})
);
expect(SessionUser.request).toHaveBeenCalledWith("/superuser/invoicing/period/flags", "POST", {
target_type: "order",
target_id: 42,
reason: "Needs review",
});
expect(wrapper.emitted("flagCreated")?.[0]?.[0]).toEqual(
expect.objectContaining({
id: 901,
source: "manual",
})
);
});
it("renders customer flag creation as a red bottom section with a customer label", async () => {
SessionUser.hasPermission.mockImplementation((permission) =>
[
"add_customer_attribute",
"delete_customer_attribute",
"list_customer_attributes",
"add_invoice_period_flag",
"superuser",
].includes(permission)
);
const geometrySpy = mockMenuGeometry();
const wrapper = mountDesktopFlyoutButton({
order_id: null,
customer_number: 123456,
reg_1: null,
reg_2: null,
flagTarget: {
target_type: "customer",
target_id: 123456,
},
});
await flushMicrotasks();
await wrapper.find(".dropdown-trigger button").trigger("click");
await flushMicrotasks();
await flushMicrotasks();
const sectionIds = wrapper
.findAll('[data-testid^="action-settings-wheel-section-"]')
.map((section) => section.attributes("data-testid"));
expect(sectionIds.at(-1)).toBe("action-settings-wheel-section-invoice-period-flags");
expect(wrapper.text()).toContain("admin.pos.settings_wheel.add_flag_customer");
expect(wrapper.text()).not.toContain("admin.pos.settings_wheel.add_flag_target");
const addCustomerFlagButton = wrapper
.findAll("button.dropdown-item-action")
.find((buttonWrapper) => buttonWrapper.text().includes("admin.pos.settings_wheel.add_flag_customer"));
expect(addCustomerFlagButton).toBeTruthy();
expect(addCustomerFlagButton.find(".has-text-danger").exists()).toBe(true);
expect(addCustomerFlagButton.find(".has-text-warning").exists()).toBe(false);
geometrySpy.mockRestore();
});
});