import { readFileSync } from "node:fs"; import { join } from "node:path"; import { describe, expect, it } from "vitest"; import { readI18nRuntimeMessages } from "./helpers/readI18nRuntimeMessages"; const root = process.cwd(); const routerSource = readFileSync(join(root, "src/router.js"), "utf8"); const invoicesViewSource = readFileSync( join(root, "src/views/dashboards/superUserDashboard/CollectedOrderInvoices.vue"), "utf8" ); const overviewSource = readFileSync( join(root, "src/views/dashboards/superUserDashboard/collectedOrderInvoices/CollectedOrderInvoicesOverview.vue"), "utf8" ); const overviewEconomicSource = readFileSync( join(root, "src/views/dashboards/superUserDashboard/collectedOrderInvoices/processors/InvoicesOverviewEconomic.vue"), "utf8" ); const overviewBoxSource = readFileSync( join(root, "src/views/dashboards/superUserDashboard/collectedOrderInvoices/InvoiceProcessorOverviewBox.vue"), "utf8" ); const listPaginationSource = readFileSync( join(root, "src/components/displays/pagination/models/SuperUserDashboard/CollectedOrderInvoicesListPagination.vue"), "utf8" ); const invoiceOrdersPaginationSource = readFileSync( join(root, "src/components/displays/pagination/models/SuperUserDashboard/InvoiceOrdersPagination.vue"), "utf8" ); const tableLabeledPaginationSource = readFileSync( join(root, "src/components/displays/pagination/TableLabeledPagination.vue"), "utf8" ); const periodSource = readFileSync( join(root, "src/views/dashboards/superUserDashboard/InvoicingBillingPeriod/InvoicingBillingPeriod.vue"), "utf8" ); const periodTopSource = readFileSync( join(root, "src/views/dashboards/superUserDashboard/InvoicingBillingPeriod/displays/layout/Top.vue"), "utf8" ); const periodLeftSource = readFileSync( join(root, "src/views/dashboards/superUserDashboard/InvoicingBillingPeriod/displays/layout/Left.vue"), "utf8" ); const periodRightSource = readFileSync( join(root, "src/views/dashboards/superUserDashboard/InvoicingBillingPeriod/displays/layout/Right.vue"), "utf8" ); const periodDateSelectorSource = readFileSync( join( root, "src/views/dashboards/superUserDashboard/InvoicingBillingPeriod/displays/InvoicingBillingPeriodDatePeriodSelector.vue" ), "utf8" ); const periodStatisticsSource = readFileSync( join( root, "src/views/dashboards/superUserDashboard/InvoicingBillingPeriod/displays/layout/InvoicingBillingPeriodStatistics.vue" ), "utf8" ); const periodViewImportSource = readFileSync( join( root, "src/views/dashboards/superUserDashboard/InvoicingBillingPeriod/imports/InvoicingBillingPeriodImportView.vue" ), "utf8" ); const periodViewInvoicePerOrderSource = readFileSync( join( root, "src/views/dashboards/superUserDashboard/InvoicingBillingPeriod/views/InvoicingBillingPeriodViewInvoicePerOrder.vue" ), "utf8" ); const periodViewSpecialArrangementsSource = readFileSync( join( root, "src/views/dashboards/superUserDashboard/InvoicingBillingPeriod/views/InvoicingBillingPeriodViewSpecialArrangements.vue" ), "utf8" ); const periodViewAllSource = readFileSync( join(root, "src/views/dashboards/superUserDashboard/InvoicingBillingPeriod/views/InvoicingBillingPeriodViewAll.vue"), "utf8" ); const periodObjectTreeSource = readFileSync( join(root, "src/views/dashboards/superUserDashboard/InvoicingBillingPeriod/components/InvoicingPeriodObjectTree.vue"), "utf8" ); const periodTreeNodeServiceSource = readFileSync( join(root, "src/views/dashboards/superUserDashboard/InvoicingBillingPeriod/services/invoicingPeriodTreeNodes.js"), "utf8" ); const buefyTreeSource = readFileSync(join(root, "src/components/buefy/tree/BuefyTree.vue"), "utf8"); const buefyTreeNodeSource = readFileSync(join(root, "src/components/buefy/tree/BuefyTreeNode.vue"), "utf8"); const periodViewSelfWashSource = readFileSync( join( root, "src/views/dashboards/superUserDashboard/InvoicingBillingPeriod/views/InvoicingBillingPeriodViewSelfWash.vue" ), "utf8" ); const xlvaskUsagePaginationSource = readFileSync( join(root, "src/components/displays/pagination/models/DepartmentPos/XLVaskUsagePagination.vue"), "utf8" ); const xlvaskUsagePeriodConstantsSource = readFileSync( join(root, "src/components/displays/department/pos/sync/xlvaskUsagePeriodConstants.js"), "utf8" ); const xlvaskUsageOrdersTableSource = readFileSync( join(root, "src/components/displays/department/pos/sync/xlvaskUsageOrdersTable.vue"), "utf8" ); const desktopHeaderSource = readFileSync(join(root, "src/components/viewport/page/headers/DesktopHeader.vue"), "utf8"); const tabletHeaderSource = readFileSync(join(root, "src/components/viewport/page/headers/TabletHeader.vue"), "utf8"); const mobileHeaderSource = readFileSync(join(root, "src/components/viewport/page/headers/MobileHeader.vue"), "utf8"); const queueMonitorSource = readFileSync( join(root, "src/components/viewport/page/headers/menu/CollectedInvoiceQueueMonitor.vue"), "utf8" ); const localeMessages = ["da", "en", "sv", "de", "no"].map((locale) => ({ locale, messages: readI18nRuntimeMessages(locale), })); describe("superuser invoices route wiring", () => { it("maps /superuser/invoices to CollectedOrderInvoices view", () => { expect(routerSource).toMatch(/name:\s*["']collectedorderinvoices["']/); expect(routerSource).toMatch(/path:\s*["']\/superuser\/invoices["']/); expect(routerSource).toContain("component: CollectedOrderInvoices"); }); it("maps monthly distribution routes to InvoiceDistributionMonthView", () => { expect(routerSource).toMatch(/name:\s*["']collectedorderinvoicesdistribution["']/); expect(routerSource).toMatch(/path:\s*["']\/superuser\/invoices\/distribution\/:year\/:month["']/); expect(routerSource).toMatch(/name:\s*["']collectedorderinvoicesdistributiontab["']/); expect(routerSource).toMatch(/path:\s*["']\/superuser\/invoices\/distribution\/:year\/:month\/:tab["']/); expect(routerSource).toContain("component: InvoiceDistributionMonthView"); }); it("keeps details route for a single collected invoice", () => { expect(routerSource).toMatch(/name:\s*["']collectedorderinvoice["']/); expect(routerSource).toMatch(/path:\s*["']\/superuser\/invoices\/:collectedOrderInvoiceId["']/); expect(routerSource).toContain("component: CollectedOrderInvoice"); }); }); describe("CollectedOrderInvoices tab container contract", () => { it("defines all expected top-level tabs", () => { expect(invoicesViewSource).toContain("{ name: 'Overblik', slot: 'overview'"); expect(invoicesViewSource).toContain("{ name: 'Fakturaer', slot: 'invoices'"); expect(invoicesViewSource).toContain("{ name: 'Periode', slot: 'period'"); expect(invoicesViewSource).toContain("slot: 'distribution'"); }); it("keeps active tab synced with route query", () => { expect(invoicesViewSource).toContain("const getQueryActiveTab = () => ("); expect(invoicesViewSource).toContain("watch(getQueryActiveTab, (newTab) => {"); expect(invoicesViewSource).toContain("activeTab.value = newTab;"); expect(invoicesViewSource).toContain("watch(activeTab, (newTab) => {"); expect(invoicesViewSource).toContain( "router.replace({ query: { ...router.currentRoute.value.query, activeTab: newTab } });" ); }); it("defaults to overview when route query is missing or invalid", () => { expect(invoicesViewSource).toContain("const activeTab = ref('overview');"); expect(invoicesViewSource).toContain("activeTab.value = 'overview';"); }); it("maps each tab slot to the expected component", () => { expect(invoicesViewSource).toContain("case 'overview':"); expect(invoicesViewSource).toContain("return CollectedOrderInvoicesOverview;"); expect(invoicesViewSource).toContain("case 'invoices':"); expect(invoicesViewSource).toContain("return CollectedOrderInvoicesListPagination;"); expect(invoicesViewSource).toContain("case 'period':"); expect(invoicesViewSource).toContain("return InvoicingBillingPeriod;"); expect(invoicesViewSource).toContain("case 'distribution':"); expect(invoicesViewSource).toContain("return InvoiceDistributionOverview;"); }); }); describe("Overfoersler queue monitor header contract", () => { it("mounts the monitor in desktop, tablet, and mobile headers", () => { expect(desktopHeaderSource).toContain( 'import CollectedInvoiceQueueMonitor from "@/components/viewport/page/headers/menu/CollectedInvoiceQueueMonitor.vue";' ); expect(tabletHeaderSource).toContain( 'import CollectedInvoiceQueueMonitor from "@/components/viewport/page/headers/menu/CollectedInvoiceQueueMonitor.vue";' ); expect(mobileHeaderSource).toContain( 'import CollectedInvoiceQueueMonitor from "@/components/viewport/page/headers/menu/CollectedInvoiceQueueMonitor.vue";' ); expect(desktopHeaderSource).toContain(""); expect(tabletHeaderSource).toContain(""); expect(mobileHeaderSource).toContain(''); }); it("keeps the monitor invoice-route gated and read-only", () => { expect(queueMonitorSource).toContain("isCollectedInvoiceQueueMonitorRoute(route.path)"); expect(queueMonitorSource).toContain("SessionUser.canAccessSuperUser()"); expect(queueMonitorSource).toContain("queue.monitor(50)"); expect(queueMonitorSource).toContain("queue.dismiss(job.id)"); expect(queueMonitorSource).toContain("queue.dismissTerminal()"); expect(queueMonitorSource).not.toContain("queue.run("); }); it("renders compact counts, dropdown rows, and clear controls", () => { expect(queueMonitorSource).toContain('data-testid="invoice-transfer-monitor"'); expect(queueMonitorSource).toContain('data-testid="invoice-transfer-monitor-button"'); expect(queueMonitorSource).toContain('data-testid="invoice-transfer-monitor-menu"'); expect(queueMonitorSource).toContain('data-testid="invoice-transfer-monitor-job"'); expect(queueMonitorSource).toContain('data-testid="invoice-transfer-monitor-clear-one"'); expect(queueMonitorSource).toContain('data-testid="invoice-transfer-monitor-clear-all"'); expect(queueMonitorSource).toContain("counts.queued ?? 0"); expect(queueMonitorSource).toContain("counts.in_progress ?? 0"); expect(queueMonitorSource).toContain("counts.failed ?? 0"); expect(queueMonitorSource).toContain("counts.completed ?? 0"); }); it("outlines the dropdown and marks uncleared failures with a red indicator", () => { expect(queueMonitorSource).toContain("hasFailedJobs"); expect(queueMonitorSource).toContain("'has-failed-transfers': hasFailedJobs"); expect(queueMonitorSource).toContain(".queue-monitor-menu .dropdown-content"); expect(queueMonitorSource).toContain("border: 2px solid #d1d5db"); expect(queueMonitorSource).toContain(".queue-monitor-progress.has-failed-transfers span"); expect(queueMonitorSource).toContain("background: #ef4444"); }); it("shows customer context and active queue refresh loading in the monitor", () => { expect(queueMonitorSource).toContain("const activeCount = computed"); expect(queueMonitorSource).toContain("const isRefreshingActiveQueue = computed"); expect(queueMonitorSource).toContain("jobCustomer(job)"); expect(queueMonitorSource).toContain("queue-monitor-customer"); expect(queueMonitorSource).toContain("'is-refreshing-active': isRefreshingActiveQueue"); expect(queueMonitorSource).toContain("queue-monitor-progress-loading"); }); it("turns the monitor progress fully green when all visible transfers completed successfully", () => { expect(queueMonitorSource).toContain("const isSuccessfulComplete = computed"); expect(queueMonitorSource).toContain("Number(counts.value.completed ?? 0) === total"); expect(queueMonitorSource).toContain("const displayProgressPercent = computed"); expect(queueMonitorSource).toContain("'is-successful-complete': isSuccessfulComplete"); expect(queueMonitorSource).toContain(".queue-monitor-progress.is-successful-complete"); expect(queueMonitorSource).toContain("background: #22c55e"); }); }); describe("Overview tab contract", () => { it("renders economic overview processor", () => { expect(overviewSource).toContain(""); }); it("loads economic stats via session request", () => { expect(overviewEconomicSource).toContain("SessionUser.request("); expect(overviewEconomicSource).toContain("'/collected-invoices/economic/overview'"); expect(overviewEconomicSource).toContain("'GET'"); }); it("updates all key counters from API response", () => { expect(overviewEconomicSource).toContain("total.value = result.total;"); expect(overviewEconomicSource).toContain("paid.value = result.paid;"); expect(overviewEconomicSource).toContain("unpaid.value = result.unpaid;"); expect(overviewEconomicSource).toContain("overdue.value = result.overdue;"); expect(overviewEconomicSource).toContain("booked.value = result.booked;"); expect(overviewEconomicSource).toContain("draft.value = result.draft;"); expect(overviewEconomicSource).toContain("sent.value = result.sent;"); expect(overviewEconomicSource).toContain("error.value = result.error;"); }); it("keeps both overview actions wired to refresh after execution", () => { expect(overviewEconomicSource).toContain("runCheckDrafts()"); expect(overviewEconomicSource).toContain("runCheckErrors()"); expect(overviewEconomicSource).toContain("refresh();"); }); it("supports error lifecycle and dismiss interaction", () => { expect(overviewEconomicSource).toContain("const addErrorMessage ="); expect(overviewEconomicSource).toContain("const removeErrorMessage ="); expect(overviewEconomicSource).toContain("const clearErrorMessages ="); expect(overviewEconomicSource).toContain("const hasErrorMessages ="); expect(overviewEconomicSource).toContain(''); }); it("keeps refresh propagation through overview card boxes", () => { expect(overviewEconomicSource).toContain(" { it("requires options/count/loading props and optional onClick handler", () => { expect(overviewBoxSource).toContain("options:"); expect(overviewBoxSource).toContain("count:"); expect(overviewBoxSource).toContain("loading:"); expect(overviewBoxSource).toContain("onClick:"); }); it("shows spinner while loading and emits refresh otherwise", () => { expect(overviewBoxSource).toContain('v-if="loading"'); expect(overviewBoxSource).toContain("fa-spinner fa-spin"); expect(overviewBoxSource).toContain("fa-sync-alt"); expect(overviewBoxSource).toContain("@click=\"$emit('refresh')\""); }); it("shows View action only when click callback exists", () => { expect(overviewBoxSource).toContain("const hasClickAction = () => {"); expect(overviewBoxSource).toContain('v-if="hasClickAction()"'); expect(overviewBoxSource).toContain("View"); expect(overviewBoxSource).toContain('v-if="!hasClickAction()"'); }); }); describe("Fakturaer tab contract", () => { it("configures endpoint, sorting, filters, and optional auto-load", () => { expect(listPaginationSource).toContain('setEndpoint("/collected-invoices", false);'); expect(listPaginationSource).toContain('setEndpoint("/collected-invoices/ready-to-invoice", false);'); expect(listPaginationSource).toContain('setOrder("closed_at", "asc", false);'); expect(listPaginationSource).toContain("setHideSearchField(props.hideSearch);"); expect(listPaginationSource).toContain('setFilter("customer_number", props.setCustomerFilter, false);'); expect(listPaginationSource).toContain("setFilter(\"closed_at\", 'is_null', false);"); expect(listPaginationSource).toContain("setFilter(\"booked_invoice_id\", 'is_null', false);"); expect(listPaginationSource).toContain("if (props.autoLoad) {"); expect(listPaginationSource).toContain("loadList();"); }); it("supports mass compare flow across all pages with auth and ordering", () => { expect(listPaginationSource).toContain("const fetchAllPages = async () => {"); expect(listPaginationSource).toContain( "const safeItemsPerPage = Math.max(1, Number(metaItemsPerPage.value || 0));" ); expect(listPaginationSource).toContain( "const totalPages = Math.ceil(Number(metaTotalItems.value || 0) / safeItemsPerPage);" ); expect(listPaginationSource).toContain("const token = localStorage.getItem('token');"); expect(listPaginationSource).toContain("if (totalPages > 20) {"); expect(listPaginationSource).toContain("axios.get(API_URL + endpoint.value"); expect(listPaginationSource).toContain("Authorization: `Bearer ${token}`"); expect(listPaginationSource).toContain("order: `${orderBy.value}:${orderDirection.value}`"); }); it("keeps compare modal workflow and button safeguards", () => { expect(listPaginationSource).toContain("initCompareDataStructure();"); expect(listPaginationSource).toContain("clearProgress();"); expect(listPaginationSource).toContain("setTitle('Sammenlign fakturaer med E-conomic');"); expect(listPaginationSource).toContain("getParsedDataFormatFunction()(allInvoices);"); expect(listPaginationSource).toContain("await showMassDataInsertModal();"); expect(listPaginationSource).toContain("Sammenlign alle med E-conomic"); expect(listPaginationSource).toContain(':disabled="isMassComparing || isLoading || !list || list.length === 0"'); }); }); describe("Invoice orders pagination contract", () => { it("does not expose the invalid order error-status filter", () => { expect(invoiceOrdersPaginationSource).not.toContain("pagination.error_status"); expect(invoiceOrdersPaginationSource).not.toContain("setFilter('error_message'"); expect(invoiceOrdersPaginationSource).not.toContain('setFilter("error_message"'); }); it("collapses filters behind a text toggle when hide-filter is enabled", () => { expect(invoiceOrdersPaginationSource).toContain("const areFiltersVisible = ref(!props.hideFilter);"); expect(invoiceOrdersPaginationSource).toContain(':show-filters="areFiltersVisible"'); expect(invoiceOrdersPaginationSource).toContain(':hide-search="props.hideSearch"'); expect(invoiceOrdersPaginationSource).toContain(':hide-pagination="props.hidePagination"'); expect(invoiceOrdersPaginationSource).toContain('