@furystack/shades
Version:
A lightweight UI framework for FuryStack with JSX support
272 lines • 10.8 kB
JavaScript
import { createInjector, Injector } from '@furystack/inject';
import { describe, expect, expectTypeOf, it } from 'vitest';
import { buildDocumentTitle, extractNavTree, resolveRouteTitle, resolveRouteTitles, } from './route-meta-utils.js';
describe('resolveRouteTitle', () => {
const injector = createInjector();
it('should return undefined when no meta is configured', async () => {
const entry = {
route: { component: () => ({}) },
match: { path: '/about', params: {} },
query: null,
hash: undefined,
};
expect(await resolveRouteTitle(entry, injector)).toBeUndefined();
});
it('should return undefined when meta has no title', async () => {
const entry = {
route: { meta: {}, component: () => ({}) },
match: { path: '/about', params: {} },
query: null,
hash: undefined,
};
expect(await resolveRouteTitle(entry, injector)).toBeUndefined();
});
it('should return a static string title', async () => {
const entry = {
route: { meta: { title: 'About' }, component: () => ({}) },
match: { path: '/about', params: {} },
query: null,
hash: undefined,
};
expect(await resolveRouteTitle(entry, injector)).toBe('About');
});
it('should resolve a synchronous function title', async () => {
const entry = {
route: {
meta: { title: ({ match }) => `User ${match.params.id}` },
component: () => ({}),
},
match: { path: '/users/42', params: { id: '42' } },
query: null,
hash: undefined,
};
expect(await resolveRouteTitle(entry, injector)).toBe('User 42');
});
it('should resolve an async function title', async () => {
const entry = {
route: {
meta: {
title: async ({ match }) => {
const { id } = match.params;
return `Movie ${id}`;
},
},
component: () => ({}),
},
match: { path: '/movies/7', params: { id: '7' } },
query: null,
hash: undefined,
};
expect(await resolveRouteTitle(entry, injector)).toBe('Movie 7');
});
it('should pass the injector to the title resolver', async () => {
const entry = {
route: {
meta: {
title: ({ injector: inj }) => (inj instanceof Injector ? 'has-injector' : 'no-injector'),
},
component: () => ({}),
},
match: { path: '/test', params: {} },
query: null,
hash: undefined,
};
expect(await resolveRouteTitle(entry, injector)).toBe('has-injector');
});
});
describe('resolveRouteTitles', () => {
const injector = createInjector();
it('should resolve all titles in a mixed chain', async () => {
const chain = [
{
route: { meta: { title: 'Media' }, component: () => ({}) },
match: { path: '/media', params: {} },
query: null,
hash: undefined,
},
{
route: { meta: { title: 'Movies' }, component: () => ({}) },
match: { path: '/movies', params: {} },
query: null,
hash: undefined,
},
{
route: {
meta: { title: async ({ match }) => `Movie ${match.params.id}` },
component: () => ({}),
},
match: { path: '/7', params: { id: '7' } },
query: null,
hash: undefined,
},
];
const titles = await resolveRouteTitles(chain, injector);
expect(titles).toEqual(['Media', 'Movies', 'Movie 7']);
});
it('should return an empty array for an empty chain', async () => {
const titles = await resolveRouteTitles([], injector);
expect(titles).toEqual([]);
});
it('should include undefined for entries without titles', async () => {
const chain = [
{
route: { meta: { title: 'Root' }, component: () => ({}) },
match: { path: '/', params: {} },
query: null,
hash: undefined,
},
{
route: { component: () => ({}) },
match: { path: '/child', params: {} },
query: null,
hash: undefined,
},
];
const titles = await resolveRouteTitles(chain, injector);
expect(titles).toEqual(['Root', undefined]);
});
});
describe('buildDocumentTitle', () => {
it('should join titles with default separator', () => {
expect(buildDocumentTitle(['Media', 'Movies'])).toBe('Media - Movies');
});
it('should filter out undefined entries', () => {
expect(buildDocumentTitle(['Media', undefined, 'Movies'])).toBe('Media - Movies');
});
it('should use a custom separator', () => {
expect(buildDocumentTitle(['Media', 'Movies', 'Superman'], { separator: ' / ' })).toBe('Media / Movies / Superman');
});
it('should prepend a prefix', () => {
expect(buildDocumentTitle(['Media', 'Movies'], { prefix: 'My App' })).toBe('My App - Media - Movies');
});
it('should combine prefix and custom separator', () => {
expect(buildDocumentTitle(['Media', 'Movies', 'Superman'], { prefix: 'My App', separator: ' / ' })).toBe('My App / Media / Movies / Superman');
});
it('should return empty string for empty titles', () => {
expect(buildDocumentTitle([])).toBe('');
});
it('should return only prefix when all titles are undefined', () => {
expect(buildDocumentTitle([undefined, undefined], { prefix: 'My App' })).toBe('My App');
});
it('should return prefix alone when titles is empty', () => {
expect(buildDocumentTitle([], { prefix: 'My App' })).toBe('My App');
});
});
describe('extractNavTree', () => {
it('should extract a flat route tree', () => {
const routes = {
'/about': { meta: { title: 'About' }, component: () => ({}) },
'/contact': { meta: { title: 'Contact' }, component: () => ({}) },
};
const tree = extractNavTree(routes);
expect(tree).toEqual([
{ pattern: '/about', fullPath: '/about', meta: { title: 'About' }, children: undefined },
{ pattern: '/contact', fullPath: '/contact', meta: { title: 'Contact' }, children: undefined },
]);
});
it('should extract nested routes recursively', () => {
const routes = {
'/media': {
meta: { title: 'Media' },
component: () => ({}),
children: {
'/movies': { meta: { title: 'Movies' }, component: () => ({}) },
'/music': { meta: { title: 'Music' }, component: () => ({}) },
},
},
};
const tree = extractNavTree(routes);
expect(tree).toHaveLength(1);
expect(tree[0].pattern).toBe('/media');
expect(tree[0].fullPath).toBe('/media');
expect(tree[0].children).toHaveLength(2);
expect(tree[0].children[0]).toEqual({
pattern: '/movies',
fullPath: '/media/movies',
meta: { title: 'Movies' },
children: undefined,
});
expect(tree[0].children[1]).toEqual({
pattern: '/music',
fullPath: '/media/music',
meta: { title: 'Music' },
children: undefined,
});
});
it('should handle root "/" parent path correctly', () => {
const routes = {
'/': {
meta: { title: 'Home' },
component: () => ({}),
children: {
'/settings': { meta: { title: 'Settings' }, component: () => ({}) },
},
},
};
const tree = extractNavTree(routes);
expect(tree[0].fullPath).toBe('/');
expect(tree[0].children[0].fullPath).toBe('/settings');
});
it('should compute correct fullPath for deeply nested routes (3+ levels)', () => {
const routes = {
'/a': {
meta: { title: 'A' },
component: () => ({}),
children: {
'/b': {
meta: { title: 'B' },
component: () => ({}),
children: {
'/c': { meta: { title: 'C' }, component: () => ({}) },
},
},
},
},
};
const tree = extractNavTree(routes);
expect(tree[0].fullPath).toBe('/a');
expect(tree[0].children[0].fullPath).toBe('/a/b');
expect(tree[0].children[0].children[0].fullPath).toBe('/a/b/c');
});
it('should include routes without meta', () => {
const routes = {
'/hidden': { component: () => ({}) },
};
const tree = extractNavTree(routes);
expect(tree[0].meta).toBeUndefined();
});
describe('typed output', () => {
it('should narrow pattern and fullPath for a flat typed tree', () => {
const routes = {
'/about': { component: () => ({}) },
'/contact': { component: () => ({}) },
};
const tree = extractNavTree(routes);
expectTypeOf(tree[0].pattern).toEqualTypeOf();
expectTypeOf(tree[0].fullPath).toEqualTypeOf();
});
it('should include nested composed paths in the fullPath union', () => {
const routes = {
'/media': {
component: () => ({}),
children: {
'/movies': { component: () => ({}) },
'/music': { component: () => ({}) },
},
},
};
const tree = extractNavTree(routes);
expectTypeOf(tree[0].fullPath).toEqualTypeOf();
expectTypeOf(tree[0].children).toEqualTypeOf();
});
it('should preserve backward compatibility with the widened default', () => {
const node = {
pattern: '/anything',
fullPath: '/anything',
};
expectTypeOf(node.pattern).toEqualTypeOf();
expectTypeOf(node.fullPath).toEqualTypeOf();
});
});
});
//# sourceMappingURL=route-meta-utils.spec.js.map