Files
pleno-vue/tests/unit/order-content-table-addon-ordering.spec.js
T

275 lines
7.5 KiB
JavaScript

// @vitest-environment jsdom
import { flushPromises, mount } from "@vue/test-utils";
import { beforeEach, describe, expect, it, vi } from "vitest";
import OrderContentTable from "@/components/displays/superuser/tables/OrderContentTable.vue";
import { createTestI18n } from "./helpers/mountWithApp.js";
// Track mock state in a hoisted holder so the vi.mock factory below can stay
// free of top-level variables (vitest hoists vi.mock above imports, so the
// factory must not reference any binding that has not been hoisted).
const requestHolder = vi.hoisted(() => ({
responses: [],
calls: 0,
}));
const showPopperMock = vi.hoisted(() => vi.fn());
const removePopperIfOpenMock = vi.hoisted(() => vi.fn());
const popperBoxMock = vi.hoisted(() => vi.fn((title, body) => ({ title, body })));
const swalFireMock = vi.hoisted(() => vi.fn(() => Promise.resolve({ isConfirmed: false })));
vi.mock("sweetalert2", () => ({
default: {
fire: swalFireMock,
},
}));
vi.mock("@/components/displays/PopperDefault.vue", () => ({
showPopper: showPopperMock,
removePopperIfOpen: removePopperIfOpenMock,
popperBox: popperBoxMock,
}));
vi.mock("@/components/session/token/SessionUser.vue", () => ({
SessionUser: {
request: () => {
requestHolder.calls += 1;
const next = requestHolder.responses.shift();
return Promise.resolve(next ?? { data: { data: [] } });
},
functions: {
currency: {
toLocal: (value) => String(value ?? ""),
},
redirectTo: {
department: vi.fn(),
},
},
objects: {
vehicles: {
columns: {
wash_subscription: {
label: "Wash subscription",
},
},
},
},
},
}));
const i18n = createTestI18n({
en: {
global: {
cancel: "Cancel",
no_data: "No data",
quantity: "Quantity",
},
tables: {
products: {
name: "Product",
price: "Price",
},
},
objects: {
columns: {
notes: "Notes",
reference: "Reference",
},
},
invoice_period: {
flags: {
status: {
resolved: "Resolved",
ignored: "Ignored",
false_positive: "False positive",
reason_placeholder: "Optional reason",
},
},
},
},
});
const buildWrapper = (props = {}) =>
mount(OrderContentTable, {
props: {
orderId: 42,
...props,
},
global: {
plugins: [i18n],
},
});
describe("OrderContentTable addon ordering", () => {
beforeEach(() => {
requestHolder.responses = [];
requestHolder.calls = 0;
showPopperMock.mockReset();
removePopperIfOpenMock.mockReset();
popperBoxMock.mockReset();
swalFireMock.mockReset();
});
it("renders primary items before their addons even when the API returns them out of order", async () => {
// Simulate the legacy MySQL response: addons appear before the primary
// item they belong to. After the fix, the component must render the
// primary first.
const apiResponse = [
{
id: 102,
order_id: 42,
product_id: 9,
related_item_id: 101,
price: 50,
quantity: 1,
include_in_invoice: true,
notes: "",
reference: "",
product: { id: 9, name: "Addon A" },
},
{
id: 103,
order_id: 42,
product_id: 10,
related_item_id: 101,
price: 30,
quantity: 1,
include_in_invoice: true,
notes: "",
reference: "",
product: { id: 10, name: "Addon B" },
},
{
id: 101,
order_id: 42,
product_id: 1,
related_item_id: 0,
price: 200,
quantity: 1,
include_in_invoice: true,
notes: "",
reference: "",
product: { id: 1, name: "Primary wash" },
},
];
requestHolder.responses.push({ data: { data: apiResponse } });
requestHolder.responses.push({ data: { data: apiResponse } });
const wrapper = buildWrapper();
await flushPromises();
const renderedRows = wrapper.findAll("tbody tr");
const productNames = renderedRows.map((row) => String(row.text() || "").trim());
const primaryIndex = productNames.findIndex((text) => text.includes("Primary wash"));
const addonAIndex = productNames.findIndex((text) => text.includes("Addon A"));
const addonBIndex = productNames.findIndex((text) => text.includes("Addon B"));
expect(primaryIndex).toBeGreaterThanOrEqual(0);
expect(addonAIndex).toBeGreaterThanOrEqual(0);
expect(addonBIndex).toBeGreaterThanOrEqual(0);
expect(primaryIndex).toBeLessThan(addonAIndex);
expect(primaryIndex).toBeLessThan(addonBIndex);
});
it("preserves insertion order for addons that share the same parent", async () => {
const apiResponse = [
{
id: 101,
order_id: 42,
product_id: 1,
related_item_id: 0,
price: 200,
quantity: 1,
include_in_invoice: true,
notes: "",
reference: "",
product: { id: 1, name: "Primary wash" },
},
{
id: 103,
order_id: 42,
product_id: 10,
related_item_id: 101,
price: 30,
quantity: 1,
include_in_invoice: true,
notes: "",
reference: "",
product: { id: 10, name: "Addon B" },
},
{
id: 102,
order_id: 42,
product_id: 9,
related_item_id: 101,
price: 50,
quantity: 1,
include_in_invoice: true,
notes: "",
reference: "",
product: { id: 9, name: "Addon A" },
},
];
// OrderContentTable mounts twice on initial render (immediate watcher plus
// a top-level call); queue two identical responses.
requestHolder.responses.push({ data: { data: apiResponse } });
requestHolder.responses.push({ data: { data: apiResponse } });
const wrapper = buildWrapper();
await flushPromises();
const renderedRows = wrapper.findAll("tbody tr");
const productNames = renderedRows.map((row) => String(row.text() || "").trim());
const addonAIndex = productNames.findIndex((text) => text.includes("Addon A"));
const addonBIndex = productNames.findIndex((text) => text.includes("Addon B"));
expect(addonAIndex).toBeLessThan(addonBIndex);
});
it("sorts localOrderItems defensively when useLocalOrderItems is true", async () => {
const localItems = [
{
id: 201,
order_id: 7,
product_id: 9,
related_item_id: 200,
price: 50,
quantity: 1,
include_in_invoice: true,
notes: "",
reference: "",
product: { id: 9, name: "Local addon" },
},
{
id: 200,
order_id: 7,
product_id: 1,
related_item_id: 0,
price: 200,
quantity: 1,
include_in_invoice: true,
notes: "",
reference: "",
product: { id: 1, name: "Local primary" },
},
];
const wrapper = buildWrapper({
orderId: 7,
useLocalOrderItems: true,
localOrderItems: localItems,
});
await flushPromises();
const renderedRows = wrapper.findAll("tbody tr");
const productNames = renderedRows.map((row) => String(row.text() || "").trim());
const primaryIndex = productNames.findIndex((text) => text.includes("Local primary"));
const addonIndex = productNames.findIndex((text) => text.includes("Local addon"));
expect(primaryIndex).toBeLessThan(addonIndex);
expect(requestHolder.calls).toBe(0);
});
});