// @vitest-environment jsdom import { computed, nextTick } from "vue"; import { mount } from "@vue/test-utils"; import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; const { requestMock, startDateRef, endDateRef, routeState, periodRefreshSignalRef, finishPeriodRefreshMock, routerReplaceMock, availableViewNamesRef, currentViewRef, sharedVariablesRef, } = vi.hoisted(() => { const { reactive, ref } = require("vue"); return { requestMock: vi.fn(), startDateRef: ref(new Date("2026-04-01T00:00:00.000Z")), endDateRef: ref(new Date("2026-04-30T23:59:59.000Z")), routeState: reactive({ query: { activeTab: "period", }, }), periodRefreshSignalRef: ref({ sequence: 0, customerNumbers: [], invoiceCollectionIds: [], }), finishPeriodRefreshMock: vi.fn(), routerReplaceMock: vi.fn(() => Promise.resolve()), availableViewNamesRef: ref({}), currentViewRef: ref("all"), sharedVariablesRef: ref({}), }; }); vi.mock("vue-router", () => ({ useRoute: () => routeState, useRouter: () => ({ replace: routerReplaceMock, }), })); vi.mock("@/components/session/token/SessionUser.vue", () => ({ SessionUser: { request: requestMock, objects: { global: { language: { all: "All", invoice_per_order: "Invoice per order", fixed_price_arrangements: "Fixed pricing", only_tank_cleaning: "Tank cleaning", special_arrangements: "Special arrangements", possible_duplicates: "Possible duplicates", }, }, vehicles: { columns: { wash_subscription: { label: "Vehicle subscriptions", }, }, }, }, }, })); vi.mock( "@/views/dashboards/superUserDashboard/InvoicingBillingPeriod/imports/InvoicingBillingPeriodImportDates.vue", () => ({ dates: { variables: { start: startDateRef, end: endDateRef, }, computed: { isEntireMonth: computed(() => true), formattedStartDate: computed(() => startDateRef.value.toISOString().split("T")[0]), formattedEndDate: computed(() => endDateRef.value.toISOString().split("T")[0]), }, }, }) ); vi.mock( "@/views/dashboards/superUserDashboard/InvoicingBillingPeriod/imports/InvoicingBillingPeriodImportView.vue", () => ({ view: { variables: { sharedVariables: sharedVariablesRef, currentView: currentViewRef, availableViewNames: availableViewNamesRef, }, }, }) ); vi.mock( "@/views/dashboards/superUserDashboard/InvoicingBillingPeriod/imports/InvoicingBillingPeriodImportInvoiceQueue.vue", () => ({ invoiceQueue: { periodRefreshSignal: periodRefreshSignalRef, finishPeriodRefresh: finishPeriodRefreshMock, }, }) ); import Right from "@/views/dashboards/superUserDashboard/InvoicingBillingPeriod/displays/layout/Right.vue"; import { periodPaging, resetPeriodPagingState, } from "@/views/dashboards/superUserDashboard/InvoicingBillingPeriod/imports/InvoicingBillingPeriodImportPaging.js"; const flushAll = async () => { await nextTick(); await Promise.resolve(); await nextTick(); }; let mountedWrappers = []; const mountRight = () => { const wrapper = mount(Right, { global: { stubs: { WhiteBox: { template: "
" }, ColorIndicator: { props: ["label"], template: "
{{ label?.text }}
" }, }, }, }); mountedWrappers.push(wrapper); return wrapper; }; const emptyPeriodResponse = () => ({ data: { data: { types: { all: [], invoice_per_order: [], fixed_pricing: [], tank_cleaning: [], special_arrangements: [], vehicle_subscriptions: [], possible_duplicates: [], }, }, }, }); const selfWashResponse = (rows = [], total = rows.length) => ({ data: { data: rows, meta: { pagination: { total, }, }, }, }); const mockPeriodResponses = (...responses) => { let periodResponseIndex = 0; requestMock.mockImplementation((url) => { if (url === "/superuser/invoicing/period") { const response = responses[periodResponseIndex] ?? responses[responses.length - 1] ?? emptyPeriodResponse(); periodResponseIndex += 1; return Promise.resolve(response); } return Promise.resolve(selfWashResponse()); }); }; const periodCalls = () => requestMock.mock.calls.filter(([url]) => url === "/superuser/invoicing/period"); const selfWashCalls = () => requestMock.mock.calls.filter(([url]) => url === "/modules/xlvask/services/usage/orders"); describe("Invoicing period queue-driven refresh", () => { beforeEach(() => { window.localStorage.setItem("token", "unit-test-period-cache-token"); requestMock.mockReset(); mockPeriodResponses(emptyPeriodResponse()); routeState.query.activeTab = "period"; delete routeState.query.periodView; delete routeState.query.periodSearch; delete routeState.query.periodPage; delete routeState.query.periodLimit; periodRefreshSignalRef.value = { sequence: 0, customerNumbers: [], invoiceCollectionIds: [], }; finishPeriodRefreshMock.mockReset(); routerReplaceMock.mockClear(); resetPeriodPagingState(); sharedVariablesRef.value = {}; availableViewNamesRef.value = {}; currentViewRef.value = "all"; }); afterEach(() => { mountedWrappers.forEach((wrapper) => wrapper.unmount()); mountedWrappers = []; window.sessionStorage.clear(); window.localStorage.clear(); }); it("refreshes the current paginated page when queue activity reaches a terminal state", async () => { mockPeriodResponses( { data: { data: { types: { all: [ { customer_number: 1001, customer_name: "Keep Customer", transactions: [], expanded: true }, { customer_number: 1002, customer_name: "Old Customer", transactions: [], expanded: true }, { customer_number: 1003, customer_name: "After Customer", transactions: [] }, ], invoice_per_order: [], fixed_pricing: [{ customer_number: 1001, customer_name: "Keep Customer", transactions: [] }], tank_cleaning: [], special_arrangements: [], vehicle_subscriptions: [], possible_duplicates: [], }, }, }, }, { data: { data: { types: { all: [ { customer_number: 1001, customer_name: "Keep Customer", transactions: [], expanded: true }, { customer_number: 1002, customer_name: "Updated Customer", transactions: [], expanded: true }, { customer_number: 1003, customer_name: "After Customer", transactions: [] }, ], invoice_per_order: [], fixed_pricing: [{ customer_number: 1001, customer_name: "Keep Customer", transactions: [] }], tank_cleaning: [], special_arrangements: [], vehicle_subscriptions: [], possible_duplicates: [], }, }, }, } ); mountRight(); await flushAll(); expect(periodCalls()).toHaveLength(1); periodRefreshSignalRef.value = { sequence: 1, customerNumbers: [1002], invoiceCollectionIds: [3002], }; await flushAll(); expect(periodCalls()).toHaveLength(2); expect(periodCalls().at(-1)).toEqual([ "/superuser/invoicing/period", "GET", { dateFrom: "2026-04-01", dateTo: "2026-04-30", periodView: "all", page: 1, limit: 100, search: "", flagTab: "all", includeRequiresAction: 1, includeBooked: 1, }, ]); expect(sharedVariablesRef.value.types.all.map((customer) => customer.customer_number)).toEqual([1001, 1002, 1003]); expect(sharedVariablesRef.value.types.all[0].expanded).toBe(true); expect(sharedVariablesRef.value.types.all[1].customer_name).toBe("Updated Customer"); expect(sharedVariablesRef.value.types.all[1].expanded).toBe(true); expect(sharedVariablesRef.value.types.fixed_pricing.map((customer) => customer.customer_number)).toEqual([1001]); expect(finishPeriodRefreshMock).toHaveBeenCalledWith([1002], [3002]); }); it("requests the active period page and renders selector counts from backend type_counts", async () => { mockPeriodResponses({ data: { data: { types: { all: [{ customer_number: 2001, customer_name: "Visible Customer", transactions: [] }], invoice_per_order: [], fixed_pricing: [], tank_cleaning: [], special_arrangements: [], vehicle_subscriptions: [], possible_duplicates: [], }, type_counts: { all: { requires_action: 2, draft: 1, manual_flags: 0, automatic_flags: 0, completed: 37, total: 40, }, }, }, meta: { pagination: { page: 1, per_page: 100, total: 40, search: "", filters: { includeRequiresAction: true, includeBooked: true, }, order: null, }, }, }, }); const wrapper = mountRight(); await flushAll(); expect(periodCalls()).toContainEqual([ "/superuser/invoicing/period", "GET", { dateFrom: "2026-04-01", dateTo: "2026-04-30", periodView: "all", page: 1, limit: 100, search: "", flagTab: "all", includeRequiresAction: 1, includeBooked: 1, }, ]); expect(wrapper.get("[data-testid='invoicing-period-view-selector-all']").text()).toContain("All (37/40)"); }); it("preloads the next active page and keeps current plus previous month cache warm", async () => { vi.useFakeTimers(); vi.setSystemTime(new Date("2026-05-12T10:00:00.000Z")); try { mockPeriodResponses({ data: { data: { types: { all: [{ customer_number: 2501, customer_name: "Warm Customer", transactions: [] }], invoice_per_order: [], fixed_pricing: [], tank_cleaning: [], special_arrangements: [], vehicle_subscriptions: [], possible_duplicates: [], }, }, meta: { pagination: { page: 1, per_page: 100, total: 250, search: "", filters: { includeRequiresAction: true, includeBooked: true, }, order: null, }, }, }, }); mountRight(); await flushAll(); expect(periodCalls()).toHaveLength(1); await vi.advanceTimersByTimeAsync(100); await flushAll(); const requestedParams = periodCalls().map(([, , params]) => params); expect(requestedParams).toContainEqual({ dateFrom: "2026-04-01", dateTo: "2026-04-30", periodView: "all", page: 2, limit: 100, search: "", flagTab: "all", includeRequiresAction: 1, includeBooked: 1, periodWarm: 1, }); expect(requestedParams).toContainEqual({ dateFrom: "2026-05-01", dateTo: "2026-05-31", periodView: "all", page: 1, limit: 100, search: "", includeRequiresAction: 1, includeBooked: 1, periodWarm: 1, }); } finally { vi.useRealTimers(); } }); it("renders cached period data while a fresh request is still pending", async () => { mockPeriodResponses({ data: { data: { types: { all: [{ customer_number: 3001, customer_name: "Cached Customer", transactions: [] }], invoice_per_order: [], fixed_pricing: [], tank_cleaning: [], special_arrangements: [], vehicle_subscriptions: [], possible_duplicates: [], }, type_counts: { all: { requires_action: 0, draft: 0, manual_flags: 0, automatic_flags: 0, completed: 1, total: 1, }, }, }, meta: { pagination: { page: 1, per_page: 100, total: 1, search: "", filters: { includeRequiresAction: true, includeBooked: true, }, order: null, }, }, }, }); const firstWrapper = mountRight(); await flushAll(); firstWrapper.unmount(); mountedWrappers = mountedWrappers.filter((wrapper) => wrapper !== firstWrapper); sharedVariablesRef.value = {}; requestMock.mockImplementation((url) => { if (url === "/superuser/invoicing/period") { return new Promise(() => {}); } return Promise.resolve(selfWashResponse()); }); const secondWrapper = mountRight(); await flushAll(); expect(periodCalls()).toHaveLength(2); expect(sharedVariablesRef.value.types.all[0].customer_name).toBe("Cached Customer"); expect(secondWrapper.get("[data-testid='invoicing-period-view-selector-all']").text()).toContain("All (1/1)"); }); it("keeps the current customer list visible while an uncached reload is pending", async () => { let resolveReload; requestMock.mockImplementation((url, _method, params = {}) => { if (url !== "/superuser/invoicing/period") { return Promise.resolve(selfWashResponse()); } if (params.search === "Beta") { return new Promise((resolve) => { resolveReload = resolve; }); } return Promise.resolve({ data: { data: { types: { all: [{ customer_number: 3101, customer_name: "Current Customer", transactions: [] }], invoice_per_order: [], fixed_pricing: [], tank_cleaning: [], special_arrangements: [], vehicle_subscriptions: [], possible_duplicates: [], }, }, meta: { pagination: { page: 1, per_page: 100, total: 1, search: "", filters: { includeRequiresAction: true, includeBooked: true, }, order: null, }, }, }, }); }); mountRight(); await flushAll(); periodPaging.search = "Beta"; await flushAll(); expect(sharedVariablesRef.value.types.all[0].customer_name).toBe("Current Customer"); expect(periodPaging.isRefreshing).toBe(true); resolveReload({ data: { data: { types: { all: [{ customer_number: 3102, customer_name: "Reloaded Customer", transactions: [] }], invoice_per_order: [], fixed_pricing: [], tank_cleaning: [], special_arrangements: [], vehicle_subscriptions: [], possible_duplicates: [], }, }, meta: { pagination: { page: 1, per_page: 100, total: 1, search: "Beta", filters: { includeRequiresAction: true, includeBooked: true, }, order: null, }, }, }, }); await flushAll(); expect(sharedVariablesRef.value.types.all[0].customer_name).toBe("Reloaded Customer"); expect(periodPaging.isRefreshing).toBe(false); }); it("keeps the current right-menu view rendered until the switched view is ready", async () => { let resolveViewSwitch; requestMock.mockImplementation((url, _method, params = {}) => { if (url !== "/superuser/invoicing/period") { return Promise.resolve(selfWashResponse()); } if (params.periodView === "fixed_pricing") { return new Promise((resolve) => { resolveViewSwitch = resolve; }); } return Promise.resolve({ data: { data: { types: { all: [{ customer_number: 3201, customer_name: "All Customer", transactions: [] }], invoice_per_order: [], fixed_pricing: [], tank_cleaning: [], special_arrangements: [], vehicle_subscriptions: [], possible_duplicates: [], }, }, meta: { pagination: { page: 1, per_page: 100, total: 1, search: "", filters: { includeRequiresAction: true, includeBooked: true, }, order: null, }, }, }, }); }); const wrapper = mountRight(); await flushAll(); await wrapper.get("[data-testid='invoicing-period-view-selector-fixed_pricing']").trigger("click"); await flushAll(); expect(currentViewRef.value).toBe("all"); expect(sharedVariablesRef.value.types.all[0].customer_name).toBe("All Customer"); expect(periodPaging.isRefreshing).toBe(true); resolveViewSwitch({ data: { data: { types: { all: [], invoice_per_order: [], fixed_pricing: [{ customer_number: 3202, customer_name: "Fixed Customer", transactions: [] }], tank_cleaning: [], special_arrangements: [], vehicle_subscriptions: [], possible_duplicates: [], }, }, meta: { pagination: { page: 1, per_page: 100, total: 1, search: "", filters: { includeRequiresAction: true, includeBooked: true, }, order: null, }, }, }, }); await flushAll(); expect(currentViewRef.value).toBe("fixed_pricing"); expect(sharedVariablesRef.value.types.fixed_pricing[0].customer_name).toBe("Fixed Customer"); expect(periodPaging.isRefreshing).toBe(false); }); it("keeps the current right-menu view when a background view switch fails", async () => { let rejectViewSwitch; requestMock.mockImplementation((url, _method, params = {}) => { if (url !== "/superuser/invoicing/period") { return Promise.resolve(selfWashResponse()); } if (params.periodView === "fixed_pricing") { return new Promise((_resolve, reject) => { rejectViewSwitch = reject; }); } return Promise.resolve({ data: { data: { types: { all: [{ customer_number: 3211, customer_name: "Stable Customer", transactions: [] }], invoice_per_order: [], fixed_pricing: [], tank_cleaning: [], special_arrangements: [], vehicle_subscriptions: [], possible_duplicates: [], }, }, meta: { pagination: { page: 1, per_page: 100, total: 1, search: "", filters: { includeRequiresAction: true, includeBooked: true, }, order: null, }, }, }, }); }); const wrapper = mountRight(); await flushAll(); await wrapper.get("[data-testid='invoicing-period-view-selector-fixed_pricing']").trigger("click"); await flushAll(); rejectViewSwitch(new Error("Reload failed")); await flushAll(); expect(currentViewRef.value).toBe("all"); expect(sharedVariablesRef.value.types.all[0].customer_name).toBe("Stable Customer"); expect(periodPaging.isRefreshing).toBe(false); }); it("reuses cached Selvvask selector counts without repeating the broad usage-order request", async () => { requestMock.mockImplementation((url) => { if (url === "/superuser/invoicing/period") { return Promise.resolve(emptyPeriodResponse()); } return Promise.resolve( selfWashResponse( [ { id: 8101, linked_order_id: 7001 }, { id: 8102, linked_order_id: null }, ], 2 ) ); }); const firstWrapper = mountRight(); await flushAll(); expect(firstWrapper.get("[data-testid='invoicing-period-view-selector-self_wash']").text()).toContain( "Selvvask (1/2)" ); expect(selfWashCalls()).toHaveLength(1); firstWrapper.unmount(); mountedWrappers = mountedWrappers.filter((wrapper) => wrapper !== firstWrapper); requestMock.mockImplementation((url) => { if (url === "/superuser/invoicing/period") { return new Promise(() => {}); } return Promise.resolve(selfWashResponse([], 0)); }); const secondWrapper = mountRight(); await flushAll(); expect(secondWrapper.get("[data-testid='invoicing-period-view-selector-self_wash']").text()).toContain( "Selvvask (1/2)" ); expect(selfWashCalls()).toHaveLength(1); }); it("loads Selvvask selector counts with the full selected-period limit", async () => { requestMock.mockImplementation((url) => { if (url === "/superuser/invoicing/period") { return Promise.resolve(emptyPeriodResponse()); } return Promise.resolve( selfWashResponse( [ { id: 8101, linked_order_id: 7001 }, { id: 8102, linked_order_id: null, automation: { status: "accepted", action: "attach_order", }, }, { id: 8103, linked_order_id: null }, ], 3 ) ); }); const wrapper = mountRight(); await flushAll(); expect(wrapper.get("[data-testid='invoicing-period-view-selector-self_wash']").text()).toContain("Selvvask (2/3)"); expect(selfWashCalls()[0][2]).toMatchObject({ limit: 10000, filters: "StartTime-date_from:2026-04-01,StartTime-date_to:2026-04-30", order: "StartTime:desc", }); }); it("ignores stale period responses that resolve after a newer search request", async () => { const deferred = []; requestMock.mockImplementation((url) => { if (url !== "/superuser/invoicing/period") { return Promise.resolve(selfWashResponse()); } return new Promise((resolve) => { deferred.push(resolve); }); }); mountRight(); await flushAll(); const callsBeforeSearch = periodCalls().length; periodPaging.search = "new"; await flushAll(); expect(periodCalls().length).toBeGreaterThan(callsBeforeSearch); const latestDeferredIndex = deferred.length - 1; deferred[latestDeferredIndex]({ data: { data: { types: { all: [{ customer_number: 4002, customer_name: "New Result", transactions: [] }], invoice_per_order: [], fixed_pricing: [], tank_cleaning: [], special_arrangements: [], vehicle_subscriptions: [], possible_duplicates: [], }, }, meta: { pagination: { page: 1, per_page: 100, total: 1, search: "new", filters: { includeRequiresAction: true, includeBooked: true, }, order: null, }, }, }, }); await flushAll(); for (let index = 0; index < latestDeferredIndex; index += 1) { deferred[index]({ data: { data: { types: { all: [{ customer_number: 4001, customer_name: "Old Result", transactions: [] }], invoice_per_order: [], fixed_pricing: [], tank_cleaning: [], special_arrangements: [], vehicle_subscriptions: [], possible_duplicates: [], }, }, }, }); } await flushAll(); expect(sharedVariablesRef.value.types.all[0].customer_name).toBe("New Result"); }); it("refreshes when the Period tab becomes active after being in another tab", async () => { routeState.query.activeTab = "overview"; mountRight(); await flushAll(); const initialCallCount = requestMock.mock.calls.length; expect(initialCallCount).toBeGreaterThanOrEqual(1); routeState.query.activeTab = "period"; await flushAll(); expect(requestMock.mock.calls.length).toBeGreaterThan(initialCallCount); }); });