Files
pleno-vue/tests/unit/xlvask-manual-review.spec.js
T
Jeppe BandCleanup Agent f5ccb2a2a9 fix(pleno-vue): XL Vask manual review buttons + Fakturer nu visibility (#271)
Makes XL Vask accept/reject/ignore buttons always visible when review is
enabled (not gated on AI autopilot suggestion). Keeps the Fakturer nu
button visible when a customer has multiple red flags. Includes vitest
tests for the manual-review flow. Required for tomorrow's manual review
+ accepted order workflow.

Co-authored-by: Cleanup Agent <agent@truckwash.io>
2026-08-09 22:02:47 +02:00

204 lines
6.0 KiB
JavaScript

// @vitest-environment jsdom
import { flushPromises, shallowMount } from "@vue/test-utils";
import { beforeEach, describe, expect, it, vi } from "vitest";
import { ref } from "vue";
import { createI18n } from "vue-i18n";
const { request, loadList, fire } = vi.hoisted(() => ({
request: vi.fn(),
loadList: vi.fn(),
fire: vi.fn(),
}));
vi.mock("sweetalert2", () => ({ default: { fire } }));
vi.mock("@/components/session/token/SessionUser.vue", () => ({
SessionUser: {
request,
functions: {
currency: { toLocal: (value) => String(value) },
parseErrorMessage: (error) => String(error),
},
objects: {
global: {
language: {
completed: "Completed",
generated: "Generated",
possible_duplicates: "Possible duplicates",
price_match: "Price match",
price_unmatch: "Price mismatch",
},
},
orders: { meta: { title: "Orders" } },
},
},
}));
vi.mock("@/components/pagination/paginatedList.vue", () => ({
usePaginatedListInstance: () => ({ loadList }),
}));
vi.mock("@/components/pagination/departmentTabs.vue", () => ({
departments: ref([{ id: 1, name: "Hall 1" }]),
getDepartments: vi.fn(),
getDepartmentName: () => "Hall 1",
}));
vi.mock("@/components/displays/department/pos/sync/xlvaskUsageAmountCache.js", () => ({
clearCachedXlvaskUsageAmount: vi.fn(),
getCachedXlvaskUsageAmount: vi.fn(() => null),
setCachedXlvaskUsageAmount: vi.fn(),
}));
import XlvaskUsageOrdersTable from "@/components/displays/department/pos/sync/xlvaskUsageOrdersTable.vue";
const reviewEligibleRow = (overrides = {}) => ({
id: 9101,
reg_1: "ZZ99001",
created_at: new Date().toISOString(),
customer_name: "Test customer",
department_id: 1,
lane: 1,
total_net_amount: 250,
import_state: "new",
resolution_state: "needs_review",
certainty: "uncertain",
planned_action: "none",
// No AI suggestion yet — automation is empty (the autopilot hasn't run).
automation: {
status: "none",
review_eligible: true,
},
...overrides,
});
const mountTable = (props = {}) =>
shallowMount(XlvaskUsageOrdersTable, {
props: {
objects: [reviewEligibleRow()],
allowReviewActions: true,
allowSelectMultiple: true,
...props,
},
global: {
plugins: [
createI18n({ legacy: false, locale: "en", missingWarn: false, fallbackWarn: false, messages: { en: {} } }),
],
stubs: {
WhiteBoxCard: {
template: "<section><slot name='header'/><slot name='content'/></section>",
},
OrderItemsTable: true,
},
},
});
describe("XL-Vask manual review buttons", () => {
beforeEach(() => {
request.mockReset();
loadList.mockReset();
fire.mockReset();
});
it("exposes accept / reject / ignore buttons even when the autopilot has no suggestion", () => {
const wrapper = mountTable();
expect(wrapper.find("[data-testid='xlvask-accept-9101']").exists()).toBe(true);
expect(wrapper.find("[data-testid='xlvask-reject-9101']").exists()).toBe(true);
expect(wrapper.find("[data-testid='xlvask-ignore-9101']").exists()).toBe(true);
});
it("sends force_manual=true when accepting without an AI suggestion", async () => {
const wrapper = mountTable();
request.mockResolvedValue({
data: {
data: {
preview: { id: "preview-id", selection_hash: "abc12345".repeat(8) },
},
},
});
await wrapper.find("[data-testid='xlvask-accept-9101']").trigger("click");
await flushPromises();
const previewCall = request.mock.calls.find(
([endpoint, method]) =>
endpoint === "/modules/xlvask/services/usage/automation/decisions/preview" && method === "POST"
);
expect(previewCall).toBeDefined();
const payload = previewCall[2];
expect(payload).toMatchObject({
usage_log_ids: [9101],
action: "create_order",
force_manual: true,
});
});
it("does not send force_manual when the AI already has a matching suggestion", async () => {
const row = reviewEligibleRow({
automation: {
id: 5001,
status: "suggested",
action: "create_order",
can_accept: true,
can_deny: true,
can_ignore: true,
can_attach_order: true,
can_create_order: true,
review_eligible: true,
},
});
const wrapper = mountTable({ objects: [row] });
request.mockResolvedValue({
data: {
data: {
preview: { id: "preview-id-2", selection_hash: "def67890".repeat(8) },
},
},
});
await wrapper.find("[data-testid='xlvask-accept-9101']").trigger("click");
await flushPromises();
const previewCall = request.mock.calls.find(
([endpoint, method]) =>
endpoint === "/modules/xlvask/services/usage/automation/decisions/preview" && method === "POST"
);
expect(previewCall).toBeDefined();
const payload = previewCall[2];
expect(payload.action).toBe("create_order");
expect(payload).not.toHaveProperty("force_manual");
});
it("hides the buttons once the row is already resolved (no double-action)", () => {
const row = reviewEligibleRow({
resolution_state: "auto_linked",
automation: { status: "accepted", action: "attach_order", review_eligible: false },
});
const wrapper = mountTable({ objects: [row] });
expect(wrapper.find("[data-testid='xlvask-accept-9101']").exists()).toBe(false);
expect(wrapper.find("[data-testid='xlvask-reject-9101']").exists()).toBe(false);
expect(wrapper.find("[data-testid='xlvask-ignore-9101']").exists()).toBe(false);
});
it("surfaces the backend error when the manual review is rejected", async () => {
const wrapper = mountTable();
request.mockRejectedValue(new Error("Wash-id uniqueness activation is blocked"));
await wrapper.find("[data-testid='xlvask-accept-9101']").trigger("click");
await flushPromises();
expect(fire).toHaveBeenCalledWith(
expect.objectContaining({
icon: "error",
title: "invoicing_period.xlvask_autopilot.preview.error_title",
})
);
});
});