Files
Megamenu/tests/MegaMenuCore.test.ts
Torsten Brendgen 50b85468f1 feat: Enhance MegaMenu with localization, caching, and new properties
- Added localization support for loading states, empty navigation, and submenu actions in both German and English.
- Introduced new properties in IMenuItem for description, openInNewWindow, and external links.
- Updated SPTermStorePickerService to handle cache versioning and language-specific caching.
- Implemented a new MegaMenuCore service for handling term properties, cache management, and navigation URL resolution.
- Refactored TaxonomyNavigationService to build menu hierarchy and filter hidden terms.
- Added automated tests for MegaMenu core functionalities and static asset validation.
- Removed unused ItemDictionary service.
- Created a comprehensive ToDo document outlining the implementation status and future tasks.
2026-07-20 21:34:24 +02:00

90 lines
4.8 KiB
TypeScript

import {
buildMenuHierarchy,
buildTermCacheKey,
compareTermsByConfiguredOrder,
getLocalizedTermLabel,
getTermProperty,
isExternalNavigationUrl,
isNavigationUrlActive,
isTermHidden,
MegaMenuTermProperties,
parseBooleanProperty,
readCachedValue,
resolveNavigationUrl
} from '../src/services/MegaMenuCore';
import { ITerm } from '../src/services/ISPTermStorePickerService';
let passed: number = 0;
function assert(name: string, condition: boolean): void {
if (!condition) {
throw new Error('FAILED: ' + name);
}
passed++;
console.log('OK:', name);
}
function term(values: any): ITerm { // tslint:disable-line:no-any
return values as ITerm;
}
assert('boolean property accepts German yes', parseBooleanProperty('ja'));
assert('boolean property rejects false', !parseBooleanProperty('false'));
assert('hidden local term property', isTermHidden(term({ LocalCustomProperties: { 'MegaMenu.Hidden': 'true' } })));
assert('description local term property', getTermProperty(term({ LocalCustomProperties: { 'MegaMenu.Description': 'Forms' } }), MegaMenuTermProperties.description) === 'Forms');
assert('open-in-new-window local term property', parseBooleanProperty(getTermProperty(term({ LocalCustomProperties: { 'MegaMenu.OpenInNewWindow': '1' } }), MegaMenuTermProperties.openInNewWindow)));
assert('site collection token is resolved',
resolveNavigationUrl('~sitecollection/SitePages/Home.aspx', 'https://portal/sites/hr') ===
'https://portal/sites/hr/SitePages/Home.aspx');
assert('relative URL is accepted', resolveNavigationUrl('/sites/hr', 'https://portal') === '/sites/hr');
assert('javascript URL is blocked', resolveNavigationUrl('javascript:alert(1)', 'https://portal') === undefined);
assert('data URL is blocked', resolveNavigationUrl('data:text/html,test', 'https://portal') === undefined);
assert('control characters in URL are blocked', resolveNavigationUrl('java\nscript:alert(1)', 'https://portal') === undefined);
assert('mailto URL is accepted', resolveNavigationUrl('mailto:test@example.com', 'https://portal') === 'mailto:test@example.com');
assert('external host is detected', isExternalNavigationUrl('https://example.com/a', 'https://portal'));
assert('protocol-relative external host is detected', isExternalNavigationUrl('//example.com/a', 'https://portal'));
assert('same origin is not external', !isExternalNavigationUrl('https://portal/a', 'https://portal'));
assert('current child page activates navigation parent',
isNavigationUrlActive('https://portal/sites/hr/docs/view.aspx?x=1', '/sites/hr/docs', 'https://portal'));
assert('similar path does not create false active state',
!isNavigationUrlActive('https://portal/sites/hr/documents', '/sites/hr/docs', 'https://portal'));
const keyA: string = buildTermCacheKey('https://portal/sites/a', 'ABC', 1031, '1');
const keyB: string = buildTermCacheKey('https://portal/sites/a', 'ABC', 1033, '1');
const keyC: string = buildTermCacheKey('https://portal/sites/a', 'ABC', 1031, '2');
assert('cache key includes language', keyA !== keyB);
assert('cache key includes configuration version', keyA !== keyC);
assert('cache key is deterministic', keyA === buildTermCacheKey('https://portal/sites/a', 'abc', 1031, '1'));
assert('valid cache value is returned', readCachedValue<string>(JSON.stringify({ expiresAt: 200, value: 'menu' }), 100, false) === 'menu');
assert('expired cache is rejected normally', readCachedValue<string>(JSON.stringify({ expiresAt: 50, value: 'menu' }), 100, false) === undefined);
assert('expired cache is available as fallback', readCachedValue<string>(JSON.stringify({ expiresAt: 50, value: 'menu' }), 100, true) === 'menu');
assert('malformed cache is rejected', readCachedValue<string>('{invalid', 100, true) === undefined);
const localized = term({
Name: 'Default',
Labels: { _Child_Items_: [
{ Language: 1033, Value: 'Company', IsDefaultForLanguage: true },
{ Language: 1031, Value: 'Unternehmen' }
] }
});
assert('German term label is selected', getLocalizedTermLabel(localized, 1031) === 'Unternehmen');
assert('default label is fallback', getLocalizedTermLabel(localized, 1040) === 'Company');
interface ITestItem { name: string; items?: ITestItem[]; }
const child: ITestItem = { name: 'child' };
const root: ITestItem = { name: 'root' };
const hierarchy: ITestItem[] = buildMenuHierarchy([
{ id: 'child', parentId: 'root', depth: 2, item: child },
{ id: 'root', depth: 1, item: root }
]);
assert('hierarchy works when child precedes parent', hierarchy.length === 1 && hierarchy[0].items[0] === child);
const sorted: ITerm[] = [
term({ PathOfTerm: 'B', CustomSortOrderIndex: 1 }),
term({ PathOfTerm: 'A', CustomSortOrderIndex: 0 })
].sort(compareTermsByConfiguredOrder);
assert('custom term order wins', sorted[0].PathOfTerm === 'A');
console.log('MegaMenu core tests passed:', passed);