- Introduced `.prettierrc.json` to enforce consistent code formatting across the project. - Updated unit and e2e test files to address formatting issues, improve readability, and ensure alignment with the new Prettier configuration.
456 lines
16 KiB
JavaScript
456 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,
|
|
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 === "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);
|
|
});
|
|
});
|