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() }) })