Files
pleno-vue/tests/unit/system-search-modal-contract.spec.js
T

167 lines
10 KiB
JavaScript

import { readFileSync } from "node:fs";
import { join } from "node:path";
import { describe, expect, it } from "vitest";
import { readJsonFile } from "./helpers/readJsonFile";
const root = process.cwd();
const modalSource = readFileSync(
join(root, "src/components/viewport/page/headers/menu/NavigationMenuGlobalSearch.vue"),
"utf8"
);
const supportSource = readFileSync(
join(root, "src/components/viewport/page/headers/menu/systemSearchSupport.ts"),
"utf8"
);
const departmentGoalSupportSource = readFileSync(
join(root, "src/components/viewport/page/headers/menu/systemSearchDepartmentGoalSupport.ts"),
"utf8"
);
const enLocale = readJsonFile(join(root, "src/i18n/locales/en.json"));
const daLocale = readJsonFile(join(root, "src/i18n/locales/da.json"));
const deLocale = readJsonFile(join(root, "src/i18n/locales/de.json"));
const entityTypesMatch = supportSource.match(/export const SYSTEM_SEARCH_ENTITY_TYPES = \[(.*?)\] as const;/s);
const systemSearchEntityTypes = Array.from(entityTypesMatch?.[1]?.matchAll(/'([^']+)'/g) ?? [], (match) => match[1]);
const findMissingEntityTypeLabels = (localeSource) =>
systemSearchEntityTypes.filter(
(entityType) =>
!localeSource.global_search?.entity_types?.[entityType] && !localeSource.objects?.[entityType]?.title
);
describe("advanced system search modal UI contract", () => {
it("uses support registry to build typed cards and open targets", () => {
expect(modalSource).toContain("buildSystemSearchViewModel");
expect(modalSource).toContain("resolveSystemSearchNavigationTarget");
expect(modalSource).toContain("const visibleResultCards = computed");
expect(modalSource).toContain("const keyboardState = ref<SystemSearchKeyboardState>");
expect(modalSource).toContain("const onSearchShellKeydown = (event: KeyboardEvent) =>");
expect(modalSource).toContain("const focusQueryFromKeyboardNavigation = () =>");
expect(modalSource).toContain("const isQueryInputTarget = (target: EventTarget | null): boolean =>");
expect(modalSource).toContain("const setSidebarGroupButtonRef = (key: string, element: Element | null) =>");
expect(modalSource).toContain("const setResultCardRef = (key: string, element: Element | null) =>");
expect(modalSource).toContain("const scrollSelectedResultIntoView = (selectedResultKey: string | null) => {");
expect(modalSource).toContain(
"const ensureDepartmentGoalProgressLoaded = async (card: VisibleResultCard): Promise<void> => {"
);
expect(modalSource).toContain("await SessionUser.request(");
expect(modalSource).toContain("'/goals/department'");
expect(modalSource).toContain("{ id: Number.isInteger(goalId) ? goalId : card.departmentGoalId }");
expect(modalSource).toContain("new window.IntersectionObserver((entries) => {");
expect(modalSource).toContain(
"const highlightFirstVisibleResult = (pane: SystemSearchKeyboardPane = keyboardState.value.pane) =>"
);
expect(modalSource).toContain("const onQueryInputEnter = () => {");
expect(modalSource).toContain("resultScrollBoxRef.value?.scrollTo({ top: 0 })");
expect(modalSource).toContain("scrollIntoView({ block: 'nearest', inline: 'nearest' })");
expect(modalSource).toContain('@keydown.capture="onSearchShellKeydown"');
expect(modalSource).toContain('@keyup.enter="onQueryInputEnter"');
expect(modalSource).toContain('v-for="card in visibleResultCards"');
expect(modalSource).toContain("card.viewModel.keyFields");
expect(modalSource).toContain("const relatedTargets = resolveCardRelatedTargets(");
expect(modalSource).toContain("relatedTargets,");
expect(modalSource).toContain("resolveSyntheticNavigationTarget");
expect(modalSource).toContain('@click="openResult(card)"');
expect(modalSource).not.toContain("const resolvePath =");
});
it("renders null-safe payload inspector and badge/key-field structure", () => {
expect(modalSource).toContain("card.viewModel.parsedPayload ?? card.viewModel.rawPayload");
expect(modalSource).toContain("result-badges");
expect(modalSource).toContain("result-key-fields");
expect(modalSource).toContain("const resultRangeLabel = computed(() => (");
expect(modalSource).toContain("const resolveEntityTypeLabel = (entityType: string): string => {");
expect(modalSource).toContain("const objectTitleKey = `objects.${normalized}.title`;");
expect(modalSource).toContain("const humanize = (value: string) => resolveEntityTypeLabel(value);");
expect(modalSource).toContain("result-item--selected");
expect(modalSource).toContain("sidebar-group-button--selected");
expect(modalSource).toContain("sidebar-group-button--active-pane");
expect(modalSource).toContain("search-result-toolbar__summary");
expect(modalSource).not.toContain("result-group-summary");
expect(modalSource).not.toContain("showGrouped");
expect(modalSource).toContain('title="Arrow keys navigate groups"');
expect(modalSource).toContain('title="Arrow keys navigate results"');
expect(modalSource).toContain("result-object-id");
expect(modalSource).toContain(':ref="(element) => setSidebarGroupButtonRef(group.key, element)"');
expect(modalSource).toContain(':ref="(element) => setResultCardRef(getCardKey(card), element)"');
expect(modalSource).toContain('ref="resultScrollBoxRef" class="box p-3 search-result-box"');
expect(modalSource).toContain("related-actions-panel");
expect(modalSource).toContain('v-if="hasRelatedActions(card)"');
expect(modalSource).toContain(
'v-if="shouldShowDepartmentGoalOverview(card)" class="department-goal-overview mb-2"'
);
expect(modalSource).toContain("{{ t('global_search.department_goals.overview') }}");
expect(modalSource).toContain("@click=\"openRelatedTarget(card, 'order')\"");
expect(modalSource).toContain("@click=\"openRelatedTarget(card, 'department')\"");
expect(modalSource).toContain("@click=\"openRelatedTarget(card, 'vehicle')\"");
expect(modalSource).toContain('@click="copyCardIdentifier(card)"');
expect(modalSource).toContain('@click="copyCardReference(card)"');
expect(modalSource).toContain('@click="copyCardRegistration(card)"');
expect(supportSource).toContain("export const normalizePayload");
expect(departmentGoalSupportSource).toContain(
"export const buildDepartmentGoalOverview = (payload: SearchPayload | null, fallbackId: string | null = null): DepartmentGoalOverview | null => {"
);
});
it("defines translated search entity labels for every system search entity type", () => {
expect(entityTypesMatch).not.toBeNull();
expect(findMissingEntityTypeLabels(enLocale)).toEqual([]);
expect(findMissingEntityTypeLabels(daLocale)).toEqual([]);
expect(findMissingEntityTypeLabels(deLocale)).toEqual([]);
expect(enLocale.global_search.entity_types.bookings).toBe("Legacy bookings");
expect(daLocale.global_search.entity_types.bookings).toBe("Legacy bookinger");
expect(deLocale.global_search.entity_types.bookings).toBe("Legacy-Buchungen");
expect(enLocale.global_search.department_goals.overview).toBe("Goal progress");
expect(daLocale.global_search.department_goals.overview).toBe("Målfremskridt");
expect(deLocale.global_search.department_goals.overview).toBe("Zielstatus");
});
it("resolves customer actions from nested payload fields before falling back to generic customer routes", () => {
expect(modalSource).toContain("'customer_context.customer_number'");
expect(modalSource).toContain("'customer_context.user_id'");
expect(modalSource).toContain(
"const extractUserIdFromPayload = (row: SearchResult, payload: SearchPayload | null, entityAlias: string): number | null =>"
);
expect(modalSource).toContain(
"const resolvedUserId = card.userId ?? await resolveUserIdForDiscountEdit(card.viewModel.rawPayload, card.customerNumber);"
);
const resolveUserIndex = modalSource.indexOf(
"const resolvedUserId = card.userId ?? await resolveUserIdForDiscountEdit(card.viewModel.rawPayload, card.customerNumber);"
);
const relatedTargetIndex = modalSource.indexOf("if (card.relatedTargets.customer) {");
expect(resolveUserIndex).toBeGreaterThan(-1);
expect(relatedTargetIndex).toBeGreaterThan(resolveUserIndex);
});
it("registers a global double-shift shortcut that opens the search modal", () => {
expect(modalSource).toContain("const DOUBLE_SHIFT_MAX_INTERVAL_MS = 420");
expect(modalSource).toContain("const onGlobalKeyDown = (event: KeyboardEvent) =>");
expect(modalSource).toContain("const onGlobalKeyUp = (event: KeyboardEvent) =>");
expect(modalSource).toContain("selectFirstVisibleResult();");
expect(modalSource).toContain("highlightFirstVisibleResult('query');");
expect(modalSource).toContain("focusKeyboardPane('sidebar');");
expect(modalSource).toContain("focusQueryFromKeyboardNavigation();");
expect(modalSource).toContain("isQueryInputTarget(event.target) && event.key === 'ArrowDown'");
expect(modalSource).toContain("shouldBypassTextEntryForSystemSearchKey(keyboardState.value.pane, event.key)");
expect(modalSource).toContain("if (event.key === 'Shift')");
expect(modalSource).toContain("openSearchFromShortcut();");
expect(modalSource).toContain("window.addEventListener('keydown', onGlobalKeyDown)");
expect(modalSource).toContain("window.addEventListener('keyup', onGlobalKeyUp)");
expect(modalSource).toContain("window.removeEventListener('keydown', onGlobalKeyDown)");
expect(modalSource).toContain("window.removeEventListener('keyup', onGlobalKeyUp)");
});
it("sends include/exclude type filters as arrays for both GET and POST requests", () => {
expect(modalSource).toContain("include_types?: EntityType[]");
expect(modalSource).toContain("exclude_types?: EntityType[]");
expect(modalSource).toContain("params.include_types = [...includeTypes.value]");
expect(modalSource).toContain("params.exclude_types = [...excludeTypes.value]");
expect(modalSource).toContain("body.include_types = [...includeTypes.value]");
expect(modalSource).toContain("body.exclude_types = [...excludeTypes.value]");
expect(modalSource).not.toContain("params.include_types = includeTypes.value.join(',')");
expect(modalSource).not.toContain("params.exclude_types = excludeTypes.value.join(',')");
});
});