import { describe, expect, it, vi } from 'vitest'
import { createApp, nextTick } from 'vue'
import { createMemoryHistory, createRouter, type RouteRecordRaw } from 'vue-router'
import { useRouteFilters, type UseRouteFiltersReturn, type UseRouteFiltersSchema } from './useRouteFilters'
const routes: RouteRecordRaw[] = [{ path: '/', component: { template: '
' } }]
const flushPromises = () => new Promise((resolve) => setTimeout(resolve, 0))
type TestSchema = UseRouteFiltersSchema<
{ status: string[]; active: boolean; count: number },
{ status: string[]; active: boolean }
>
const mountWithRoute = async (initialQuery: Record = {}, schema?: Partial) => {
const router = createRouter({ history: createMemoryHistory(), routes })
await router.push({ path: '/', query: initialQuery })
await router.isReady()
const replaceSpy = vi.spyOn(router, 'replace')
let composable!: UseRouteFiltersReturn<
{ status: string[]; active: boolean; count: number },
{ status: string[]; active: boolean }
>
const resolvedSchema: TestSchema = {
filter: { status: [] as string[], active: false as boolean, count: 0 },
draft: { status: [] as string[], active: false as boolean },
sort: {},
search: '',
page: 0,
...schema,
}
const app = createApp({
setup() {
composable = useRouteFilters(resolvedSchema)
return () => null
},
})
app.use(router)
app.mount(document.createElement('div'))
await nextTick()
return { composable, router, replaceSpy, app }
}
describe('useRouteFilters', () => {
it('initializes filter from route query with typed defaults', async () => {
const { composable, app } = await mountWithRoute({
status: ['open', 'closed'],
active: 'true',
count: '3',
})
expect(composable.filter.status).toEqual(['open', 'closed'])
expect(composable.filter.active).toBe(true)
expect(composable.filter.count).toBe(3)
app.unmount()
})
it('syncs filter to route and back without looping', async () => {
const { composable, router, replaceSpy, app } = await mountWithRoute({ active: 'true' })
replaceSpy.mockClear()
composable.filter.active = true
await nextTick()
await flushPromises()
expect(replaceSpy).not.toHaveBeenCalled()
composable.filter.status = ['open']
await nextTick()
await flushPromises()
expect(router.currentRoute.value.query).toMatchObject({
status: ['open'],
active: 'true',
})
await router.replace({ query: { status: ['new'], active: 'false' } })
await nextTick()
expect(composable.filter.status).toEqual(['new'])
expect(composable.filter.active).toBe(false)
app.unmount()
})
it('reset keeps unrelated query params and clear removes managed ones', async () => {
const { composable, router, app } = await mountWithRoute({
tab: 'details',
active: 'true',
_search: 'foo',
_page: '5',
})
await composable.reset()
await flushPromises()
expect(composable.filter.active).toBe(false)
expect(composable.search.value).toBe('')
expect(composable.page.value).toBe(0)
expect(router.currentRoute.value.query).toEqual({ tab: 'details' })
composable.filter.active = true
composable.search.value = 'bar'
await nextTick()
await flushPromises()
await composable.clear()
await flushPromises()
expect(router.currentRoute.value.query).toEqual({})
app.unmount()
})
it('draft apply and cancel keep URL in sync only after apply', async () => {
const { composable, router, app } = await mountWithRoute({ status: ['active'] })
composable.draft.open()
composable.draft.state.status = ['changed']
await nextTick()
await flushPromises()
expect(router.currentRoute.value.query.status).toEqual(['active'])
expect(composable.filter.status).toEqual(['active'])
composable.draft.cancel()
expect(composable.draft.state.status).toEqual(['active'])
composable.draft.state.status = ['applied']
composable.draft.apply()
await nextTick()
await flushPromises()
expect(composable.filter.status).toEqual(['applied'])
expect(router.currentRoute.value.query.status).toEqual(['applied'])
app.unmount()
})
it('sort toggle cycles none → desc → asc → none', async () => {
const { composable, app } = await mountWithRoute()
composable.sort.toggle('name')
expect(composable.sort.value).toBe('-name')
composable.sort.toggle('name')
expect(composable.sort.value).toBe('+name')
composable.sort.toggle('name')
expect(composable.sort.value).toBe('')
composable.sort.toggle('created')
composable.sort.toggle('name')
expect(composable.sort.value).toBe('-name')
app.unmount()
})
})