one
Version:
One is a new React Framework that makes Vite serve both native and web.
318 lines (271 loc) • 8.79 kB
text/typescript
import { describe, expect, it } from 'vitest'
import type { One } from './vite/types'
import { setClientMatches } from './useMatches'
describe('useMatches', () => {
describe('setClientMatches', () => {
it('should update client matches without errors', () => {
const matches: One.RouteMatch[] = [
{
routeId: '/_layout',
pathname: '/',
params: {},
loaderData: { title: 'Root' },
},
{
routeId: '/page',
pathname: '/page',
params: {},
loaderData: { content: 'Page content' },
},
]
// should not throw
expect(() => setClientMatches(matches)).not.toThrow()
})
it('should accept empty matches array', () => {
expect(() => setClientMatches([])).not.toThrow()
})
})
describe('type safety', () => {
it('should have correct RouteMatch type', () => {
const match: One.RouteMatch = {
routeId: 'test',
pathname: '/test',
params: { id: '123' },
loaderData: { foo: 'bar' },
}
expect(match.routeId).toBe('test')
expect(match.pathname).toBe('/test')
expect(match.params.id).toBe('123')
expect(match.loaderData).toEqual({ foo: 'bar' })
})
it('should allow string[] params', () => {
const match: One.RouteMatch = {
routeId: 'test',
pathname: '/test',
params: { slugs: ['a', 'b', 'c'] },
loaderData: null,
}
expect(match.params.slugs).toEqual(['a', 'b', 'c'])
})
it('should allow undefined loaderData', () => {
const match: One.RouteMatch = {
routeId: 'test',
pathname: '/test',
params: {},
loaderData: undefined,
}
expect(match.loaderData).toBeUndefined()
})
})
})
describe('RouteMatch ordering', () => {
it('matches should be ordered parent to child (root layout first)', () => {
const matches: One.RouteMatch[] = [
{ routeId: '/_layout', pathname: '/', params: {}, loaderData: { level: 'root' } },
{
routeId: '/docs/_layout',
pathname: '/docs',
params: {},
loaderData: { level: 'docs' },
},
{
routeId: '/docs/intro',
pathname: '/docs/intro',
params: {},
loaderData: { level: 'page' },
},
]
// root layout should be first
expect(matches[0].routeId).toBe('/_layout')
expect((matches[0].loaderData as any).level).toBe('root')
// docs layout should be second
expect(matches[1].routeId).toBe('/docs/_layout')
expect((matches[1].loaderData as any).level).toBe('docs')
// page should be last
expect(matches[2].routeId).toBe('/docs/intro')
expect((matches[2].loaderData as any).level).toBe('page')
})
it('last match should be the current page', () => {
const matches: One.RouteMatch[] = [
{ routeId: '/_layout', pathname: '/', params: {}, loaderData: {} },
{
routeId: '/page',
pathname: '/page',
params: { id: '123' },
loaderData: { title: 'Page' },
},
]
const pageMatch = matches[matches.length - 1]
expect(pageMatch.routeId).toBe('/page')
expect((pageMatch.loaderData as any).title).toBe('Page')
})
})
describe('setClientMatches reactivity', () => {
it('should notify listeners when matches change', () => {
let notifyCount = 0
// simulate a listener (like what useSyncExternalStore would use)
const listener = () => {
notifyCount++
}
// manually test the subscription mechanism
const matches1: One.RouteMatch[] = [
{ routeId: '/page1', pathname: '/page1', params: {}, loaderData: {} },
]
const matches2: One.RouteMatch[] = [
{ routeId: '/page2', pathname: '/page2', params: {}, loaderData: {} },
]
// first call
setClientMatches(matches1)
// no listener yet, so notifyCount is still 0
// second call
setClientMatches(matches2)
// still no listener
expect(notifyCount).toBe(0) // we didn't subscribe
})
it('should handle multiple sequential updates', () => {
const matches1: One.RouteMatch[] = [
{ routeId: '/a', pathname: '/a', params: {}, loaderData: { n: 1 } },
]
const matches2: One.RouteMatch[] = [
{ routeId: '/b', pathname: '/b', params: {}, loaderData: { n: 2 } },
]
const matches3: One.RouteMatch[] = [
{ routeId: '/c', pathname: '/c', params: {}, loaderData: { n: 3 } },
]
// should not throw on rapid updates
expect(() => {
setClientMatches(matches1)
setClientMatches(matches2)
setClientMatches(matches3)
}).not.toThrow()
})
})
describe('RouteMatch with dynamic params', () => {
it('should handle single dynamic param', () => {
const match: One.RouteMatch = {
routeId: '/users/[id]',
pathname: '/users/123',
params: { id: '123' },
loaderData: { user: { name: 'John' } },
}
expect(match.params.id).toBe('123')
})
it('should handle multiple dynamic params', () => {
const match: One.RouteMatch = {
routeId: '/users/[userId]/posts/[postId]',
pathname: '/users/123/posts/456',
params: { userId: '123', postId: '456' },
loaderData: { post: { title: 'Hello' } },
}
expect(match.params.userId).toBe('123')
expect(match.params.postId).toBe('456')
})
it('should handle catch-all params', () => {
const match: One.RouteMatch = {
routeId: '/docs/[...slug]',
pathname: '/docs/getting-started/intro',
params: { slug: ['getting-started', 'intro'] },
loaderData: { doc: { content: 'Hello' } },
}
expect(match.params.slug).toEqual(['getting-started', 'intro'])
})
})
describe('RouteMatch loaderData scenarios', () => {
it('should handle complex nested loaderData', () => {
const match: One.RouteMatch = {
routeId: '/dashboard',
pathname: '/dashboard',
params: {},
loaderData: {
user: {
id: 1,
profile: {
name: 'John',
settings: {
theme: 'dark',
notifications: true,
},
},
},
posts: [
{ id: 1, title: 'Post 1' },
{ id: 2, title: 'Post 2' },
],
},
}
const data = match.loaderData as any
expect(data.user.profile.settings.theme).toBe('dark')
expect(data.posts).toHaveLength(2)
})
it('should handle null loaderData', () => {
const match: One.RouteMatch = {
routeId: '/empty',
pathname: '/empty',
params: {},
loaderData: null,
}
expect(match.loaderData).toBeNull()
})
it('should handle array loaderData', () => {
const match: One.RouteMatch = {
routeId: '/list',
pathname: '/list',
params: {},
loaderData: [1, 2, 3, 4, 5],
}
expect(match.loaderData).toEqual([1, 2, 3, 4, 5])
})
it('should handle primitive loaderData', () => {
const match: One.RouteMatch = {
routeId: '/count',
pathname: '/count',
params: {},
loaderData: 42,
}
expect(match.loaderData).toBe(42)
})
})
describe('finding matches', () => {
const createMatches = (): One.RouteMatch[] => [
{ routeId: '/_layout', pathname: '/', params: {}, loaderData: { root: true } },
{
routeId: '/docs/_layout',
pathname: '/docs',
params: {},
loaderData: { navItems: ['intro', 'guide'] },
},
{
routeId: '/docs/[slug]',
pathname: '/docs/intro',
params: { slug: 'intro' },
loaderData: { title: 'Introduction', headings: ['h1', 'h2'] },
},
]
it('should find match by exact routeId', () => {
const matches = createMatches()
const found = matches.find((m) => m.routeId === '/docs/_layout')
expect(found).toBeDefined()
expect((found!.loaderData as any).navItems).toEqual(['intro', 'guide'])
})
it('should find match by routeId pattern', () => {
const matches = createMatches()
const found = matches.find((m) => m.routeId.includes('_layout'))
expect(found).toBeDefined()
expect(found!.routeId).toBe('/_layout')
})
it('should get page match (last in array)', () => {
const matches = createMatches()
const pageMatch = matches[matches.length - 1]
expect(pageMatch.routeId).toBe('/docs/[slug]')
expect((pageMatch.loaderData as any).title).toBe('Introduction')
})
it('should get layout match for a page', () => {
const matches = createMatches()
// find the layout that contains "docs"
const layoutMatch = matches.find(
(m) => m.routeId.includes('/docs/') && m.routeId.includes('_layout')
)
expect(layoutMatch).toBeDefined()
expect((layoutMatch!.loaderData as any).navItems).toBeDefined()
})
})