UNPKG

@furystack/shades

Version:

A lightweight UI framework for FuryStack with JSX support

267 lines 11.3 kB
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