Brings all of the develop branch's commits into master. ## What this contains The 9 commits on develop that landed in this round — all XL Vask-related UI fixes plus the 'visible primary-button hover state' visual-diff: - **PR #303** (TRU-5 / AUT-1) — style(AUT-1): visible primary-button hover state + visual diff - **PR #304** (TRU-10 / AUT-6) — fix(invoicing-period): propagate flagged wash start date to Selvvask view - **PR #305** (TRU-12 / AUT-8) — feat(xlvask): render friendly notice for 404 from /modules/xlvask/services/usage/orders - **PR #306** (TRU-9 / AUT-5) — i18n(test): lock in xlvask_review / xlvask_usage_log mirroring to the global v2 fallback - **PR #307** (TRU-13 / AUT-9) — i18n(xlvask_review): translate missing keys for no, sv, de, en - **PR #308** (TRU-15 / AUT-11) — test(e2e): add Playwright smoke test for XL Vask flag → Selvvash navigation - **PR #309** (TRU-11 / AUT-7) — feat(TRU-11): propagate department selector to Selvvask usage query - **PR #310** (TRU-19 / AUT-15) — test(TRU-19): lock self-serve program number range + button registry contract - **PR #311** (TRU-8 / AUT-4) — fix(invoicing-flag-list): explain empty XL Vask hover preview when flag context has no metadata ## Why The XL Vask integration bug surfaced from the user-reported message "XL Vask-registreringen er hverken ignoreret eller knyttet til en ordre i den valgte periode. doesn't show the wash." After dispatching 9 diagnostic + fix tasks and merging all 9 PRs into develop via the OpenSymphony orchestrator running against MiniMax M3, this PR is the canonical release to bring develop's accumulated changes into master. No new code in this PR — just the squash-merged output of the 9 source PRs combined into a single develop→master merge. ## Verification All 9 source PRs passed: - Required CI (Action Runners) - App Store Readiness - Quality lint/i18n/build/unit/e2e suites The required checks on this PR will run the same gate. ## Notes - The api repo has its own equivalent PR/merge — see CHANGELOG for that side. --------- Co-authored-by: Jeppe <jeppe@copenhagentruckwash.io> Co-authored-by: openhands <openhands@all-hands.dev>
611 lines
22 KiB
JavaScript
611 lines
22 KiB
JavaScript
// @vitest-environment jsdom
|
|
import { mount } from "@vue/test-utils";
|
|
import { beforeEach, describe, expect, it, vi } from "vitest";
|
|
import InvoicingPeriodFlagList from "@/views/dashboards/superUserDashboard/InvoicingBillingPeriod/components/InvoicingPeriodFlagList.vue";
|
|
import { SessionUser } from "@/components/session/token/SessionUser.vue";
|
|
import { createTestI18n } from "./helpers/mountWithApp.js";
|
|
import { readI18nRuntimeMessages } from "./helpers/readI18nRuntimeMessages";
|
|
|
|
const daMessages = readI18nRuntimeMessages("da");
|
|
|
|
const swalFireMock = vi.hoisted(() => vi.fn(() => Promise.resolve({ isConfirmed: true, value: "Checked" })));
|
|
const showPopperMock = vi.hoisted(() => vi.fn());
|
|
const removePopperIfOpenMock = vi.hoisted(() => vi.fn());
|
|
const popperBoxMock = vi.hoisted(() => vi.fn((title, body) => ({ title, body })));
|
|
|
|
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: vi.fn(() => Promise.resolve({ data: { data: null } })),
|
|
functions: {
|
|
redirectTo: {
|
|
department: vi.fn(),
|
|
superUser: vi.fn(),
|
|
},
|
|
},
|
|
},
|
|
}));
|
|
|
|
const i18n = createTestI18n({
|
|
en: {
|
|
global: {
|
|
cancel: "Cancel",
|
|
},
|
|
invoice_period: {
|
|
flags: {
|
|
automatic: {
|
|
price_mismatch: "{product} product price differs from expected.",
|
|
multiple_identical_primary_vehicle_items: "Order contains multiple identical primary vehicle items.",
|
|
wash_certificate_item_without_certificate: "Wash certificate item is present without a wash certificate.",
|
|
customer_rule_requires_reference: "Order is missing a required reference.",
|
|
customer_rule_requires_po_number: "Order is missing a required PO number.",
|
|
xlvask_missing_order_link: "XL Vask wash is neither ignored nor linked to an order in the selected period.",
|
|
},
|
|
preview: {
|
|
no_order_items: "No order items available.",
|
|
no_xlvask_usage_log: "No XL Vask details available.",
|
|
no_xlvask_usage_log_metadata:
|
|
"XL Vask registration (no metadata available — see api flag-context serializer)...",
|
|
product: "Product",
|
|
quantity: "Qty",
|
|
price: "Price",
|
|
order_items: "Order items",
|
|
expected_price: "Expected price",
|
|
xlvask_usage_log: "XL Vask registration",
|
|
wash_id: "Wash ID",
|
|
registration_number: "Registration",
|
|
start_time: "Start time",
|
|
customer: "Customer",
|
|
},
|
|
tokens: {
|
|
expected: "expected",
|
|
order: "Order",
|
|
wash_certificate_item: "Wash certificate item",
|
|
wash_certificate: "Wash certificate",
|
|
xlvask_usage_log: "XL Vask wash",
|
|
},
|
|
status: {
|
|
resolved: "Resolved",
|
|
ignored: "Ignored",
|
|
false_positive: "False positive",
|
|
reason_placeholder: "Optional reason",
|
|
},
|
|
tooltip: {
|
|
created_at: "Created",
|
|
created_by: "By",
|
|
unknown: "Unknown",
|
|
},
|
|
},
|
|
},
|
|
},
|
|
});
|
|
|
|
const TooltipStub = {
|
|
props: {
|
|
position: String,
|
|
type: String,
|
|
multilined: Boolean,
|
|
appendToBody: Boolean,
|
|
},
|
|
template: `
|
|
<div
|
|
class="b-tooltip-stub"
|
|
:data-position="position"
|
|
:data-type="type"
|
|
:data-multilined="String(multilined)"
|
|
:data-append-to-body="String(appendToBody)"
|
|
>
|
|
<div class="b-tooltip-content-stub"><slot name="content" /></div>
|
|
<slot />
|
|
</div>
|
|
`,
|
|
};
|
|
|
|
const mountList = (flags, plugin = i18n) =>
|
|
mount(InvoicingPeriodFlagList, {
|
|
props: {
|
|
flags,
|
|
},
|
|
global: {
|
|
plugins: [plugin],
|
|
stubs: {
|
|
BTooltip: TooltipStub,
|
|
},
|
|
},
|
|
});
|
|
|
|
describe("InvoicingPeriodFlagList", () => {
|
|
beforeEach(() => {
|
|
swalFireMock.mockReset();
|
|
swalFireMock.mockResolvedValue({ isConfirmed: true, value: "Checked" });
|
|
showPopperMock.mockClear();
|
|
removePopperIfOpenMock.mockClear();
|
|
popperBoxMock.mockClear();
|
|
SessionUser.request.mockReset();
|
|
SessionUser.request.mockResolvedValue({ data: { data: null } });
|
|
SessionUser.functions.redirectTo.department.mockClear();
|
|
SessionUser.functions.redirectTo.superUser.mockClear();
|
|
});
|
|
|
|
it("renders manual flags before localized automatic warnings with matching flag colors", () => {
|
|
const wrapper = mountList([
|
|
{
|
|
id: 1,
|
|
source: "manual",
|
|
reason: "Customer asked for manual review.",
|
|
},
|
|
{
|
|
id: "auto-price-1",
|
|
source: "automatic",
|
|
definition_key: "price_mismatch",
|
|
message_key: "invoice_period.flags.automatic.price_mismatch",
|
|
message_params: {
|
|
product: "Spot Free",
|
|
},
|
|
},
|
|
]);
|
|
|
|
const rows = wrapper.findAll(".invoice-period-flag-row");
|
|
expect(rows).toHaveLength(2);
|
|
expect(rows[0].text()).toContain("Customer asked for manual review.");
|
|
expect(rows[0].find("i").classes()).toContain("has-text-danger");
|
|
expect(rows[1].text()).toContain("Spot Free product price differs from expected.");
|
|
expect(rows[1].find("i").classes()).toContain("has-text-warning");
|
|
});
|
|
|
|
it("does not render inactive flags", () => {
|
|
const wrapper = mountList([
|
|
{
|
|
id: 1,
|
|
source: "manual",
|
|
status: "resolved",
|
|
reason: "Already resolved.",
|
|
},
|
|
{
|
|
id: "auto-price-1",
|
|
source: "automatic",
|
|
status: "active",
|
|
definition_key: "price_mismatch",
|
|
message_key: "invoice_period.flags.automatic.price_mismatch",
|
|
message_params: {
|
|
product: "Spot Free",
|
|
},
|
|
},
|
|
]);
|
|
|
|
const rows = wrapper.findAll(".invoice-period-flag-row");
|
|
expect(rows).toHaveLength(1);
|
|
expect(wrapper.text()).not.toContain("Already resolved.");
|
|
expect(wrapper.text()).toContain("Spot Free product price differs from expected.");
|
|
});
|
|
|
|
it("shows manual flag creation metadata in a buefy tooltip", () => {
|
|
const wrapper = mountList([
|
|
{
|
|
id: 1,
|
|
source: "manual",
|
|
reason: "Customer asked for manual review.",
|
|
created_at: "2026-05-11 10:00:00",
|
|
created_by: 42,
|
|
created_by_name: "Jeppe",
|
|
},
|
|
{
|
|
id: "auto-price-1",
|
|
source: "automatic",
|
|
definition_key: "price_mismatch",
|
|
message_key: "invoice_period.flags.automatic.price_mismatch",
|
|
message_params: {
|
|
product: "Spot Free",
|
|
},
|
|
},
|
|
]);
|
|
|
|
const tooltip = wrapper.get('[data-testid="invoice-period-flag-created-tooltip-1"]');
|
|
expect(wrapper.get(".b-tooltip-stub").attributes("data-multilined")).toBe("true");
|
|
expect(wrapper.get(".b-tooltip-stub").attributes("data-append-to-body")).toBe("true");
|
|
expect(tooltip.text()).toContain("Created");
|
|
expect(tooltip.text()).toContain("By");
|
|
expect(tooltip.text()).toContain("Jeppe");
|
|
expect(tooltip.text()).toMatch(/10|2026|5\/11|11\/5/);
|
|
expect(wrapper.find('[data-testid="invoice-period-flag-created-tooltip-auto-price-1"]').exists()).toBe(false);
|
|
});
|
|
|
|
it("opens highlighted order items and previews expected price details from automatic messages", async () => {
|
|
const wrapper = mountList([
|
|
{
|
|
id: "auto-price-1",
|
|
source: "automatic",
|
|
fingerprint: "price-fingerprint",
|
|
target_type: "order_item_field",
|
|
target_id: 77,
|
|
field: "price",
|
|
order_id: 42,
|
|
order_item_id: 77,
|
|
message_parts: [
|
|
{ type: "order_item", text: "Spot Free" },
|
|
{ type: "text", text: " product price differs from " },
|
|
{ type: "expected_price", text: "expected" },
|
|
{ type: "text", text: "." },
|
|
],
|
|
context: {
|
|
department_id: 3,
|
|
order_id: 42,
|
|
order_item_id: 77,
|
|
order_items: [
|
|
{ id: 77, product_name: "Spot Free", quantity: 1, price: 99 },
|
|
{ id: 78, product_name: "Wash", quantity: 1, price: 199 },
|
|
],
|
|
expected_price_breakdown: {
|
|
product_price: 100,
|
|
department_price: 90,
|
|
product_discount_percentage: 10,
|
|
applied_discount_percentage: 10,
|
|
expected_price: 81,
|
|
},
|
|
},
|
|
},
|
|
]);
|
|
|
|
await wrapper.get(".invoice-period-flag-token").trigger("click");
|
|
expect(SessionUser.functions.redirectTo.department).toHaveBeenCalledWith(
|
|
3,
|
|
"modules/pos/orders/42?highlightOrderItem=77",
|
|
true
|
|
);
|
|
|
|
await wrapper.findAll(".invoice-period-flag-token")[1].trigger("mouseover");
|
|
expect(popperBoxMock).toHaveBeenCalledWith("Expected price", expect.stringContaining("81"));
|
|
expect(showPopperMock).toHaveBeenCalledTimes(1);
|
|
});
|
|
|
|
it("makes order and wash certificate item warning tokens hoverable and clickable", async () => {
|
|
const wrapper = mountList([
|
|
{
|
|
id: "auto-order-1",
|
|
source: "automatic",
|
|
fingerprint: "order-fingerprint",
|
|
definition_key: "multiple_identical_primary_vehicle_items",
|
|
message_key: "invoice_period.flags.automatic.multiple_identical_primary_vehicle_items",
|
|
order_id: 42,
|
|
context: {
|
|
department_id: 3,
|
|
order_id: 42,
|
|
order_items: [
|
|
{ id: 71, product_name: "Truck wash", quantity: 1, price: 199 },
|
|
{ id: 72, product_name: "Truck wash", quantity: 1, price: 199 },
|
|
],
|
|
},
|
|
},
|
|
{
|
|
id: "auto-certificate-1",
|
|
source: "automatic",
|
|
fingerprint: "certificate-fingerprint",
|
|
definition_key: "wash_certificate_item_without_certificate",
|
|
message_key: "invoice_period.flags.automatic.wash_certificate_item_without_certificate",
|
|
order_id: 43,
|
|
order_item_id: 88,
|
|
context: {
|
|
department_id: 4,
|
|
order_id: 43,
|
|
order_item_id: 88,
|
|
order_items: [
|
|
{ id: 88, product_name: "Wash certificate", quantity: 1, price: 0 },
|
|
{ id: 89, product_name: "Wash", quantity: 1, price: 199 },
|
|
],
|
|
},
|
|
},
|
|
]);
|
|
|
|
const tokens = wrapper.findAll(".invoice-period-flag-token");
|
|
expect(tokens.map((token) => token.text())).toEqual(["Order", "Wash certificate item"]);
|
|
|
|
await tokens[0].trigger("mouseover");
|
|
expect(popperBoxMock).toHaveBeenLastCalledWith("Order items", expect.stringContaining("Truck wash"));
|
|
await tokens[0].trigger("click");
|
|
expect(SessionUser.functions.redirectTo.department).toHaveBeenLastCalledWith(3, "modules/pos/orders/42", true);
|
|
|
|
await tokens[1].trigger("mouseover");
|
|
expect(popperBoxMock).toHaveBeenLastCalledWith(
|
|
"Order items",
|
|
expect.stringContaining("has-background-warning-light")
|
|
);
|
|
await tokens[1].trigger("click");
|
|
expect(SessionUser.functions.redirectTo.department).toHaveBeenLastCalledWith(
|
|
4,
|
|
"modules/pos/orders/43?highlightOrderItem=88",
|
|
true
|
|
);
|
|
});
|
|
|
|
it("makes required order field warning tokens hoverable and clickable", async () => {
|
|
const wrapper = mountList([
|
|
{
|
|
id: "auto-po-1",
|
|
source: "automatic",
|
|
fingerprint: "po-fingerprint",
|
|
definition_key: "customer_rule_requires_po_number",
|
|
message_key: "invoice_period.flags.automatic.customer_rule_requires_po_number",
|
|
order_id: 44,
|
|
context: {
|
|
department_id: 5,
|
|
order_id: 44,
|
|
order_items: [
|
|
{ id: 91, product_name: "Forvogn", quantity: 1, price: 649 },
|
|
{ id: 92, product_name: "Trailer", quantity: 1, price: 599 },
|
|
],
|
|
},
|
|
},
|
|
]);
|
|
|
|
const token = wrapper.get(".invoice-period-flag-token");
|
|
expect(token.text()).toBe("Order");
|
|
|
|
await token.trigger("mouseover");
|
|
expect(popperBoxMock).toHaveBeenLastCalledWith("Order items", expect.stringContaining("Forvogn"));
|
|
|
|
await token.trigger("click");
|
|
expect(SessionUser.functions.redirectTo.department).toHaveBeenLastCalledWith(5, "modules/pos/orders/44", true);
|
|
});
|
|
|
|
it("makes XL Vask registration warning tokens hoverable and clickable", async () => {
|
|
const wrapper = mountList([
|
|
{
|
|
id: "auto-xlvask-1",
|
|
source: "automatic",
|
|
fingerprint: "xlvask-fingerprint",
|
|
definition_key: "xlvask_missing_order_link",
|
|
message_key: "invoice_period.flags.automatic.xlvask_missing_order_link",
|
|
target_type: "xlvask_usage_log",
|
|
target_id: 55,
|
|
xlvask_usage_log_id: 55,
|
|
message_params: {
|
|
wash_id: "wash-55",
|
|
registration_number: "AB12345",
|
|
},
|
|
context: {
|
|
customer_name: "XL Customer",
|
|
xlvask_usage_log_id: 55,
|
|
wash_id: "wash-55",
|
|
registration_number: "AB12345",
|
|
start_time: "2026-05-11 10:00:00",
|
|
},
|
|
},
|
|
]);
|
|
|
|
const token = wrapper.get(".invoice-period-flag-token");
|
|
expect(token.text()).toBe("XL Vask wash");
|
|
|
|
await token.trigger("mouseover");
|
|
expect(popperBoxMock).toHaveBeenLastCalledWith("XL Vask registration", expect.stringContaining("AB12345"));
|
|
expect(showPopperMock).toHaveBeenCalledTimes(1);
|
|
|
|
await token.trigger("click");
|
|
expect(SessionUser.functions.redirectTo.superUser).toHaveBeenCalledWith(
|
|
"/invoices?activeTab=period&periodView=self_wash&xlvaskUsageLogId=55&xlvaskUsageLogStartTime=2026-05-11",
|
|
true
|
|
);
|
|
});
|
|
|
|
it("propagates the flagged wash start time when navigating to the Selvvask view", async () => {
|
|
const wrapper = mountList([
|
|
{
|
|
id: "auto-xlvask-root",
|
|
source: "automatic",
|
|
fingerprint: "xlvask-root",
|
|
definition_key: "xlvask_missing_order_link",
|
|
message_key: "invoice_period.flags.automatic.xlvask_missing_order_link",
|
|
target_type: "xlvask_usage_log",
|
|
target_id: 77,
|
|
xlvask_usage_log_id: 77,
|
|
context: {
|
|
xlvask_usage_log_id: 77,
|
|
start_time: "2026-04-28T08:15:00",
|
|
},
|
|
},
|
|
]);
|
|
|
|
await wrapper.get(".invoice-period-flag-token").trigger("click");
|
|
expect(SessionUser.functions.redirectTo.superUser).toHaveBeenLastCalledWith(
|
|
"/invoices?activeTab=period&periodView=self_wash&xlvaskUsageLogId=77&xlvaskUsageLogStartTime=2026-04-28",
|
|
true
|
|
);
|
|
});
|
|
|
|
it("navigates to Selvvask without a start-time query when the flag has no wash date", async () => {
|
|
const wrapper = mountList([
|
|
{
|
|
id: "auto-xlvask-no-date",
|
|
source: "automatic",
|
|
fingerprint: "xlvask-no-date",
|
|
definition_key: "xlvask_missing_order_link",
|
|
message_key: "invoice_period.flags.automatic.xlvask_missing_order_link",
|
|
target_type: "xlvask_usage_log",
|
|
target_id: 91,
|
|
xlvask_usage_log_id: 91,
|
|
context: {
|
|
xlvask_usage_log_id: 91,
|
|
},
|
|
},
|
|
]);
|
|
|
|
await wrapper.get(".invoice-period-flag-token").trigger("click");
|
|
expect(SessionUser.functions.redirectTo.superUser).toHaveBeenLastCalledWith(
|
|
"/invoices?activeTab=period&periodView=self_wash&xlvaskUsageLogId=91",
|
|
true
|
|
);
|
|
});
|
|
|
|
// When the API flag-context serializer does not provide any of the
|
|
// identity fields (wash_id / registration_number / start_time / customer_name),
|
|
// the hover preview should not pretend the data is missing — it should
|
|
// tell the operator that the metadata simply isn't being supplied.
|
|
it("explains the empty XL Vask hover preview when flag context has no metadata fields", async () => {
|
|
const wrapper = mountList([
|
|
{
|
|
id: "auto-xlvask-empty",
|
|
source: "automatic",
|
|
fingerprint: "xlvask-empty-fingerprint",
|
|
definition_key: "xlvask_missing_order_link",
|
|
message_key: "invoice_period.flags.automatic.xlvask_missing_order_link",
|
|
target_type: "xlvask_usage_log",
|
|
target_id: 99,
|
|
xlvask_usage_log_id: 99,
|
|
context: {},
|
|
},
|
|
]);
|
|
|
|
const token = wrapper.get(".invoice-period-flag-token");
|
|
expect(token.text()).toBe("XL Vask wash");
|
|
|
|
await token.trigger("mouseover");
|
|
expect(popperBoxMock).toHaveBeenLastCalledWith(
|
|
"XL Vask registration",
|
|
"XL Vask registration (no metadata available — see api flag-context serializer)..."
|
|
);
|
|
expect(showPopperMock).toHaveBeenCalledTimes(1);
|
|
});
|
|
|
|
it("renders invoice period warnings in proper Danish", () => {
|
|
const wrapper = mountList(
|
|
[
|
|
{
|
|
id: "auto-order-1",
|
|
source: "automatic",
|
|
definition_key: "multiple_identical_primary_vehicle_items",
|
|
message_key: "invoice_period.flags.automatic.multiple_identical_primary_vehicle_items",
|
|
},
|
|
{
|
|
id: "auto-certificate-1",
|
|
source: "automatic",
|
|
definition_key: "wash_certificate_item_without_certificate",
|
|
message_key: "invoice_period.flags.automatic.wash_certificate_item_without_certificate",
|
|
},
|
|
{
|
|
id: "auto-price-1",
|
|
source: "automatic",
|
|
definition_key: "price_mismatch",
|
|
message_key: "invoice_period.flags.automatic.price_mismatch",
|
|
message_params: {
|
|
product: "Forevogn",
|
|
},
|
|
},
|
|
],
|
|
createTestI18n({ en: daMessages })
|
|
);
|
|
|
|
expect(wrapper.text()).toContain("Ordren indeholder flere ens primære køretøjsprodukter.");
|
|
expect(wrapper.text()).toContain("Vaskecertifikatlinjen findes uden et vaskecertifikat.");
|
|
expect(wrapper.text()).toContain("Forevogn: Produktprisen afviger fra den forventede pris.");
|
|
expect(wrapper.find('button[title="Løst"]').exists()).toBe(true);
|
|
});
|
|
|
|
it("persists manual and automatic status decisions through the correct endpoints", async () => {
|
|
const wrapper = mountList([
|
|
{
|
|
id: 12,
|
|
source: "manual",
|
|
reason: "Manual review",
|
|
},
|
|
{
|
|
id: "auto-price-1",
|
|
source: "automatic",
|
|
fingerprint: "price-fingerprint",
|
|
target_type: "order_item_field",
|
|
target_id: 77,
|
|
field: "price",
|
|
definition_key: "price_mismatch",
|
|
message: "Automatic review",
|
|
},
|
|
]);
|
|
|
|
await wrapper.get('[data-testid="invoice-period-flag-12"] button[title="Resolved"]').trigger("click");
|
|
expect(SessionUser.request).toHaveBeenCalledWith("/superuser/invoicing/period/flags/12/status", "PATCH", {
|
|
status: "resolved",
|
|
reason: "Checked",
|
|
});
|
|
|
|
await wrapper.get('[data-testid="invoice-period-flag-auto-price-1"] button[title="Ignored"]').trigger("click");
|
|
expect(SessionUser.request).toHaveBeenCalledWith("/superuser/invoicing/period/flags/automatic/status", "POST", {
|
|
fingerprint: "price-fingerprint",
|
|
status: "ignored",
|
|
target_type: "order_item_field",
|
|
target_id: 77,
|
|
field: "price",
|
|
definition_key: "price_mismatch",
|
|
reason: "Checked",
|
|
});
|
|
expect(wrapper.emitted("statusChanged")).toHaveLength(2);
|
|
});
|
|
|
|
// Regression test for TBC-105 / error-report #10: a `historical_primary_product_mismatch`
|
|
// flag must surface the historical product name (e.g. "Forvogn med hænger") through the
|
|
// i18n message_key path. The backend now only emits this flag when the current row's reg_2
|
|
// presence matches the historical orders' reg_2 presence, so the FE must render whatever
|
|
// the API does send. As of this commit there is no specific handler in flagMessageParts()
|
|
// for `historical_primary_product_mismatch`; the component falls back to flag.message.
|
|
// This test pins that contract so any future handler addition doesn't regress the
|
|
// visible text on existing deployments.
|
|
it("renders historical_primary_product_mismatch automatic flags with the historical product name", () => {
|
|
const wrapper = mountList([
|
|
{
|
|
id: "auto-historical-1",
|
|
source: "automatic",
|
|
fingerprint: "historical-fingerprint-1",
|
|
target_type: "order_item",
|
|
target_id: 905,
|
|
definition_key: "historical_primary_product_mismatch",
|
|
message_key: "invoice_period.flags.automatic.historical_primary_product_mismatch",
|
|
message_params: {
|
|
product: "Forvogn",
|
|
expected_product: "Forvogn med hænger",
|
|
},
|
|
message: "Forvogn differs from the registration number's usual product Forvogn med hænger.",
|
|
order_id: 502,
|
|
order_item_id: 905,
|
|
},
|
|
]);
|
|
|
|
const row = wrapper.get('[data-testid="invoice-period-flag-auto-historical-1"]');
|
|
expect(row.text()).toContain("Forvogn");
|
|
expect(row.text()).toContain("differs from");
|
|
expect(row.text()).toContain("Forvogn med hænger");
|
|
// The component does not yet emit an interactive order-item token for this flag type;
|
|
// pinning the current behaviour so the eventual handler addition is intentional.
|
|
expect(row.findAll(".invoice-period-flag-token")).toHaveLength(0);
|
|
});
|
|
|
|
// Backstop: the API may still send `historical_primary_product_mismatch` with no
|
|
// message_params (older API versions, or a row that was cached before the schema bump).
|
|
// The FE must not blow up; it should fall back to flag.message verbatim.
|
|
it("falls back to flag.message when historical_primary_product_mismatch has no message_params", () => {
|
|
const wrapper = mountList([
|
|
{
|
|
id: "auto-historical-2",
|
|
source: "automatic",
|
|
fingerprint: "historical-fingerprint-2",
|
|
target_type: "order_item",
|
|
target_id: 906,
|
|
definition_key: "historical_primary_product_mismatch",
|
|
message_key: "invoice_period.flags.automatic.historical_primary_product_mismatch",
|
|
message: "Spot Free differs from the registration number's usual product Forvogn med hænger.",
|
|
},
|
|
]);
|
|
|
|
const row = wrapper.get('[data-testid="invoice-period-flag-auto-historical-2"]');
|
|
expect(row.text()).toContain("Spot Free differs from the registration number's usual product Forvogn med hænger.");
|
|
// No order-item token because order_id is missing — should fall back to plain text only.
|
|
expect(row.findAll(".invoice-period-flag-token")).toHaveLength(0);
|
|
});
|
|
});
|