## 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.
169 lines
6.3 KiB
JavaScript
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("<script>");
|
|
expect(html).not.toContain("<script>");
|
|
});
|
|
});
|