167 lines
10 KiB
JavaScript
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(',')");
|
|
});
|
|
});
|