Files
VRCX/src/components/nav-menu/__tests__/navMenuUtils.test.js
2026-03-17 20:56:25 +09:00

314 lines
11 KiB
JavaScript

import { describe, expect, test } from 'vitest';
import {
getFirstNavRoute,
isEntryNotified,
normalizeHiddenKeys,
sanitizeLayout
} from '../navMenuUtils';
// Minimal nav definitions for testing
const testDefinitions = [
{ key: 'feed', routeName: 'feed' },
{ key: 'search', routeName: 'search' },
{ key: 'tools', routeName: 'tools' },
{ key: 'charts-instance', routeName: 'charts-instance' },
{ key: 'charts-mutual', routeName: 'charts-mutual' },
{ key: 'charts-hot-worlds', routeName: 'charts-hot-worlds' },
{ key: 'notification', routeName: 'notification' },
{ key: 'direct-access', action: 'direct-access' }
];
const testDefinitionMap = new Map(testDefinitions.map((d) => [d.key, d]));
const mockT = (key) => `translated:${key}`;
const mockGenerateFolderId = () => 'generated-folder-id';
// ─── normalizeHiddenKeys ─────────────────────────────────────────────
describe('normalizeHiddenKeys', () => {
test('returns empty array for non-array input', () => {
expect(normalizeHiddenKeys(null, testDefinitionMap)).toEqual([]);
expect(normalizeHiddenKeys(undefined, testDefinitionMap)).toEqual([]);
expect(normalizeHiddenKeys('string', testDefinitionMap)).toEqual([]);
expect(normalizeHiddenKeys(42, testDefinitionMap)).toEqual([]);
});
test('returns empty array for empty array', () => {
expect(normalizeHiddenKeys([], testDefinitionMap)).toEqual([]);
});
test('filters out invalid keys', () => {
expect(
normalizeHiddenKeys(
['feed', 'nonexistent', 'search'],
testDefinitionMap
)
).toEqual(['feed', 'search']);
});
test('deduplicates keys', () => {
expect(
normalizeHiddenKeys(['feed', 'feed', 'search'], testDefinitionMap)
).toEqual(['feed', 'search']);
});
test('filters out falsy values', () => {
expect(
normalizeHiddenKeys(
[null, '', undefined, 'feed'],
testDefinitionMap
)
).toEqual(['feed']);
});
test('preserves order of valid keys', () => {
expect(
normalizeHiddenKeys(['tools', 'feed', 'search'], testDefinitionMap)
).toEqual(['tools', 'feed', 'search']);
});
});
// ─── getFirstNavRoute ────────────────────────────────────────────────
describe('getFirstNavRoute', () => {
test('returns null for empty layout', () => {
expect(getFirstNavRoute([], testDefinitionMap)).toBeNull();
});
test('returns first item routeName', () => {
const layout = [{ type: 'item', key: 'feed' }];
expect(getFirstNavRoute(layout, testDefinitionMap)).toBe('feed');
});
test('skips items without routeName', () => {
const layout = [
{ type: 'item', key: 'direct-access' },
{ type: 'item', key: 'search' }
];
expect(getFirstNavRoute(layout, testDefinitionMap)).toBe('search');
});
test('returns route from folder items', () => {
const layout = [
{
type: 'folder',
items: ['feed', 'search']
}
];
expect(getFirstNavRoute(layout, testDefinitionMap)).toBe('feed');
});
test('returns null when no routable items exist', () => {
const layout = [{ type: 'item', key: 'direct-access' }];
expect(getFirstNavRoute(layout, testDefinitionMap)).toBeNull();
});
test('returns null for unknown keys', () => {
const layout = [{ type: 'item', key: 'unknown' }];
expect(getFirstNavRoute(layout, testDefinitionMap)).toBeNull();
});
test('checks folder items for routable entry', () => {
const layout = [
{
type: 'folder',
items: ['direct-access', 'tools']
}
];
expect(getFirstNavRoute(layout, testDefinitionMap)).toBe('tools');
});
});
// ─── isEntryNotified ─────────────────────────────────────────────────
describe('isEntryNotified', () => {
test('returns false for null/undefined entry', () => {
expect(isEntryNotified(null, ['feed'])).toBe(false);
expect(isEntryNotified(undefined, ['feed'])).toBe(false);
});
test('matches by index', () => {
const entry = { index: 'feed' };
expect(isEntryNotified(entry, ['feed', 'search'])).toBe(true);
});
test('matches by routeName', () => {
const entry = { routeName: 'search' };
expect(isEntryNotified(entry, ['search'])).toBe(true);
});
test('matches by path last segment', () => {
const entry = { path: '/app/settings' };
expect(isEntryNotified(entry, ['settings'])).toBe(true);
});
test('returns false when no match', () => {
const entry = { index: 'feed', routeName: 'feed' };
expect(isEntryNotified(entry, ['search', 'tools'])).toBe(false);
});
test('matches any of multiple targets', () => {
const entry = {
index: 'feed',
routeName: 'home',
path: '/app/dashboard'
};
expect(isEntryNotified(entry, ['dashboard'])).toBe(true);
});
test('returns false for empty notifiedMenus', () => {
const entry = { index: 'feed' };
expect(isEntryNotified(entry, [])).toBe(false);
});
});
// ─── sanitizeLayout ──────────────────────────────────────────────────
describe('sanitizeLayout', () => {
const runSanitize = (layout, hiddenKeys = []) =>
sanitizeLayout(
layout,
hiddenKeys,
testDefinitionMap,
testDefinitions,
mockT,
mockGenerateFolderId
);
test('returns default items for null/undefined layout', () => {
const result = runSanitize(null);
// Should include all non-chart items + charts folder
expect(result.length).toBeGreaterThan(0);
expect(result.some((e) => e.type === 'item' && e.key === 'feed')).toBe(
true
);
});
test('preserves valid item entries', () => {
const layout = [{ type: 'item', key: 'feed' }];
const result = runSanitize(layout);
expect(result[0]).toEqual({ type: 'item', key: 'feed' });
});
test('skips invalid item keys', () => {
const layout = [
{ type: 'item', key: 'feed' },
{ type: 'item', key: 'nonexistent' }
];
const result = runSanitize(layout);
expect(result.find((e) => e.key === 'nonexistent')).toBeUndefined();
});
test('deduplicates item keys', () => {
const layout = [
{ type: 'item', key: 'feed' },
{ type: 'item', key: 'feed' }
];
const result = runSanitize(layout);
const feedEntries = result.filter(
(e) => e.type === 'item' && e.key === 'feed'
);
expect(feedEntries.length).toBe(1);
});
test('creates folder entries from valid items', () => {
const layout = [
{
type: 'folder',
id: 'my-folder',
name: 'My Folder',
icon: 'ri-star-line',
items: ['feed', 'search']
}
];
const result = runSanitize(layout);
const folder = result.find(
(e) => e.type === 'folder' && e.id === 'my-folder'
);
expect(folder).toBeDefined();
expect(folder.items).toEqual(['feed', 'search']);
expect(folder.name).toBe('My Folder');
});
test('generates folder ID when missing', () => {
const layout = [
{
type: 'folder',
name: 'No ID Folder',
items: ['feed']
}
];
const result = runSanitize(layout);
const folder = result.find((e) => e.type === 'folder');
expect(folder.id).toBe('generated-folder-id');
});
test('translates folder name from nameKey', () => {
const layout = [
{
type: 'folder',
id: 'f1',
nameKey: 'nav_tooltip.favorites',
items: ['feed']
}
];
const result = runSanitize(layout);
const folder = result.find((e) => e.type === 'folder');
expect(folder.name).toBe('translated:nav_tooltip.favorites');
});
test('appends missing definitions not in layout or hidden', () => {
const layout = [{ type: 'item', key: 'feed' }];
const result = runSanitize(layout);
// All non-chart, non-hidden items should be present
expect(result.some((e) => e.key === 'search')).toBe(true);
expect(result.some((e) => e.key === 'tools')).toBe(true);
});
test('does not append hidden keys', () => {
const layout = [{ type: 'item', key: 'feed' }];
const result = runSanitize(layout, ['search', 'tools']);
expect(
result.find((e) => e.type === 'item' && e.key === 'search')
).toBeUndefined();
expect(
result.find((e) => e.type === 'item' && e.key === 'tools')
).toBeUndefined();
});
test('converts legacy "charts" item to charts folder', () => {
const layout = [{ type: 'item', key: 'charts' }];
const result = runSanitize(layout);
const chartsFolder = result.find(
(e) => e.type === 'folder' && e.id === 'default-folder-charts'
);
expect(chartsFolder).toBeDefined();
expect(chartsFolder.items).toEqual([
'charts-instance',
'charts-mutual',
'charts-hot-worlds'
]);
});
test('auto-appends charts folder when charts keys are neither used nor hidden', () => {
const layout = [{ type: 'item', key: 'feed' }];
const result = runSanitize(layout);
const chartsFolder = result.find(
(e) => e.type === 'folder' && e.id === 'default-folder-charts'
);
expect(chartsFolder).toBeDefined();
});
test('skips empty folders', () => {
const layout = [
{
type: 'folder',
id: 'empty-folder',
name: 'Empty',
items: []
}
];
const result = runSanitize(layout);
expect(result.find((e) => e.id === 'empty-folder')).toBeUndefined();
});
});