Include selected invoice-period dates when importing XLVask usage from the Selvvask period view.
628 lines
34 KiB
JavaScript
628 lines
34 KiB
JavaScript
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 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).toContain("name: 'collectedorderinvoices'");
|
|
expect(routerSource).toContain("path: '/superuser/invoices'");
|
|
expect(routerSource).toContain("component: CollectedOrderInvoices");
|
|
});
|
|
|
|
it("maps monthly distribution routes to InvoiceDistributionMonthView", () => {
|
|
expect(routerSource).toContain("name: 'collectedorderinvoicesdistribution'");
|
|
expect(routerSource).toContain("path: '/superuser/invoices/distribution/:year/:month'");
|
|
expect(routerSource).toContain("name: 'collectedorderinvoicesdistributiontab'");
|
|
expect(routerSource).toContain("path: '/superuser/invoices/distribution/:year/:month/:tab'");
|
|
expect(routerSource).toContain("component: InvoiceDistributionMonthView");
|
|
});
|
|
|
|
it("keeps details route for a single collected invoice", () => {
|
|
expect(routerSource).toContain("name: 'collectedorderinvoice'");
|
|
expect(routerSource).toContain("path: '/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("<CollectedInvoiceQueueMonitor />");
|
|
expect(tabletHeaderSource).toContain("<CollectedInvoiceQueueMonitor />");
|
|
expect(mobileHeaderSource).toContain('<CollectedInvoiceQueueMonitor class="mobile-queue-monitor" />');
|
|
});
|
|
|
|
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("<invoices-overview-economic />");
|
|
});
|
|
|
|
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('<button class="delete" @click="removeErrorMessage(index)"></button>');
|
|
});
|
|
|
|
it("keeps refresh propagation through overview card boxes", () => {
|
|
expect(overviewEconomicSource).toContain("<InvoiceProcessorOverviewBox");
|
|
expect(overviewEconomicSource).toContain('@refresh="refresh"');
|
|
});
|
|
});
|
|
|
|
describe("InvoiceProcessorOverviewBox contract", () => {
|
|
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("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('<template #afterSearch v-if="props.hideFilter">');
|
|
expect(invoiceOrdersPaginationSource).toContain('data-testid="invoice-orders-toggle-filters"');
|
|
expect(invoiceOrdersPaginationSource).toContain('t("pagination.show_filters")');
|
|
expect(invoiceOrdersPaginationSource).toContain('t("pagination.hide_filters")');
|
|
expect(tableLabeledPaginationSource).toContain("showFilters:");
|
|
expect(tableLabeledPaginationSource).toContain("hideSearch:");
|
|
expect(tableLabeledPaginationSource).toContain("hidePagination:");
|
|
expect(tableLabeledPaginationSource).toContain(':hide-search="props.hideSearch"');
|
|
expect(tableLabeledPaginationSource).toContain('v-if="!props.hideSearch || $slots.buttons"');
|
|
expect(tableLabeledPaginationSource).toContain('v-if="showFilters"');
|
|
expect(tableLabeledPaginationSource).toContain('v-if="!props.hidePagination"');
|
|
expect(tableLabeledPaginationSource).toContain('data-testid="table-labeled-pagination-filters"');
|
|
});
|
|
|
|
it("updates date filters via a dedicated range handler", () => {
|
|
expect(invoiceOrdersPaginationSource).toContain(
|
|
"const onDateRangeSelected = (newSelectionStartDate, newSelectionToDate) => {"
|
|
);
|
|
expect(invoiceOrdersPaginationSource).toContain("formatLocalDateOnly(newSelectionStartDate)");
|
|
expect(invoiceOrdersPaginationSource).toContain("formatLocalDateOnly(newSelectionToDate)");
|
|
expect(invoiceOrdersPaginationSource).toContain("date_from.value = formattedStartDate;");
|
|
expect(invoiceOrdersPaginationSource).toContain("date_to.value = formattedEndDate;");
|
|
expect(invoiceOrdersPaginationSource).toContain('setFilter("created_at-date_from", formattedStartDate, true);');
|
|
expect(invoiceOrdersPaginationSource).toContain('setFilter("created_at-date_to", formattedEndDate, true);');
|
|
expect(invoiceOrdersPaginationSource).toContain('<DatePeriodSelector :on-selection-change="onDateRangeSelected"');
|
|
});
|
|
});
|
|
|
|
describe("Periode tab contract", () => {
|
|
it("assembles top/left/right/bottom sections in the billing-period layout", () => {
|
|
expect(periodSource).toContain("<InvoicingBillingPeriodLayoutDefault>");
|
|
expect(periodSource).toContain("<template #content-top>");
|
|
expect(periodSource).toContain("<template #content-left>");
|
|
expect(periodSource).toContain("<template #content-right>");
|
|
expect(periodSource).toContain("<template #content-bottom>");
|
|
});
|
|
|
|
it("keeps top selector and left dynamic view renderer", () => {
|
|
expect(periodTopSource).toContain("<InvoicingBillingPeriodDatePeriodSelector/>");
|
|
expect(periodLeftSource).toContain('<Component v-bind:is="view.computed.currentViewComponent.value" />');
|
|
});
|
|
|
|
it("fetches period dataset and refreshes when date range changes", () => {
|
|
expect(periodRightSource).toContain("SessionUser.request(");
|
|
expect(periodRightSource).toContain("'/superuser/invoicing/period'");
|
|
expect(periodRightSource).toContain("dateFrom: dates.computed.formattedStartDate.value");
|
|
expect(periodRightSource).toContain("dateTo: dates.computed.formattedEndDate.value");
|
|
expect(periodRightSource).toContain("const applyRoutePeriodDates = () =>");
|
|
expect(periodRightSource).toContain("dates.functions.setSelection(requestedStartDate, requestedEndDate);");
|
|
expect(periodRightSource).toContain("startDate: dates.computed.formattedStartDate.value");
|
|
expect(periodRightSource).toContain("endDate: dates.computed.formattedEndDate.value");
|
|
expect(periodRightSource).toContain("watch(");
|
|
expect(periodRightSource).toContain("[() => dates.variables.start.value, () => dates.variables.end.value]");
|
|
expect(periodRightSource).toContain("const loadPeriodForDateChange = () =>");
|
|
expect(periodRightSource).toContain("periodPaging.isDateLoading = true;");
|
|
expect(periodRightSource).toContain("getPeriod({ forceRefresh: true, showLoading: true })");
|
|
expect(periodRightSource).toContain("status.loading = showLoading;");
|
|
});
|
|
|
|
it("warms current and previous month period pages and preloads adjacent pages", () => {
|
|
expect(periodRightSource).toContain("const fetchAndCachePeriodPage = async");
|
|
expect(periodRightSource).toContain("preloadAdjacentPeriodPages(requestParameters, paginationMeta)");
|
|
expect(periodRightSource).toContain("const warmCurrentAndPreviousPeriodMonths = () =>");
|
|
expect(periodRightSource).toContain("const previousMonth = new Date(now.getFullYear(), now.getMonth() - 1, 1)");
|
|
expect(periodRightSource).toContain('periodViews = Array.from(new Set(["all", getBackendPeriodView()]))');
|
|
expect(periodRightSource).toContain("periodWarm: 1");
|
|
expect(periodRightSource).toContain("setCachedPeriodPage(cacheKey, normalizedPeriodResult, paginationMeta)");
|
|
expect(periodRightSource).toContain("periodCacheRequestInFlight");
|
|
});
|
|
|
|
it("keeps period statistics free of normal-path debug console noise", () => {
|
|
expect(periodStatisticsSource).not.toContain("Response from fixed pricing distribution API");
|
|
expect(periodStatisticsSource).not.toContain("Fixed pricing distribution fetched successfully");
|
|
expect(periodStatisticsSource).not.toContain("Response from vehicle subscription distribution API");
|
|
expect(periodStatisticsSource).not.toContain("Vehicle subscription distribution fetched successfully");
|
|
expect(periodStatisticsSource).not.toContain("Department 75 total calculation resulted in null");
|
|
expect(periodStatisticsSource).not.toContain("onMounted(() =>");
|
|
});
|
|
|
|
it("defines the open invoice collection label in loaded locale messages", () => {
|
|
localeMessages.forEach(({ messages }) => {
|
|
expect(messages.global.open).toBeTruthy();
|
|
});
|
|
});
|
|
|
|
it("exposes all right-panel period views and click switching", () => {
|
|
expect(periodRightSource).toContain("view: 'all'");
|
|
expect(periodRightSource).toContain("view: 'invoice_per_order'");
|
|
expect(periodRightSource).toContain("view: 'fixed_pricing'");
|
|
expect(periodRightSource).toContain("view: 'tank_cleaning'");
|
|
expect(periodRightSource).toContain("view: 'special_arrangements'");
|
|
expect(periodRightSource).toContain("view: 'vehicle_subscriptions'");
|
|
expect(periodRightSource).toContain("view: 'possible_duplicates'");
|
|
expect(periodRightSource).toContain("view: 'self_wash'");
|
|
expect(periodRightSource).toContain("pendingPeriodView.value = type.view;");
|
|
expect(periodRightSource).toContain("view.variables.currentView.value = viewToCommit");
|
|
});
|
|
|
|
it("keeps date selector update action and auto change emission", () => {
|
|
expect(periodDateSelectorSource).toContain("showReloadAnimation();");
|
|
expect(periodDateSelectorSource).toContain("dates.functions.setSelection(startDate, endDate);");
|
|
expect(periodDateSelectorSource).toContain('v-bind:auto-emit-change="true"');
|
|
expect(periodDateSelectorSource).toContain(
|
|
"const isReloadButtonLoading = computed(() => isReloadAnimationActive.value || periodPaging.isDateLoading)"
|
|
);
|
|
expect(periodDateSelectorSource).toContain(":class=\"{'is-loading': isReloadButtonLoading}\"");
|
|
expect(periodDateSelectorSource).toContain(
|
|
'@click="onSelectionChange(dates.variables.start.value, dates.variables.end.value); showReloadAnimation();"'
|
|
);
|
|
});
|
|
|
|
it("keeps import-view mapping for all period view keys", () => {
|
|
expect(periodViewImportSource).toContain("home: InvoicingBillingPeriodViewHome");
|
|
expect(periodViewImportSource).toContain("fixed_pricing: InvoicingBillingPeriodViewAll");
|
|
expect(periodViewImportSource).toContain("vehicle_subscriptions: InvoicingBillingPeriodViewAll");
|
|
expect(periodViewImportSource).toContain("tank_cleaning: InvoicingBillingPeriodViewAll");
|
|
expect(periodViewImportSource).toContain("all: InvoicingBillingPeriodViewAll");
|
|
expect(periodViewImportSource).toContain("special_arrangements: InvoicingBillingPeriodViewAll");
|
|
expect(periodViewImportSource).toContain("invoice_per_order: InvoicingBillingPeriodViewAll");
|
|
expect(periodViewImportSource).toContain("possible_duplicates: InvoicingBillingPeriodViewAll");
|
|
expect(periodViewImportSource).toContain("self_wash: InvoicingBillingPeriodViewSelfWash");
|
|
});
|
|
|
|
it("uses the selected period as the only local Selvvask date filter", () => {
|
|
expect(periodViewSelfWashSource).toContain(':initial-date-from="dates.computed.formattedStartDate.value"');
|
|
expect(periodViewSelfWashSource).toContain(':initial-date-to="dates.computed.formattedEndDate.value"');
|
|
expect(periodViewSelfWashSource).toContain(':inherit-period-filters="true"');
|
|
expect(periodViewSelfWashSource).toContain(':load-all-at-once="true"');
|
|
expect(xlvaskUsagePaginationSource).toContain("inheritPeriodFilters");
|
|
expect(xlvaskUsagePaginationSource).toContain("loadAllAtOnce");
|
|
expect(xlvaskUsagePeriodConstantsSource).toContain("export const SELFWASH_PERIOD_ALL_LIMIT = 10000");
|
|
expect(xlvaskUsagePaginationSource).toContain("setMetaItemsPerPage(SELFWASH_PERIOD_ALL_LIMIT, false)");
|
|
expect(xlvaskUsagePaginationSource).toContain(
|
|
"const shouldShowLocalFilters = computed(() => !props.inheritPeriodFilters)"
|
|
);
|
|
expect(xlvaskUsagePaginationSource).toContain('v-if="shouldShowLocalFilters"');
|
|
expect(xlvaskUsagePaginationSource).toContain('v-if="!hideSearchField && shouldShowLocalFilters"');
|
|
expect(xlvaskUsagePaginationSource).toContain('v-if="!props.loadAllAtOnce"');
|
|
expect(xlvaskUsagePaginationSource).toContain("const buildImportUsageParams = () => {");
|
|
expect(xlvaskUsagePaginationSource).toContain("dateFrom: props.initialDateFrom");
|
|
expect(xlvaskUsagePaginationSource).toContain("dateTo: props.initialDateTo");
|
|
expect(xlvaskUsagePaginationSource).toContain("buildImportUsageParams()");
|
|
});
|
|
|
|
it("loads Selvvask selector counts and progress from the selected period", () => {
|
|
expect(periodRightSource).toContain("isUsageOrderAttachedToOrder");
|
|
expect(periodRightSource).toContain("const selfWashCounts = ref(emptySelectorCounts())");
|
|
expect(periodRightSource).toContain("buildSelfWashCountsCacheKey");
|
|
expect(periodRightSource).toContain("getCachedSelfWashCounts");
|
|
expect(periodRightSource).toContain("setCachedSelfWashCounts");
|
|
expect(periodRightSource).toContain("'/modules/xlvask/services/usage/orders'");
|
|
expect(periodRightSource).toContain("limit: SELFWASH_PERIOD_ALL_LIMIT");
|
|
expect(periodRightSource).toContain("StartTime-date_from:${dates.computed.formattedStartDate.value}");
|
|
expect(periodRightSource).toContain("StartTime-date_to:${dates.computed.formattedEndDate.value}");
|
|
expect(periodRightSource).toContain("if (type.name === 'self_wash')");
|
|
expect(periodRightSource).toContain("requires_action: Math.max(0, total - completed)");
|
|
expect(periodRightSource).not.toContain("showCounts: false");
|
|
expect(periodRightSource).not.toContain("showProgress: false");
|
|
});
|
|
|
|
it("keeps Selvvask usage pagination scoped to the period view", () => {
|
|
expect(xlvaskUsagePaginationSource).toContain("usePaginatedList");
|
|
expect(xlvaskUsagePaginationSource).toContain("provide(PaginatedListKey, paginatedList)");
|
|
expect(xlvaskUsageOrdersTableSource).toContain("usePaginatedListInstance");
|
|
expect(xlvaskUsageOrdersTableSource).toContain(
|
|
"const { orderBy, orderDirection, setOrder, loadList } = usePaginatedListInstance();"
|
|
);
|
|
});
|
|
|
|
it("keeps Selvvask usage entries wrapped inside the period content area", () => {
|
|
expect(xlvaskUsageOrdersTableSource).toContain("xlvask-usage-card-primary");
|
|
expect(xlvaskUsageOrdersTableSource).toContain("xlvask-usage-card-status");
|
|
expect(xlvaskUsageOrdersTableSource).toContain("xlvask-usage-chip-text");
|
|
expect(xlvaskUsageOrdersTableSource).toContain("getCachedXlvaskUsageAmount");
|
|
expect(xlvaskUsageOrdersTableSource).toContain("setCachedXlvaskUsageAmount");
|
|
expect(xlvaskUsageOrdersTableSource).toContain("object?.total_net_amount");
|
|
expect(xlvaskUsageOrdersTableSource).toContain("status !== 'failed'");
|
|
expect(xlvaskUsageOrdersTableSource).not.toContain("Kunne ikke behandles");
|
|
expect(xlvaskUsageOrdersTableSource).toContain("only fetch one-time fast-link details on demand");
|
|
expect(xlvaskUsageOrdersTableSource).not.toContain("scheduleFastLink(object)");
|
|
expect(xlvaskUsageOrdersTableSource).toContain("grid-template-columns: minmax(0, 1fr) minmax(9rem, 15rem);");
|
|
expect(xlvaskUsageOrdersTableSource).toContain("xlvask-order-comparison-columns");
|
|
expect(xlvaskUsageOrdersTableSource).toContain("@media screen and (max-width: 1350px)");
|
|
});
|
|
|
|
it("keeps period subview navigation on valid keys", () => {
|
|
expect(periodViewInvoicePerOrderSource).toContain("view.functions.setCurrentView('vehicle_subscriptions')");
|
|
expect(periodViewSpecialArrangementsSource).toContain("view.functions.setCurrentView('vehicle_subscriptions')");
|
|
expect(periodViewInvoicePerOrderSource).not.toContain("wash_subscriptions");
|
|
expect(periodViewSpecialArrangementsSource).not.toContain("wash_subscriptions");
|
|
});
|
|
|
|
it("keeps e-conomic drafts out of completed selector counts and renders a yellow draft segment", () => {
|
|
expect(periodRightSource).toContain("const isCustomerDraftBlocked =");
|
|
expect(periodRightSource).toContain("const draft = entries.filter(entry => isCustomerDraftBlocked(entry)).length");
|
|
expect(periodRightSource).toContain("const completed = Math.max(0, entries.length - requires_action - draft)");
|
|
expect(periodRightSource).toContain("return `${type.displayName} (${typeCounts.completed}/${typeCounts.total})`;");
|
|
expect(periodRightSource).toContain("period-selector-progress__draft");
|
|
expect(periodRightSource).toContain("background: #f6c344");
|
|
});
|
|
|
|
it("renders draft-blocked customers with a warning tag matching the completed tag shape", () => {
|
|
expect(periodViewAllSource).toContain('class="tag is-warning is-light is-small"');
|
|
expect(periodViewAllSource).toContain('aria-disabled="true"');
|
|
expect(periodViewAllSource).toContain("fa-file-alt");
|
|
expect(periodViewAllSource).not.toContain('class="button is-small is-warning is-light"');
|
|
});
|
|
|
|
it("scopes customer flag indicators to flags visible in the current customer card", () => {
|
|
expect(periodViewAllSource).toContain("const isFlagInCustomerCardScope =");
|
|
expect(periodViewAllSource).toContain("const getCustomerScopedFlags =");
|
|
expect(periodViewAllSource).toContain("const getCustomerActiveFlagCounts =");
|
|
expect(periodViewAllSource).toContain("return getFlagCounts(scopedFlags);");
|
|
expect(periodViewAllSource).toContain(
|
|
"const getCustomerCardFlags = (customer: any) => getCustomerScopedFlags(customer)"
|
|
);
|
|
expect(periodViewAllSource).toContain(':invoice-period-flags="getCustomerScopedFlags(customer)"');
|
|
expect(periodViewAllSource).toContain('targetType === "customer"');
|
|
expect(periodViewAllSource).toContain('targetType === "collected_order_invoice"');
|
|
});
|
|
|
|
it("groups possible duplicates by vehicle plate and date for comparison", () => {
|
|
expect(periodViewAllSource).toContain("buildPossibleDuplicateGroups");
|
|
expect(periodViewAllSource).toContain("possibleDuplicateGroupsInCurrentView");
|
|
expect(periodViewAllSource).toContain('data-testid="`invoicing-period-duplicate-group-${duplicateGroup.testId}`"');
|
|
expect(periodViewAllSource).toContain("duplicate-comparison-row");
|
|
expect(periodViewAllSource).toContain(':group-invoice-collection="false"');
|
|
expect(periodViewAllSource).not.toContain(':hide-pagination="false"');
|
|
expect(periodRightSource).toContain("getCountEntriesForType");
|
|
expect(periodRightSource).toContain('typeName !== "possible_duplicates"');
|
|
expect(periodRightSource).toContain("buildPossibleDuplicateGroups(entries)");
|
|
});
|
|
});
|
|
|
|
describe("Periode runtime view switching", () => {
|
|
it("keeps all selectable period views mapped in availableViews", () => {
|
|
const selectorViewMatches = [...periodRightSource.matchAll(/view:\s*'([^']+)'/g)].map((m) => m[1]);
|
|
const selectableKeys = Array.from(new Set(selectorViewMatches));
|
|
const expectedSelectableKeys = [
|
|
"all",
|
|
"invoice_per_order",
|
|
"fixed_pricing",
|
|
"tank_cleaning",
|
|
"special_arrangements",
|
|
"vehicle_subscriptions",
|
|
"possible_duplicates",
|
|
"self_wash",
|
|
];
|
|
|
|
expect(selectableKeys.sort()).toEqual(expectedSelectableKeys.sort());
|
|
selectableKeys.forEach((key) => {
|
|
expect(periodViewImportSource).toContain(`${key}:`);
|
|
});
|
|
expectedSelectableKeys
|
|
.filter((key) => key !== "self_wash")
|
|
.forEach((key) => {
|
|
expect(periodViewImportSource).toContain(`${key}: InvoicingBillingPeriodViewAll`);
|
|
});
|
|
expect(periodViewImportSource).toContain("self_wash: InvoicingBillingPeriodViewSelfWash");
|
|
});
|
|
|
|
it("keeps home view key available for initial period load", () => {
|
|
expect(periodViewImportSource).toContain("home: InvoicingBillingPeriodViewHome");
|
|
expect(periodViewImportSource).toContain('const currentView = ref<keyof typeof availableViews>("home")');
|
|
});
|
|
|
|
it("guards setCurrentView with available view existence checks", () => {
|
|
expect(periodViewImportSource).toContain("const setCurrentView = (view: keyof typeof availableViews) => {");
|
|
expect(periodViewImportSource).toContain("if (availableViews[view]) {");
|
|
expect(periodViewImportSource).toContain('console.warn(`View "${view}" does not exist in available views.`);');
|
|
});
|
|
});
|