@furystack/shades
Version:
A lightweight UI framework for FuryStack with JSX support
211 lines • 9.11 kB
JavaScript
import { createInjector } from '@furystack/inject';
import { usingAsync } from '@furystack/utils';
import { describe, expect, expectTypeOf, it } from 'vitest';
import { LocationService } from '../services/location-service.js';
import { createNestedHooks, walkRoute } from './nested-hooks.js';
const makeComponent = () => null;
describe('walkRoute', () => {
it('Should return the route when the pattern matches the path exactly', () => {
const leaf = { component: () => makeComponent() };
const routes = { '/users': leaf };
expect(walkRoute(routes, '/users')).toBe(leaf);
});
it('Should return undefined when no pattern matches', () => {
const routes = { '/users': { component: () => makeComponent() } };
expect(walkRoute(routes, '/nonexistent')).toBeUndefined();
});
it('Should descend through a `/` root pattern transparently', () => {
const leaf = { component: () => makeComponent() };
const routes = {
'/': {
component: () => makeComponent(),
children: {
'/home': leaf,
},
},
};
expect(walkRoute(routes, '/home')).toBe(leaf);
});
it('Should resolve a deeply nested path by walking children', () => {
const leaf = { component: () => makeComponent() };
const routes = {
'/navigation': {
component: () => makeComponent(),
children: {
'/tabs': leaf,
},
},
};
expect(walkRoute(routes, '/navigation/tabs')).toBe(leaf);
});
it('Should resolve through two levels of `/` roots', () => {
const leaf = { component: () => makeComponent() };
const routes = {
'/': {
component: () => makeComponent(),
children: {
'/navigation': {
component: () => makeComponent(),
children: {
'/tabs': leaf,
},
},
},
},
};
expect(walkRoute(routes, '/navigation/tabs')).toBe(leaf);
});
it('Should match a parent prefix when the path is exactly the parent pattern', () => {
const parent = {
component: () => makeComponent(),
children: {
'/child': { component: () => makeComponent() },
},
};
const routes = { '/parent': parent };
expect(walkRoute(routes, '/parent')).toBe(parent);
});
it('Should not confuse a prefix pattern that is not followed by a `/`', () => {
const routes = {
'/users': {
component: () => makeComponent(),
children: {
'/:id': { component: () => makeComponent() },
},
},
};
expect(walkRoute(routes, '/usersextra')).toBeUndefined();
});
});
describe('createNestedHooks', () => {
describe('getTypedQuery', () => {
it('Should return null when the route has no declared query validator', async () => {
const routes = {
'/bare': { component: () => makeComponent() },
};
const { getTypedQuery } = createNestedHooks(routes);
await usingAsync(createInjector(), async (injector) => {
expect(getTypedQuery(injector, '/bare')).toBeNull();
});
});
it('Should return the validated query when the current search matches', async () => {
const routes = {
'/list': {
component: () => makeComponent(),
query: (raw) => (typeof raw.page === 'number' ? { page: raw.page } : null),
},
};
const { getTypedQuery } = createNestedHooks(routes);
await usingAsync(createInjector(), async (injector) => {
const locationService = injector.get(LocationService);
locationService.onDeserializedLocationSearchChanged.setValue({ page: 3 });
expect(getTypedQuery(injector, '/list')).toEqual({ page: 3 });
});
});
it('Should return null when the validator rejects the current search', async () => {
const routes = {
'/list': {
component: () => makeComponent(),
query: (raw) => (typeof raw.page === 'number' ? { page: raw.page } : null),
},
};
const { getTypedQuery } = createNestedHooks(routes);
await usingAsync(createInjector(), async (injector) => {
const locationService = injector.get(LocationService);
locationService.onDeserializedLocationSearchChanged.setValue({ page: 'not-a-number' });
expect(getTypedQuery(injector, '/list')).toBeNull();
});
});
});
describe('getTypedHash', () => {
it('Should return undefined when the route has no declared hash tuple', async () => {
const routes = {
'/bare': { component: () => makeComponent() },
};
const { getTypedHash } = createNestedHooks(routes);
await usingAsync(createInjector(), async (injector) => {
expect(getTypedHash(injector, '/bare')).toBeUndefined();
});
});
it('Should return the current hash when it matches a declared literal', async () => {
const routes = {
'/tabs': {
component: () => makeComponent(),
hash: ['overview', 'details'],
},
};
const { getTypedHash } = createNestedHooks(routes);
await usingAsync(createInjector(), async (injector) => {
const locationService = injector.get(LocationService);
locationService.onLocationHashChanged.setValue('details');
expect(getTypedHash(injector, '/tabs')).toBe('details');
});
});
it('Should return undefined when the current hash is not in the declared tuple', async () => {
const routes = {
'/tabs': {
component: () => makeComponent(),
hash: ['overview', 'details'],
},
};
const { getTypedHash } = createNestedHooks(routes);
await usingAsync(createInjector(), async (injector) => {
const locationService = injector.get(LocationService);
locationService.onLocationHashChanged.setValue('something-else');
expect(getTypedHash(injector, '/tabs')).toBeUndefined();
});
});
it('Should resolve hashes through nested routes', async () => {
const routes = {
'/navigation': {
component: () => makeComponent(),
children: {
'/tabs': {
component: () => makeComponent(),
hash: ['ctrl-1', 'ctrl-2'],
},
},
},
};
const { getTypedHash } = createNestedHooks(routes);
await usingAsync(createInjector(), async (injector) => {
const locationService = injector.get(LocationService);
locationService.onLocationHashChanged.setValue('ctrl-1');
expect(getTypedHash(injector, '/navigation/tabs')).toBe('ctrl-1');
});
});
});
describe('Type utilities', () => {
it('Should narrow getTypedHash against the declared literal tuple', () => {
const routes = {
'/tabs': {
component: () => makeComponent(),
hash: ['overview', 'details'],
},
};
const { getTypedHash } = createNestedHooks(routes);
expectTypeOf((getTypedHash)).returns.toEqualTypeOf();
});
it('Should narrow getTypedQuery against the declared validator return type', () => {
const routes = {
'/list': {
component: () => makeComponent(),
query: (raw) => (typeof raw.page === 'number' ? { page: raw.page } : null),
},
};
const { getTypedQuery } = createNestedHooks(routes);
expectTypeOf((getTypedQuery)).returns.toEqualTypeOf();
});
it('Should reject paths not present in the route tree', () => {
const routes = {
'/tabs': {
component: () => makeComponent(),
},
};
const { getTypedHash } = createNestedHooks(routes);
// @ts-expect-error -- '/nonexistent' is not a valid route path
getTypedHash(createInjector(), '/nonexistent');
});
});
});
//# sourceMappingURL=nested-hooks.spec.js.map