458 lines
16 KiB
JavaScript
458 lines
16 KiB
JavaScript
import { describe, expect, it } from "vitest";
|
|
import {
|
|
SYSTEM_SEARCH_ENTITY_TYPES,
|
|
SYSTEM_SEARCH_SUPPORT_REGISTRY,
|
|
buildSystemSearchViewModel,
|
|
resolveSystemSearchNavigationTarget,
|
|
} from "@/components/viewport/page/headers/menu/systemSearchSupport.ts";
|
|
|
|
const superuserContext = {
|
|
canAccessSuperUser: true,
|
|
canAccessAdmin: true,
|
|
canAccessUser: true,
|
|
canAccessDepartment: () => true,
|
|
};
|
|
|
|
const adminContext = {
|
|
canAccessSuperUser: false,
|
|
canAccessAdmin: true,
|
|
canAccessUser: false,
|
|
canAccessDepartment: (departmentId) => departmentId === 4,
|
|
};
|
|
|
|
const userContext = {
|
|
canAccessSuperUser: false,
|
|
canAccessAdmin: false,
|
|
canAccessUser: true,
|
|
canAccessDepartment: () => false,
|
|
};
|
|
|
|
const restrictedContext = {
|
|
canAccessSuperUser: false,
|
|
canAccessAdmin: false,
|
|
canAccessUser: false,
|
|
canAccessDepartment: () => false,
|
|
};
|
|
|
|
const row = (entityType, overrides = {}) => ({
|
|
entity_type: entityType,
|
|
entity_id: "11",
|
|
title: "11",
|
|
score: 120,
|
|
...overrides,
|
|
});
|
|
|
|
describe("system search support registry coverage", () => {
|
|
it("contains exactly all 59 known entity types", () => {
|
|
expect(SYSTEM_SEARCH_ENTITY_TYPES).toHaveLength(59);
|
|
expect(new Set(SYSTEM_SEARCH_ENTITY_TYPES).size).toBe(59);
|
|
|
|
const registryKeys = Object.keys(SYSTEM_SEARCH_SUPPORT_REGISTRY).sort();
|
|
const entityKeys = [...SYSTEM_SEARCH_ENTITY_TYPES].sort();
|
|
expect(registryKeys).toEqual(entityKeys);
|
|
});
|
|
|
|
it("provides render and navigation strategy for every type", () => {
|
|
for (const entityType of SYSTEM_SEARCH_ENTITY_TYPES) {
|
|
const definition = SYSTEM_SEARCH_SUPPORT_REGISTRY[entityType];
|
|
expect(definition).toBeTruthy();
|
|
expect(["deep-link", "module", "generic"]).toContain(definition.navigationStrategy);
|
|
expect(definition.titleKeys.length).toBeGreaterThan(0);
|
|
expect(definition.descriptionKeys.length).toBeGreaterThan(0);
|
|
expect(definition.keyFieldSelectors.length).toBeGreaterThan(0);
|
|
|
|
const vm = buildSystemSearchViewModel(row(entityType, { payload: null }));
|
|
expect(typeof vm.title).toBe("string");
|
|
expect(Array.isArray(vm.badges)).toBe(true);
|
|
expect(Array.isArray(vm.keyFields)).toBe(true);
|
|
|
|
const nav = resolveSystemSearchNavigationTarget(row(entityType), superuserContext);
|
|
expect(nav).not.toBeNull();
|
|
}
|
|
});
|
|
});
|
|
|
|
describe("system search navigation resolver behavior", () => {
|
|
it("deep-links order details for admin/superuser with department context", () => {
|
|
const nav = resolveSystemSearchNavigationTarget(
|
|
row("orders", {
|
|
entity_id: "34455",
|
|
payload: { id: 34455, department: "4" },
|
|
}),
|
|
adminContext
|
|
);
|
|
expect(nav.strategy).toBe("deep-link");
|
|
expect(nav.to.path).toBe("/admin/4/modules/pos/orders/34455");
|
|
});
|
|
|
|
it("opens user order detail for user role", () => {
|
|
const nav = resolveSystemSearchNavigationTarget(
|
|
row("orders", { entity_id: "6078", payload: { id: 6078 } }),
|
|
userContext
|
|
);
|
|
expect(nav.to.path).toBe("/user/orders/6078");
|
|
});
|
|
|
|
it("falls back to generic detail when bookings are missing department context", () => {
|
|
const nav = resolveSystemSearchNavigationTarget(row("bookings", { payload: null }), adminContext);
|
|
expect(nav.strategy).toBe("generic");
|
|
expect(nav.to.path).toContain("/search/system/record/bookings/11");
|
|
});
|
|
|
|
it("routes department self-serve entities to module pages when department is known", () => {
|
|
const nav = resolveSystemSearchNavigationTarget(
|
|
row("department_selfserve_questions", {
|
|
payload: { id: "19", department: "4" },
|
|
}),
|
|
adminContext
|
|
);
|
|
expect(nav.strategy).toBe("module");
|
|
expect(nav.to.path).toBe("/admin/4/modules/self-serve/questions");
|
|
});
|
|
|
|
it("routes customers and vehicles based on user role context", () => {
|
|
const customerForSuper = resolveSystemSearchNavigationTarget(row("customers"), superuserContext);
|
|
const customerForUser = resolveSystemSearchNavigationTarget(row("customers"), userContext);
|
|
const vehicleForSuper = resolveSystemSearchNavigationTarget(
|
|
row("vehicles", { payload: { registration_number: "AB12345" } }),
|
|
superuserContext
|
|
);
|
|
const vehicleForUser = resolveSystemSearchNavigationTarget(
|
|
row("vehicles", { payload: { registration_number: "AB12345" } }),
|
|
userContext
|
|
);
|
|
|
|
expect(customerForSuper.to.path).toBe("/superuser/customers");
|
|
expect(customerForUser.to.path).toBe("/user/profile");
|
|
expect(vehicleForSuper.to.path).toBe("/superuser/vehicles/AB12345");
|
|
expect(vehicleForUser.to.path).toBe("/user/vehicles");
|
|
});
|
|
|
|
it("supports relation-target payload identifiers for orders, departments, and vehicles", () => {
|
|
const orderFromRelation = resolveSystemSearchNavigationTarget(
|
|
row("orders", {
|
|
entity_id: "34455",
|
|
payload: { order_id: "34455", department_id: 4 },
|
|
}),
|
|
adminContext
|
|
);
|
|
const departmentFromRelation = resolveSystemSearchNavigationTarget(
|
|
row("departments", {
|
|
entity_id: "4",
|
|
payload: { department_id: 4 },
|
|
}),
|
|
adminContext
|
|
);
|
|
const vehicleFromRelation = resolveSystemSearchNavigationTarget(
|
|
row("vehicles", {
|
|
entity_id: "AB12345",
|
|
payload: { reg: "AB12345" },
|
|
}),
|
|
superuserContext
|
|
);
|
|
|
|
expect(orderFromRelation.to.path).toBe("/admin/4/modules/pos/orders/34455");
|
|
expect(departmentFromRelation.to.path).toBe("/admin/4");
|
|
expect(vehicleFromRelation.to.path).toBe("/superuser/vehicles/AB12345");
|
|
});
|
|
|
|
it("routes xlvask and config/module types to their module pages with superuser access", () => {
|
|
const usage = resolveSystemSearchNavigationTarget(row("xlvask_usage_logs"), superuserContext);
|
|
const moduleConfig = resolveSystemSearchNavigationTarget(row("module_config"), superuserContext);
|
|
const fxrates = resolveSystemSearchNavigationTarget(row("fxratesapi_conversion_rates"), superuserContext);
|
|
|
|
expect(usage.to.path).toBe("/superuser/xlvask/usagelogs");
|
|
expect(moduleConfig.to.path).toBe("/superuser/configuration");
|
|
expect(fxrates.to.path).toBe("/superuser/configuration/fxratesapi");
|
|
});
|
|
|
|
it("delegates object item aliases to order-item routing behavior", () => {
|
|
const nav = resolveSystemSearchNavigationTarget(
|
|
row("objects", {
|
|
entity_id: "998",
|
|
payload: {
|
|
object_type: "item",
|
|
order_id: "34455",
|
|
department: "4",
|
|
},
|
|
}),
|
|
adminContext
|
|
);
|
|
expect(nav.strategy).toBe("deep-link");
|
|
expect(nav.to.path).toBe("/admin/4/modules/pos/orders/34455");
|
|
});
|
|
|
|
it("routes object discount/fixed-price aliases to user pricing routes", () => {
|
|
const withUser = resolveSystemSearchNavigationTarget(
|
|
row("objects", {
|
|
payload: {
|
|
object_type: "discount",
|
|
user_id: "91",
|
|
},
|
|
}),
|
|
superuserContext
|
|
);
|
|
const noUser = resolveSystemSearchNavigationTarget(
|
|
row("objects", {
|
|
payload: {
|
|
object_type: "fixed_pricing",
|
|
},
|
|
}),
|
|
superuserContext
|
|
);
|
|
|
|
expect(withUser.to.path).toBe("/superuser/users/91/pricing");
|
|
expect(noUser.to.path).toBe("/superuser/users");
|
|
});
|
|
|
|
it("routes customer object aliases to the linked superuser user page when customer_user_id is present", () => {
|
|
const nav = resolveSystemSearchNavigationTarget(
|
|
row("objects", {
|
|
entity_id: "1952",
|
|
payload: {
|
|
object_type: "customer",
|
|
id: 1952,
|
|
customer_number: 12345679,
|
|
display_name: "Demo",
|
|
customer_name: "(TEST) Pleno Vognmandsforretning",
|
|
customer_context: {
|
|
customer_number: 12345679,
|
|
user_id: 1953,
|
|
name: "(TEST) Pleno Vognmandsforretning",
|
|
},
|
|
customer_user_id: 1953,
|
|
},
|
|
}),
|
|
superuserContext
|
|
);
|
|
|
|
expect(nav.strategy).toBe("deep-link");
|
|
expect(nav.to.path).toBe("/superuser/users/1953");
|
|
});
|
|
|
|
it("routes noisy attachment-like object aliases to the linked order page", () => {
|
|
const nav = resolveSystemSearchNavigationTarget(
|
|
row("objects", {
|
|
payload: {
|
|
object_type: "`DOCUMENT`",
|
|
order_id: "34455",
|
|
department_id: 4,
|
|
},
|
|
}),
|
|
adminContext
|
|
);
|
|
expect(nav.strategy).toBe("deep-link");
|
|
expect(nav.to.path).toBe("/admin/4/modules/pos/orders/34455");
|
|
});
|
|
|
|
it("routes object question/task/condition aliases to self-serve module pages", () => {
|
|
const questionNav = resolveSystemSearchNavigationTarget(
|
|
row("objects", {
|
|
payload: { object_type: "question", department_id: 4 },
|
|
}),
|
|
adminContext
|
|
);
|
|
const taskNav = resolveSystemSearchNavigationTarget(
|
|
row("objects", {
|
|
payload: { object_type: "task", department_id: 4 },
|
|
}),
|
|
adminContext
|
|
);
|
|
const conditionNav = resolveSystemSearchNavigationTarget(
|
|
row("objects", {
|
|
payload: { object_type: "condition", department_id: 4 },
|
|
}),
|
|
adminContext
|
|
);
|
|
|
|
expect(questionNav.to.path).toBe("/admin/4/modules/self-serve/questions");
|
|
expect(taskNav.to.path).toBe("/admin/4/modules/self-serve/tasks");
|
|
expect(conditionNav.to.path).toBe("/admin/4/modules/self-serve/conditions");
|
|
});
|
|
|
|
it("keeps generic fallback when object subtype lacks required routing context", () => {
|
|
const nav = resolveSystemSearchNavigationTarget(
|
|
row("objects", {
|
|
payload: { object_type: "discount" },
|
|
}),
|
|
restrictedContext
|
|
);
|
|
expect(nav.strategy).toBe("generic");
|
|
expect(nav.to.path).toContain("/search/system/record/customer_discounts/11");
|
|
});
|
|
|
|
it("routes department variables by role and department context", () => {
|
|
const adminNav = resolveSystemSearchNavigationTarget(
|
|
row("department_variables", {
|
|
payload: { department_id: 4 },
|
|
}),
|
|
adminContext
|
|
);
|
|
const superNav = resolveSystemSearchNavigationTarget(
|
|
row("department_variables", {
|
|
payload: { department_id: 4 },
|
|
}),
|
|
superuserContext
|
|
);
|
|
|
|
expect(adminNav.to.path).toBe("/admin/4");
|
|
expect(superNav.to.path).toBe("/superuser/departments/4");
|
|
});
|
|
|
|
it("handles missing payload safely and still opens generic detail", () => {
|
|
const nav = resolveSystemSearchNavigationTarget(row("module_action_logs", { payload: null }), userContext);
|
|
expect(nav.strategy).toBe("generic");
|
|
expect(nav.to.path).toContain("/search/system/record/module_action_logs/11");
|
|
});
|
|
});
|
|
|
|
describe("system search card view-model behavior", () => {
|
|
it("creates readable key-field summaries for json-like payload strings", () => {
|
|
const vm = buildSystemSearchViewModel(
|
|
row("order_bookings", {
|
|
payload: {
|
|
items: '[{"id": 10, "name": "Indvendig vask Trailer"}, {"id": 41, "name": "Safety Seal"}]',
|
|
datetime: "2025-11-24 23:00:00",
|
|
reg_1: "FC2861",
|
|
},
|
|
})
|
|
);
|
|
|
|
const itemsField = vm.keyFields.find((entry) => entry.label === "Items");
|
|
expect(itemsField.value).toContain("2 items");
|
|
expect(itemsField.value).toContain("(complete)");
|
|
expect(itemsField.value).toContain("Indvendig vask Trailer");
|
|
expect(itemsField.value).toContain("Safety Seal");
|
|
expect(vm.parsedPayload.items).toBeTypeOf("object");
|
|
});
|
|
|
|
it("picks up nested booking items and reports missing details when content is incomplete", () => {
|
|
const vm = buildSystemSearchViewModel(
|
|
row("order_bookings", {
|
|
payload: {
|
|
content: {
|
|
items: [{ id: 10, name: "Bus" }, { id: 41, name: "Spot Free- Lastbil" }, { id: 91 }],
|
|
},
|
|
datetime: "2025-11-24 23:00:00",
|
|
reg_1: "FC2861",
|
|
},
|
|
})
|
|
);
|
|
|
|
const itemsField = vm.keyFields.find((entry) => entry.label === "Items");
|
|
expect(itemsField.value).toContain("3 items");
|
|
expect(itemsField.value).toContain("missing details: 1");
|
|
expect(itemsField.value).toContain("Bus");
|
|
expect(itemsField.value).toContain("Spot Free- Lastbil");
|
|
expect(itemsField.value).toContain("#91");
|
|
});
|
|
|
|
it("shows customer discount value and timestamps in key fields", () => {
|
|
const vm = buildSystemSearchViewModel(
|
|
row("customer_discounts", {
|
|
entity_id: "326",
|
|
title: "Discount #326",
|
|
payload: {
|
|
id: 326,
|
|
customer_number: 12345679,
|
|
discount: 12.5,
|
|
fixed_price: 350,
|
|
object_id: "global",
|
|
is_category: true,
|
|
created_at: "2026-03-12T09:15:00Z",
|
|
},
|
|
})
|
|
);
|
|
|
|
expect(vm.keyFields.some((entry) => entry.label === "Discount" && entry.value === "12.5%")).toBe(true);
|
|
expect(vm.keyFields.some((entry) => entry.label === "Fixed price" && entry.value === "350")).toBe(true);
|
|
expect(vm.keyFields.some((entry) => entry.label === "Target" && entry.value === "global")).toBe(true);
|
|
expect(vm.keyFields.some((entry) => entry.label === "Category" && entry.value === "Yes")).toBe(true);
|
|
expect(vm.keyFields.some((entry) => entry.label === "Created" && entry.value.includes("2026-03-12"))).toBe(true);
|
|
});
|
|
|
|
it("reuses customer-discount render fields for objects discount aliases", () => {
|
|
const vm = buildSystemSearchViewModel(
|
|
row("objects", {
|
|
entity_id: "326",
|
|
title: "326",
|
|
payload: {
|
|
object_type: "discount",
|
|
customer_number: 12345679,
|
|
percentage: 8,
|
|
product_or_category_id: "42",
|
|
is_category: false,
|
|
},
|
|
})
|
|
);
|
|
|
|
expect(vm.keyFields.some((entry) => entry.label === "Discount" && entry.value === "8%")).toBe(true);
|
|
expect(vm.keyFields.some((entry) => entry.label === "Target" && entry.value === "42")).toBe(true);
|
|
expect(vm.keyFields.some((entry) => entry.label === "Category" && entry.value === "No")).toBe(true);
|
|
});
|
|
|
|
it("surfaces nested goal criteria and progress details for department goals", () => {
|
|
const vm = buildSystemSearchViewModel(
|
|
row("department_goals", {
|
|
entity_id: "1",
|
|
title: "1",
|
|
payload: {
|
|
id: 1,
|
|
criteria: {
|
|
label: "Monthly Revenue Goal",
|
|
type: "REVENUE",
|
|
start: "2026-03-01",
|
|
end: "2026-03-31",
|
|
target: 100000,
|
|
},
|
|
progress: {
|
|
to_date: {
|
|
count: 42000,
|
|
target: 100000,
|
|
},
|
|
},
|
|
},
|
|
})
|
|
);
|
|
|
|
expect(vm.title).toBe("Monthly Revenue Goal");
|
|
expect(vm.description).toBe("REVENUE");
|
|
expect(vm.keyFields.some((entry) => entry.label === "Start" && entry.value.includes("2026-03-01"))).toBe(true);
|
|
expect(vm.keyFields.some((entry) => entry.label === "End" && entry.value.includes("2026-03-31"))).toBe(true);
|
|
expect(vm.keyFields.some((entry) => entry.label === "Target" && entry.value.includes("100000"))).toBe(true);
|
|
expect(vm.keyFields.some((entry) => entry.label === "Progress" && entry.value.includes("42%"))).toBe(true);
|
|
});
|
|
|
|
it("reuses department-goal render fields for objects goal aliases", () => {
|
|
const vm = buildSystemSearchViewModel(
|
|
row("objects", {
|
|
entity_id: "1",
|
|
title: "1",
|
|
payload: {
|
|
object_type: "`goal`",
|
|
criteria: {
|
|
label: "Weekly Throughput Goal",
|
|
type: "ORDERS",
|
|
start: "2026-03-10",
|
|
end: "2026-03-17",
|
|
target: 50,
|
|
},
|
|
progress: {
|
|
this_week: {
|
|
count: 21,
|
|
target: 50,
|
|
},
|
|
},
|
|
},
|
|
})
|
|
);
|
|
|
|
expect(vm.title).toBe("Weekly Throughput Goal");
|
|
expect(vm.description).toBe("ORDERS");
|
|
expect(vm.keyFields.some((entry) => entry.label === "Start" && entry.value.includes("2026-03-10"))).toBe(true);
|
|
expect(vm.keyFields.some((entry) => entry.label === "End" && entry.value.includes("2026-03-17"))).toBe(true);
|
|
expect(vm.keyFields.some((entry) => entry.label === "Target" && entry.value.includes("50"))).toBe(true);
|
|
expect(vm.keyFields.some((entry) => entry.label === "Progress" && entry.value.includes("42%"))).toBe(true);
|
|
});
|
|
});
|