import { act, waitFor } from '@testing-library/react' import { renderHookWithNuqs } from '@/test/nuqs-testing' import useAppsQueryState from '../use-apps-query-state' const renderWithAdapter = (searchParams = '') => { return renderHookWithNuqs(() => useAppsQueryState(), { searchParams }) } describe('useAppsQueryState', () => { beforeEach(() => { vi.clearAllMocks() }) describe('Initialization', () => { it('should expose query and setQuery when initialized', () => { const { result } = renderWithAdapter() expect(result.current.query).toBeDefined() expect(typeof result.current.setQuery).toBe('function') }) it('should default to empty filters when search params are missing', () => { const { result } = renderWithAdapter() expect(result.current.query.tagIDs).toBeUndefined() expect(result.current.query.keywords).toBeUndefined() expect(result.current.query.isCreatedByMe).toBe(false) }) }) describe('Parsing search params', () => { it('should parse tagIDs when URL includes tagIDs', () => { const { result } = renderWithAdapter('?tagIDs=tag1;tag2;tag3') expect(result.current.query.tagIDs).toEqual(['tag1', 'tag2', 'tag3']) }) it('should parse keywords when URL includes keywords', () => { const { result } = renderWithAdapter('?keywords=search+term') expect(result.current.query.keywords).toBe('search term') }) it('should parse isCreatedByMe when URL includes true value', () => { const { result } = renderWithAdapter('?isCreatedByMe=true') expect(result.current.query.isCreatedByMe).toBe(true) }) it('should parse all params when URL includes multiple filters', () => { const { result } = renderWithAdapter( '?tagIDs=tag1;tag2&keywords=test&isCreatedByMe=true', ) expect(result.current.query.tagIDs).toEqual(['tag1', 'tag2']) expect(result.current.query.keywords).toBe('test') expect(result.current.query.isCreatedByMe).toBe(true) }) }) describe('Updating query state', () => { it('should update keywords when setQuery receives keywords', () => { const { result } = renderWithAdapter() act(() => { result.current.setQuery({ keywords: 'new search' }) }) expect(result.current.query.keywords).toBe('new search') }) it('should update tagIDs when setQuery receives tagIDs', () => { const { result } = renderWithAdapter() act(() => { result.current.setQuery({ tagIDs: ['tag1', 'tag2'] }) }) expect(result.current.query.tagIDs).toEqual(['tag1', 'tag2']) }) it('should update isCreatedByMe when setQuery receives true', () => { const { result } = renderWithAdapter() act(() => { result.current.setQuery({ isCreatedByMe: true }) }) expect(result.current.query.isCreatedByMe).toBe(true) }) it('should support partial updates when setQuery uses callback', () => { const { result } = renderWithAdapter() act(() => { result.current.setQuery({ keywords: 'initial' }) }) act(() => { result.current.setQuery(prev => ({ ...prev, isCreatedByMe: true })) }) expect(result.current.query.keywords).toBe('initial') expect(result.current.query.isCreatedByMe).toBe(true) }) }) describe('URL synchronization', () => { it('should sync keywords to URL when keywords change', async () => { const { result, onUrlUpdate } = renderWithAdapter() act(() => { result.current.setQuery({ keywords: 'search' }) }) await waitFor(() => expect(onUrlUpdate).toHaveBeenCalled()) const update = onUrlUpdate.mock.calls[onUrlUpdate.mock.calls.length - 1][0] expect(update.searchParams.get('keywords')).toBe('search') expect(update.options.history).toBe('push') }) it('should sync tagIDs to URL when tagIDs change', async () => { const { result, onUrlUpdate } = renderWithAdapter() act(() => { result.current.setQuery({ tagIDs: ['tag1', 'tag2'] }) }) await waitFor(() => expect(onUrlUpdate).toHaveBeenCalled()) const update = onUrlUpdate.mock.calls[onUrlUpdate.mock.calls.length - 1][0] expect(update.searchParams.get('tagIDs')).toBe('tag1;tag2') }) it('should sync isCreatedByMe to URL when enabled', async () => { const { result, onUrlUpdate } = renderWithAdapter() act(() => { result.current.setQuery({ isCreatedByMe: true }) }) await waitFor(() => expect(onUrlUpdate).toHaveBeenCalled()) const update = onUrlUpdate.mock.calls[onUrlUpdate.mock.calls.length - 1][0] expect(update.searchParams.get('isCreatedByMe')).toBe('true') }) it('should remove keywords from URL when keywords are cleared', async () => { const { result, onUrlUpdate } = renderWithAdapter('?keywords=existing') act(() => { result.current.setQuery({ keywords: '' }) }) await waitFor(() => expect(onUrlUpdate).toHaveBeenCalled()) const update = onUrlUpdate.mock.calls[onUrlUpdate.mock.calls.length - 1][0] expect(update.searchParams.has('keywords')).toBe(false) }) it('should remove tagIDs from URL when tagIDs are empty', async () => { const { result, onUrlUpdate } = renderWithAdapter('?tagIDs=tag1;tag2') act(() => { result.current.setQuery({ tagIDs: [] }) }) await waitFor(() => expect(onUrlUpdate).toHaveBeenCalled()) const update = onUrlUpdate.mock.calls[onUrlUpdate.mock.calls.length - 1][0] expect(update.searchParams.has('tagIDs')).toBe(false) }) it('should remove isCreatedByMe from URL when disabled', async () => { const { result, onUrlUpdate } = renderWithAdapter('?isCreatedByMe=true') act(() => { result.current.setQuery({ isCreatedByMe: false }) }) await waitFor(() => expect(onUrlUpdate).toHaveBeenCalled()) const update = onUrlUpdate.mock.calls[onUrlUpdate.mock.calls.length - 1][0] expect(update.searchParams.has('isCreatedByMe')).toBe(false) }) }) describe('Edge cases', () => { it('should treat empty tagIDs as empty list when URL param is empty', () => { const { result } = renderWithAdapter('?tagIDs=') expect(result.current.query.tagIDs).toEqual([]) }) it('should treat empty keywords as undefined when URL param is empty', () => { const { result } = renderWithAdapter('?keywords=') expect(result.current.query.keywords).toBeUndefined() }) it('should decode keywords with spaces when URL contains encoded spaces', () => { const { result } = renderWithAdapter('?keywords=test+with+spaces') expect(result.current.query.keywords).toBe('test with spaces') }) }) describe('Integration scenarios', () => { it('should keep accumulated filters when updates are sequential', () => { const { result } = renderWithAdapter() act(() => { result.current.setQuery({ keywords: 'first' }) }) act(() => { result.current.setQuery(prev => ({ ...prev, tagIDs: ['tag1'] })) }) act(() => { result.current.setQuery(prev => ({ ...prev, isCreatedByMe: true })) }) expect(result.current.query.keywords).toBe('first') expect(result.current.query.tagIDs).toEqual(['tag1']) expect(result.current.query.isCreatedByMe).toBe(true) }) }) })