- 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.
90 lines
4.8 KiB
TypeScript
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);
|