Files
pleno-vue/tests/unit/invoicing-period-tree-snapshot.spec.js
T
Jeppe B 7a5ee1aa5b Fix invoice period tree review findings (#253)
## Summary
- preserve complete snapshot item payloads during inline edits and
reject partial text-field payloads
- force snapshot refreshes after parent/mutation changes with one
bounded recovery retry
- make legacy tree-action fallback create, confirm, and apply a fresh
compatible preview
- keep collection labeling localized and report the correct changed
count

## Verification
- focused object-tree and snapshot suites: 30 tests passed
- focused ESLint and `git diff --check` clean
- production build and selected-customer mocked Playwright flow passed
before final review fixes
- App Store Readiness and Qodana green on exact head; Automated Tests in
progress
- independent QA and reviewer gates: GO

Resolves all inline review threads on the current head.
2026-08-03 13:04:19 +02:00

169 lines
6.3 KiB
JavaScript

import { describe, expect, it, vi } from "vitest";
import {
buildCompleteSnapshotRootNodes,
createInvoicingPeriodTreeSnapshotLoader,
isTreeEndpointUnavailable,
normalizeInvoicingPeriodTreeSnapshot,
} from "@/views/dashboards/superUserDashboard/InvoicingBillingPeriod/services/invoicingPeriodTreeSnapshot.ts";
import {
normalizeInvoiceCollectionActionPreview,
renderInvoiceCollectionActionPreviewHtml,
} from "@/views/dashboards/superUserDashboard/InvoicingBillingPeriod/services/invoiceCollectionActionPreview.ts";
const payload = (revision = "rev-1") => ({
complete: true,
snapshot_revision: revision,
customer_number: 2001,
date_from: "2026-06-01",
date_to: "2026-06-30",
capabilities: {
object_tree_v2: true,
actions: { merge_collections: true, queue_economic: false },
},
customer: { customer_number: 2001, customer_name: "ACME" },
collections: [
{
id: 3001,
in_selected_period: true,
complete_order_count: 2,
complete_total_net_amount: 300,
period_order_count: 1,
period_total_net_amount: 200,
orders: [
{
id: 9001,
invoice_collection_id: 3001,
in_selected_period: true,
total_net_amount: 200,
items: [{ id: 501, order_id: 9001, product_name: "Wash", quantity: 1, price: 200 }],
attachments: [],
bookings: [],
xlvask: [],
},
{
id: 9002,
invoice_collection_id: 3001,
in_selected_period: false,
total_net_amount: 100,
items: [],
attachments: [],
bookings: [],
xlvask: [],
},
],
},
],
uncollected_orders: [],
agreements: [],
payments: [],
economic_invoices: [],
});
const labels = {
collection: (id) => `Collection ${id}`,
ordersWithoutCollection: "Loose orders",
orders: "Orders",
items: "Items",
attachments: { certificates: "Certificates", images: "Images", other: "Attachments" },
bookings: "Bookings",
booking: (id) => `Booking ${id}`,
xlvask: "XL Vask",
economic: "Economic",
agreement: (id) => `Agreement ${id}`,
payment: (id) => `Payment ${id}`,
order: (id) => `Order ${id}`,
orderItemFallback: (id) => `Item ${id}`,
attachmentFallback: (id) => `Attachment ${id}`,
bookingItemFallback: (id) => `Booking item ${id}`,
xlvaskItemFallback: (index) => `XL item ${index}`,
};
describe("invoicing period selected-customer tree snapshot", () => {
it("normalizes a complete capability-gated snapshot and keeps off-period orders in the complete collection", () => {
const snapshot = normalizeInvoicingPeriodTreeSnapshot({ data: { data: payload() } });
expect(snapshot).toMatchObject({
complete: true,
source: "snapshot",
snapshot_revision: "rev-1",
capabilities: { object_tree_v2: true },
});
const roots = buildCompleteSnapshotRootNodes(snapshot, labels);
expect(roots[0].label).toBe("Collection 3001");
expect(roots[0].meta).toMatchObject({
periodOrderCount: 1,
periodTotalNetAmount: 200,
completeOrderCount: 2,
completeTotalNetAmount: 300,
offPeriodOrderCount: 1,
offPeriodTotalNetAmount: 100,
});
const orderNodes = roots[0].children[0].children;
expect(orderNodes.map((node) => node.id)).toEqual(["order:9001", "order:9002"]);
expect(orderNodes[1].meta.inSelectedPeriod).toBe(false);
expect(orderNodes[0].children[0].children[0].id).toBe("order_item:501");
});
it("rejects incomplete or capability-disabled payloads", () => {
expect(normalizeInvoicingPeriodTreeSnapshot({ ...payload(), complete: false })).toBeNull();
expect(
normalizeInvoicingPeriodTreeSnapshot({
...payload(),
capabilities: { object_tree_v2: false, actions: {} },
})
).toBeNull();
});
it("deduplicates identical loads and aborts a superseded customer request", async () => {
const pending = [];
const request = vi.fn(
(_url, _method, parameters, _catcher, _then, options) =>
new Promise((resolve, reject) => {
options.signal.addEventListener("abort", () =>
reject(Object.assign(new Error("aborted"), { name: "AbortError" }))
);
pending.push({ parameters, resolve, signal: options.signal });
})
);
const loader = createInvoicingPeriodTreeSnapshotLoader(request);
const first = loader.load({ customerNumber: 2001, dateFrom: "2026-06-01", dateTo: "2026-06-30" });
const duplicate = loader.load({ customerNumber: 2001, dateFrom: "2026-06-01", dateTo: "2026-06-30" });
expect(duplicate).toBe(first);
const second = loader.load({ customerNumber: 2002, dateFrom: "2026-06-01", dateTo: "2026-06-30" });
await expect(first).rejects.toMatchObject({ name: "AbortError" });
expect(pending[0].signal.aborted).toBe(true);
pending[1].resolve({
data: { data: { ...payload("rev-2"), customer_number: 2002, customer: { customer_number: 2002 } } },
});
await expect(second).resolves.toMatchObject({ snapshot_revision: "rev-2", customer_number: 2002 });
expect(request).toHaveBeenCalledTimes(2);
});
it("does not classify internal request cancellation as endpoint unavailability", () => {
expect(isTreeEndpointUnavailable(Object.assign(new Error("aborted"), { name: "AbortError" }))).toBe(false);
expect(isTreeEndpointUnavailable({ code: "ERR_CANCELED" })).toBe(false);
expect(isTreeEndpointUnavailable({ response: { status: 404 } })).toBe(true);
expect(isTreeEndpointUnavailable({ response: { status: 501 } })).toBe(true);
});
it("renders escaped concrete changes, merge target, and off-period impact", () => {
const preview = normalizeInvoiceCollectionActionPreview({
preview_id: "preview-1",
confirmation_phrase: "CONFIRM",
summary: { collection_count: 2, changed_count: 1, off_period_order_count: 1, off_period_total_net_amount: 100 },
target_invoice_collection_id: 3002,
changes: [{ message: "Move <script>alert(1)</script>" }],
});
const html = renderInvoiceCollectionActionPreviewHtml({
preview,
t: (key, params = {}) => `${key}:${JSON.stringify(params)}`,
formatCurrency: (value) => `${value} DKK`,
});
expect(html).toContain("3002");
expect(html).toContain("1");
expect(html).toContain("100 DKK");
expect(html).toContain("&lt;script&gt;");
expect(html).not.toContain("<script>");
});
});