- Introduced `.prettierrc.json` to enforce consistent code formatting across the project. - Updated unit and e2e test files to address formatting issues, improve readability, and ensure alignment with the new Prettier configuration.
345 lines
17 KiB
JavaScript
345 lines
17 KiB
JavaScript
import { readFileSync } from "node:fs";
|
|
import { join } from "node:path";
|
|
import { describe, expect, it } from "vitest";
|
|
|
|
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 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 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"
|
|
);
|
|
|
|
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("watch(() => router.currentRoute.value.query.activeTab");
|
|
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("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("updates date filters via a dedicated range handler", () => {
|
|
expect(invoiceOrdersPaginationSource).toContain(
|
|
"const onDateRangeSelected = (newSelectionStartDate, 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.variables.start.value.toISOString().split('T')[0]");
|
|
expect(periodRightSource).toContain("dateTo: dates.variables.end.value.toISOString().split('T')[0]");
|
|
expect(periodRightSource).toContain("watch(");
|
|
expect(periodRightSource).toContain("[() => dates.variables.start.value, () => dates.variables.end.value]");
|
|
expect(periodRightSource).toContain("getPeriod();");
|
|
});
|
|
|
|
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.variables.currentView.value = type.view;");
|
|
});
|
|
|
|
it("keeps date selector update action and auto change emission", () => {
|
|
expect(periodDateSelectorSource).toContain("dates.functions.setSelection(startDate, endDate);");
|
|
expect(periodDateSelectorSource).toContain('v-bind:auto-emit-change="true"');
|
|
expect(periodDateSelectorSource).toContain(":class=\"{'is-loading': isReloadAnimationActive}\"");
|
|
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");
|
|
});
|
|
|
|
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");
|
|
});
|
|
});
|
|
|
|
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",
|
|
];
|
|
|
|
expect(selectableKeys.sort()).toEqual(expectedSelectableKeys.sort());
|
|
selectableKeys.forEach((key) => {
|
|
expect(periodViewImportSource).toContain(`${key}:`);
|
|
expect(periodViewImportSource).toContain(`${key}: InvoicingBillingPeriodViewAll`);
|
|
});
|
|
});
|
|
|
|
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.`);');
|
|
});
|
|
});
|