Files
pleno-vue/tests/unit/invoicing-period-object-tree.spec.js
T

365 lines
12 KiB
JavaScript

// @vitest-environment jsdom
import { flushPromises, mount } from "@vue/test-utils";
import { describe, expect, it, vi, beforeEach } from "vitest";
const mocks = vi.hoisted(() => {
const order = {
id: 9001,
invoice_collection_id: 3001,
department_id: 75,
created_at: "2026-06-18",
total_net_amount: 240,
reference: "REF-9001",
notes: "",
po: "",
reg_1: "AB12345",
};
return {
order,
economicDetails: vi.fn(async () => ({
data: {
data: {
collected_invoice_id: 3001,
economic: {
draft_id: 88,
booked_id: 99,
},
internal: {
normalized: {
totals: {
net_total: 240,
line_count: 1,
},
},
},
draft: {
exists: false,
normalized: null,
},
booked: {
exists: true,
normalized: {
totals: {
net_total: 240,
line_count: 1,
},
},
},
warnings: [],
},
},
})),
economicPdf: vi.fn(async () => ({ data: { data: { url: "https://example.test/invoice.pdf" } } })),
request: vi.fn(async (url, method) => {
if (url === "/order/items" && method === "GET") {
return {
data: {
data: [
{
id: 501,
order_id: 9001,
product_name: "Premium wash",
quantity: 2,
price: 120,
reference: "",
notes: "",
},
],
},
};
}
return { data: { data: [] } };
}),
};
});
vi.mock("buefy", () => ({
BCheckbox: {
name: "BCheckbox",
props: {
modelValue: Boolean,
indeterminate: Boolean,
disabled: Boolean,
},
emits: ["update:modelValue"],
template: `
<button
class="b-checkbox-stub"
type="button"
:disabled="disabled"
:data-checked="modelValue"
:data-indeterminate="indeterminate"
@click="$emit('update:modelValue', !modelValue)"
></button>
`,
},
}));
vi.mock("sweetalert2", () => ({
default: {
fire: vi.fn(async () => ({ isConfirmed: false })),
},
}));
vi.mock("vue-i18n", () => ({
useI18n: () => ({
t: (key) => key,
locale: { value: "da-DK" },
}),
}));
vi.mock("@/components/displays/buttons/EditableTableColumn.vue", () => ({
default: {
name: "EditableTableColumn",
props: ["object", "column", "parseFunction"],
template: `<span class="editable-table-column-stub">{{ parseFunction ? parseFunction(object?.[column]) : object?.[column] }}</span>`,
},
}));
vi.mock("@/components/displays/buttons/ActionSettingsWheelButton.vue", () => ({
default: {
name: "ActionSettingsWheelButton",
props: ["menuSections", "order_id", "department_id", "invoice_collection_id", "customer_number"],
template: `
<button
type="button"
class="action-settings-wheel-stub"
:data-menu-count="menuSections.length"
:data-action-count="menuSections.reduce((count, section) => count + (section.items?.length || 0), 0)"
:data-order-id="order_id || undefined"
:data-department-id="department_id || undefined"
:data-invoice-collection-id="invoice_collection_id || undefined"
:data-customer-number="customer_number || undefined"
></button>
`,
},
}));
vi.mock("@/components/session/token/SessionUser.vue", () => ({
SessionUser: {
request: mocks.request,
functions: {
currency: {
toLocal: (value) => `${Number(value || 0)} DKK`,
},
parseErrorMessage: (error) => error?.message || String(error),
},
hasPermission: () => true,
canAccessAdmin: () => true,
canAccessSuperUser: () => true,
canAccessDepartment: () => true,
objects: {
orders: {
columns: {
reference: { label: "Reference" },
notes: { label: "Noter" },
po: { label: "PO" },
reg_1: { label: "Reg 1" },
reg_2: { label: "Reg 2" },
reg_3: { label: "Reg 3" },
include_in_invoice: { label: "Faktura" },
total_net_amount: { label: "Total" },
},
get: {
single: vi.fn(async () => mocks.order),
},
functions: {
fetchAttachments: vi.fn(async () => []),
downloadAttachment: vi.fn(async () => "https://example.test/attachment.pdf"),
removeAttachment: vi.fn(),
resendWashCertificate: vi.fn(),
mark_as_completed: vi.fn(),
},
set: {
include_in_invoice: vi.fn(),
invoice_collection_id: vi.fn(),
booking_id: vi.fn(),
wash_id: vi.fn(),
},
delete: {
single: vi.fn(),
},
showEditObjectFieldForm: vi.fn(),
},
collectedOrderInvoices: {
columns: {
name: { label: "Navn" },
notes: { label: "Noter" },
po_number: { label: "PO" },
customer_number: { label: "Kundenr." },
external_id: { label: "Eksternt ID" },
closed_at: { label: "Lukket" },
total_net_amount: { label: "Total" },
},
functions: {
economic: {
v2: {
details: mocks.economicDetails,
},
pdf: mocks.economicPdf,
},
bulk_action_preview: vi.fn(),
bulk_action_apply: vi.fn(),
},
showEditObjectFieldForm: vi.fn(),
},
order_bookings: {
get: {
single: vi.fn(),
},
functions: {
resendBookingConfirmation: vi.fn(),
resendBookingCompletionConfirmation: vi.fn(),
},
delete: {
single: vi.fn(),
},
},
},
},
}));
vi.mock("@/views/dashboards/superUserDashboard/InvoicingBillingPeriod/imports/InvoicingBillingPeriodImportInvoiceQueue.vue", () => ({
invoiceQueue: {
addInvoiceCollectionsToQueue: vi.fn(),
processInvoiceCollectionQueue: vi.fn(),
},
default: {},
}));
vi.mock("@/components/displays/department/pos/orders/invoiceCollectionBulkActions.js", () => ({
INVOICE_COLLECTION_BULK_ACTIONS: {
QUEUE_ECONOMIC: "queue_economic",
CLEAN_CUSTOMER_RULES: "clean_customer_rules",
MERGE: "merge",
SPLIT_BY_MONTH: "split_by_month",
RESET_HIDDEN_PRICES: "reset_hidden_prices",
},
}));
import InvoicingPeriodObjectTree from "@/views/dashboards/superUserDashboard/InvoicingBillingPeriod/components/InvoicingPeriodObjectTree.vue";
const mountTree = () => mount(InvoicingPeriodObjectTree, {
props: {
customer: {
customer_number: 2001,
customer_name: "ACME",
draft: {
has_valid_draft: true,
invoice_collection_ids: [3001],
},
},
transactions: [
{
...mocks.order,
booked: true,
},
],
excludedOrderIds: [],
dates: {
dateFrom: "2026-06-01",
dateTo: "2026-06-30",
},
invoicePeriodFlags: [],
},
});
const expandNode = async (wrapper, key) => {
const node = wrapper.find(`[data-node-key="${key}"]`);
expect(node.exists()).toBe(true);
await node.find(".b-tree-node-toggle").trigger("click");
await flushPromises();
};
describe("InvoicingPeriodObjectTree", () => {
beforeEach(() => {
vi.clearAllMocks();
Object.defineProperty(window, "open", {
configurable: true,
value: vi.fn(),
});
});
it("auto-expands order items when an order node is expanded", async () => {
const wrapper = mountTree();
await expandNode(wrapper, "collected_order_invoice:3001");
await expandNode(wrapper, "category:3001:collection_orders");
await expandNode(wrapper, "order:9001");
expect(mocks.request).toHaveBeenCalledWith("/order/items", "GET", { order_id: 9001 });
expect(wrapper.find('[data-node-key="category:9001:order_items"]').classes()).toContain("is-expanded");
expect(wrapper.text()).toContain("Premium wash");
});
it("renders row action wheels for actionable collection, order, and order item nodes", async () => {
const wrapper = mountTree();
await expandNode(wrapper, "collected_order_invoice:3001");
await expandNode(wrapper, "category:3001:collection_orders");
await expandNode(wrapper, "order:9001");
const collectionWheel = wrapper.find('[data-testid="invoice-period-tree-action-wheel-collected_order_invoice:3001"]');
const orderWheel = wrapper.find('[data-testid="invoice-period-tree-action-wheel-order:9001"]');
const orderItemWheel = wrapper.find('[data-testid="invoice-period-tree-action-wheel-order_item:501"]');
expect(collectionWheel.exists()).toBe(true);
expect(orderWheel.exists()).toBe(true);
expect(orderItemWheel.exists()).toBe(true);
expect(collectionWheel.attributes("data-action-count")).toBe("4");
expect(collectionWheel.attributes("data-invoice-collection-id")).toBe("3001");
expect(orderWheel.attributes("data-action-count")).toBe("3");
expect(orderWheel.attributes("data-order-id")).toBe("9001");
expect(orderWheel.attributes("data-department-id")).toBe("75");
expect(orderItemWheel.attributes("data-action-count")).toBe("1");
expect(wrapper.find('[data-testid="invoice-period-tree-action-wheel-category:9001:order_items"]').exists()).toBe(false);
});
it("shows empty editable fields without a reveal toggle", async () => {
const wrapper = mountTree();
await expandNode(wrapper, "collected_order_invoice:3001");
await expandNode(wrapper, "category:3001:collection_orders");
expect(wrapper.find('[data-testid="invoice-period-tree-field-order:9001-notes"]').exists()).toBe(true);
expect(wrapper.find('[data-testid="invoice-period-tree-field-value-order:9001-notes"]').text()).toContain("Tom");
expect(wrapper.find('[data-testid="invoice-period-tree-field-order:9001-reg_2"]').exists()).toBe(true);
expect(wrapper.find('[data-testid^="invoice-period-tree-field-empty-toggle-"]').exists()).toBe(false);
});
it("omits redundant collection name and customer number fields", async () => {
const wrapper = mountTree();
expect(wrapper.find('[data-testid="invoice-period-tree-field-collected_order_invoice:3001-notes"]').exists()).toBe(true);
expect(wrapper.find('[data-testid="invoice-period-tree-field-collected_order_invoice:3001-name"]').exists()).toBe(false);
expect(wrapper.find('[data-testid="invoice-period-tree-field-collected_order_invoice:3001-customer_number"]').exists()).toBe(false);
});
it("renders only resolved draft/booked e-conomic invoice children", async () => {
const wrapper = mountTree();
await expandNode(wrapper, "collected_order_invoice:3001");
await expandNode(wrapper, "category:3001:collection_economic");
expect(mocks.economicDetails).toHaveBeenCalledWith(3001);
expect(wrapper.text()).not.toContain("E-conomic kladde #88");
expect(wrapper.text()).toContain("E-conomic faktura #99");
});
it("downloads booked e-conomic invoice pdfs for booked invoice nodes", async () => {
const wrapper = mountTree();
await expandNode(wrapper, "collected_order_invoice:3001");
await expandNode(wrapper, "category:3001:collection_economic");
const bookedInvoiceNode = wrapper.find('[data-node-key="economic_invoice:3001:booked:99"]');
expect(bookedInvoiceNode.exists()).toBe(true);
await bookedInvoiceNode.find(".b-tree-node-content").trigger("click");
await flushPromises();
expect(mocks.economicPdf).toHaveBeenCalledWith(3001, "booked");
expect(window.open).toHaveBeenCalledWith("https://example.test/invoice.pdf", "_blank", "noopener,noreferrer");
});
});