// @vitest-environment jsdom import { mount } from "@vue/test-utils"; import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; import ActionSettingsWheelButton from "@/components/displays/buttons/ActionSettingsWheelButton.vue"; import { SessionUser } from "@/components/session/token/SessionUser.vue"; import { createTestI18n } from "./helpers/mountWithApp.js"; const getUserIdMock = vi.hoisted(() => vi.fn(() => Promise.resolve({ data: { data: { user_id: 777 } } }))); 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), 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: { showCompleteConfirmationModal: 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()), }, }, orders: { meta: { labels: { single: "Order", }, }, functions: { fetchAttachments: vi.fn(() => Promise.resolve([])), get_department_id: vi.fn(() => Promise.resolve(1)), removeAttachment: vi.fn(() => Promise.resolve({ success: true })), 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", }, functions: { download: vi.fn(() => Promise.resolve()), }, }, vehicles: { meta: { labels: { single: "Vehicle", multiple: "Vehicles", }, }, }, global: { language: { customer: "Customer", }, }, subuser_grants: { functions: { showPermissionEditForm: vi.fn(() => Promise.resolve()), }, delete: vi.fn(() => Promise.resolve()), }, }, }, })); const SETTINGS_WHEEL_TRANSLATION_KEYS = [ "admin.pos.settings_wheel.associate_order", "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.download_invoice", "admin.pos.settings_wheel.edit_permissions", "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.mark_as_completed", "admin.pos.settings_wheel.no_actions_defined", "admin.pos.settings_wheel.open_attached_file", "admin.pos.settings_wheel.password_changed", "admin.pos.settings_wheel.password_changed_text", "admin.pos.settings_wheel.please_enter_password", "admin.pos.settings_wheel.scan_qr_to_login", "admin.pos.settings_wheel.show_customer", "admin.pos.settings_wheel.show_qr_code", "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", "global.delete", "global.download", "global.loading", "global.preview", "global.print", "superuser.department_lane.force_disable_machine", "superuser.department_lane.force_enable_machine", ]; 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 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 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 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 originalFetch = global.fetch; const originalCreateObjectURL = global.URL.createObjectURL; const originalRevokeObjectURL = global.URL.revokeObjectURL; const originalMatchMedia = window.matchMedia; describe("ActionSettingsWheelButton", () => { beforeEach(() => { getUserIdMock.mockClear(); SessionUser.objects.orders.functions.showChangeInvoiceCollectionForm.mockClear(); SessionUser.objects.orders.functions.fetchAttachments.mockReset(); SessionUser.objects.orders.functions.fetchAttachments.mockResolvedValue([]); 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 }); 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("resolves customer user id once on mount and not on unrelated rerenders", 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); await wrapper.setProps({ icon: "fas fa-ellipsis-v" }); await flushMicrotasks(); expect(getUserIdMock).toHaveBeenCalledTimes(1); await wrapper.setProps({ customer_number: 654321 }); await flushMicrotasks(); expect(getUserIdMock).toHaveBeenCalledTimes(2); expect(getUserIdMock).toHaveBeenLastCalledWith(654321); }); it("does not perform customer lookup when explicit user id is provided", 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(); }); 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("builds attachment flyout rows and switches the preview panel on hover and focus", 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 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(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 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); geometrySpy.mockRestore(); }); 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(); 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(); }); });