337 lines
15 KiB
JavaScript
337 lines
15 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 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("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'");
|
|
});
|
|
|
|
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;");
|
|
});
|
|
});
|
|
|
|
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 totalPages = Math.ceil(metaTotalItems.value / metaItemsPerPage.value);");
|
|
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("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(() => dates.variables.start.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.`);");
|
|
});
|
|
});
|