// @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(); }); });