@furystack/shades
Version:
A lightweight UI framework for FuryStack with JSX support
267 lines • 11.3 kB
JavaScript
import { createInjector } from '@furystack/inject';
import { usingAsync } from '@furystack/utils';
import { afterEach, beforeEach, describe, expect, expectTypeOf, it, vi } from 'vitest';
import { initializeShadeRoot } from '../initialize.js';
import { LocationService } from '../services/location-service.js';
import { createComponent } from '../shade-component.js';
import { flushUpdates } from '../shade.js';
import { NestedRouteLink, createNestedRouteLink } from './nested-route-link.js';
describe('NestedRouteLink', () => {
beforeEach(() => {
document.body.innerHTML = '<div id="root"></div>';
});
afterEach(() => {
document.body.innerHTML = '';
});
it('Should render a link with the correct href', async () => {
await usingAsync(createInjector(), async (injector) => {
const rootElement = document.getElementById('root');
initializeShadeRoot({
injector,
rootElement,
jsxElement: (createComponent(NestedRouteLink, { id: "link", path: "/buttons" }, "Buttons")),
});
await flushUpdates();
expect(document.body.innerHTML).toBe('<div id="root"><a is="nested-route-link" id="link" href="/buttons">Buttons</a></div>');
});
});
it('Should trigger SPA navigation on click', async () => {
await usingAsync(createInjector(), async (injector) => {
const rootElement = document.getElementById('root');
const onRouteChange = vi.fn();
injector.get(LocationService).onLocationPathChanged.subscribe(onRouteChange);
initializeShadeRoot({
injector,
rootElement,
jsxElement: (createComponent(NestedRouteLink, { id: "link", path: "/buttons" }, "Buttons")),
});
await flushUpdates();
expect(onRouteChange).not.toBeCalled();
document.getElementById('link')?.click();
expect(onRouteChange).toBeCalledTimes(1);
});
});
it('Should compile route params in the href', async () => {
await usingAsync(createInjector(), async (injector) => {
const rootElement = document.getElementById('root');
initializeShadeRoot({
injector,
rootElement,
jsxElement: (createComponent(NestedRouteLink, { id: "link", path: "/users/:id", params: { id: '42' } }, "User 42")),
});
await flushUpdates();
expect(document.body.innerHTML).toBe('<div id="root"><a is="nested-route-link" id="link" href="/users/42">User 42</a></div>');
});
});
it('Should compile route params with multiple segments', async () => {
await usingAsync(createInjector(), async (injector) => {
const rootElement = document.getElementById('root');
initializeShadeRoot({
injector,
rootElement,
jsxElement: (createComponent(NestedRouteLink, { id: "link", path: "/users/:userId/posts/:postId", params: { userId: '1', postId: '99' } }, "Post")),
});
await flushUpdates();
expect(document.body.innerHTML).toBe('<div id="root"><a is="nested-route-link" id="link" href="/users/1/posts/99">Post</a></div>');
});
});
it('Should append a serialized query string to the rendered href', async () => {
await usingAsync(createInjector(), async (injector) => {
const rootElement = document.getElementById('root');
initializeShadeRoot({
injector,
rootElement,
jsxElement: (createComponent(NestedRouteLink, { id: "link", path: "/buttons", query: { page: 2 } }, "Buttons")),
});
await flushUpdates();
const link = document.getElementById('link');
expect(link.getAttribute('href')?.startsWith('/buttons?')).toBe(true);
});
});
it('Should append the hash segment to the rendered href', async () => {
await usingAsync(createInjector(), async (injector) => {
const rootElement = document.getElementById('root');
initializeShadeRoot({
injector,
rootElement,
jsxElement: (createComponent(NestedRouteLink, { id: "link", path: "/buttons", hash: "overview" }, "Buttons")),
});
await flushUpdates();
const link = document.getElementById('link');
expect(link.getAttribute('href')).toBe('/buttons#overview');
});
});
});
describe('Type utilities', () => {
describe('ConcatPaths', () => {
it('Should strip root "/" when concatenating', () => {
expectTypeOf().toEqualTypeOf();
});
it('Should concatenate non-root parent paths', () => {
expectTypeOf().toEqualTypeOf();
});
it('Should handle deeply nested paths', () => {
expectTypeOf().toEqualTypeOf();
});
});
describe('ExtractRouteParams', () => {
it('Should return Record<string, never> for paths without params', () => {
expectTypeOf().toEqualTypeOf();
});
it('Should extract a single param', () => {
expectTypeOf().toEqualTypeOf();
});
it('Should extract multiple params', () => {
expectTypeOf().toEqualTypeOf();
});
it('Should handle params at the beginning of the path', () => {
expectTypeOf().toEqualTypeOf();
});
});
describe('ExtractRoutePaths', () => {
it('Should extract top-level paths', () => {
expectTypeOf().toEqualTypeOf();
});
it('Should extract nested child paths with root parent', () => {
expectTypeOf().toEqualTypeOf();
});
it('Should extract nested child paths with non-root parent', () => {
expectTypeOf().toEqualTypeOf();
});
it('Should handle mixed flat and nested routes', () => {
expectTypeOf().toEqualTypeOf();
});
});
describe('UrlTree', () => {
it('Should accept a flat object of valid paths', () => {
const urls = {
a: '/a',
b: '/b',
};
expectTypeOf(urls).toExtend();
});
it('Should accept nested objects of valid paths', () => {
const urls = {
home: '/',
buttons: '/buttons',
layoutTests: {
index: '/layout-tests',
appBarOnly: '/layout-tests/appbar-only',
},
};
expectTypeOf(urls).toExtend();
});
});
describe('TypedNestedRouteLinkProps', () => {
it('Should make params optional for paths without parameters', () => {
expectTypeOf().toEqualTypeOf();
expectTypeOf().toExtend();
});
it('Should require params for parameterized paths', () => {
expectTypeOf().toEqualTypeOf();
expectTypeOf().toExtend();
});
it('Should require all params for multi-param paths', () => {
expectTypeOf().toExtend();
});
});
describe('NestedRouteLink param inference', () => {
it('Should infer params as optional when path has no parameters', () => {
expectTypeOf(NestedRouteLink).parameter(0).toHaveProperty('params');
expectTypeOf((NestedRouteLink))
.parameter(0)
.toExtend();
});
it('Should infer params as required when path has a parameter', () => {
expectTypeOf((NestedRouteLink))
.parameter(0)
.toExtend();
});
it('Should infer multiple params from path', () => {
expectTypeOf((NestedRouteLink))
.parameter(0)
.toExtend();
});
});
describe('createNestedRouteLink', () => {
it('Should constrain path to valid route paths', () => {
const AppLink = createNestedRouteLink();
expectTypeOf(AppLink).parameter(0).toHaveProperty('path');
});
it('Should reject invalid paths', () => {
const AppLink = createNestedRouteLink();
// @ts-expect-error -- '/nonexistent' is not a valid route path
AppLink({ path: '/nonexistent' });
});
it('Should require params for parameterized routes in the tree', () => {
const AppLink = createNestedRouteLink();
expectTypeOf((AppLink))
.parameter(0)
.toExtend();
});
it('Should require combined params from parent and child route segments', () => {
const AppLink = createNestedRouteLink();
expectTypeOf((AppLink))
.parameter(0)
.toExtend();
});
it('Should accept routes with typed match parameters (NestedRoute<T>)', () => {
const routes = {
'/stacks/:stackName': {
component: ({ match }) => createComponent("div", null, match.params.stackName),
},
};
const AppLink = createNestedRouteLink();
expectTypeOf((AppLink))
.parameter(0)
.toExtend();
});
it('Should accept a mixed route tree with typed and untyped match parameters', () => {
const usersRoute = {
component: ({ match }) => createComponent("div", null, match.params.userId),
};
const buttonsRoute = {
component: () => createComponent("div", null),
};
const routes = {
'/': {
component: ({ outlet }) => outlet ?? createComponent("div", null),
children: {
'/buttons': buttonsRoute,
'/users/:userId': usersRoute,
},
},
};
const AppLink = createNestedRouteLink();
expectTypeOf(AppLink).parameter(0).toHaveProperty('path');
expectTypeOf((AppLink))
.parameter(0)
.toExtend();
});
it('Should enforce a declared required query shape on the link', () => {
const routes = {
'/list': {
component: () => null,
query: (raw) => (typeof raw.page === 'number' ? { page: raw.page } : null),
},
};
const AppLink = createNestedRouteLink();
expectTypeOf((AppLink))
.parameter(0)
.toExtend();
});
it('Should narrow hash on the link to the declared literal tuple', () => {
const routes = {
'/tabs': {
component: () => null,
hash: ['overview', 'details'],
},
};
const AppLink = createNestedRouteLink();
expectTypeOf((AppLink))
.parameter(0)
.toExtend();
});
});
});
//# sourceMappingURL=nested-route-link.spec.js.map