mirror of https://github.com/langgenius/dify.git
364 lines
11 KiB
TypeScript
364 lines
11 KiB
TypeScript
/**
|
|
* Test suite for useAppsQueryState hook
|
|
*
|
|
* This hook manages app filtering state through URL search parameters, enabling:
|
|
* - Bookmarkable filter states (users can share URLs with specific filters active)
|
|
* - Browser history integration (back/forward buttons work with filters)
|
|
* - Multiple filter types: tagIDs, keywords, isCreatedByMe
|
|
*
|
|
* The hook syncs local filter state with URL search parameters, making filter
|
|
* navigation persistent and shareable across sessions.
|
|
*/
|
|
import { act, renderHook } from '@testing-library/react'
|
|
|
|
// Import the hook after mocks are set up
|
|
import useAppsQueryState from './use-apps-query-state'
|
|
|
|
// Mock Next.js navigation hooks
|
|
const mockPush = vi.fn()
|
|
const mockPathname = '/apps'
|
|
let mockSearchParams = new URLSearchParams()
|
|
|
|
vi.mock('next/navigation', () => ({
|
|
usePathname: vi.fn(() => mockPathname),
|
|
useRouter: vi.fn(() => ({
|
|
push: mockPush,
|
|
})),
|
|
useSearchParams: vi.fn(() => mockSearchParams),
|
|
}))
|
|
|
|
describe('useAppsQueryState', () => {
|
|
beforeEach(() => {
|
|
vi.clearAllMocks()
|
|
mockSearchParams = new URLSearchParams()
|
|
})
|
|
|
|
describe('Basic functionality', () => {
|
|
it('should return query object and setQuery function', () => {
|
|
const { result } = renderHook(() => useAppsQueryState())
|
|
|
|
expect(result.current.query).toBeDefined()
|
|
expect(typeof result.current.setQuery).toBe('function')
|
|
})
|
|
|
|
it('should initialize with empty query when no search params exist', () => {
|
|
const { result } = renderHook(() => useAppsQueryState())
|
|
|
|
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 from URL', () => {
|
|
mockSearchParams.set('tagIDs', 'tag1;tag2;tag3')
|
|
|
|
const { result } = renderHook(() => useAppsQueryState())
|
|
|
|
expect(result.current.query.tagIDs).toEqual(['tag1', 'tag2', 'tag3'])
|
|
})
|
|
|
|
it('should parse single tagID from URL', () => {
|
|
mockSearchParams.set('tagIDs', 'single-tag')
|
|
|
|
const { result } = renderHook(() => useAppsQueryState())
|
|
|
|
expect(result.current.query.tagIDs).toEqual(['single-tag'])
|
|
})
|
|
|
|
it('should parse keywords from URL', () => {
|
|
mockSearchParams.set('keywords', 'search term')
|
|
|
|
const { result } = renderHook(() => useAppsQueryState())
|
|
|
|
expect(result.current.query.keywords).toBe('search term')
|
|
})
|
|
|
|
it('should parse isCreatedByMe as true from URL', () => {
|
|
mockSearchParams.set('isCreatedByMe', 'true')
|
|
|
|
const { result } = renderHook(() => useAppsQueryState())
|
|
|
|
expect(result.current.query.isCreatedByMe).toBe(true)
|
|
})
|
|
|
|
it('should parse isCreatedByMe as false for other values', () => {
|
|
mockSearchParams.set('isCreatedByMe', 'false')
|
|
|
|
const { result } = renderHook(() => useAppsQueryState())
|
|
|
|
expect(result.current.query.isCreatedByMe).toBe(false)
|
|
})
|
|
|
|
it('should parse all params together', () => {
|
|
mockSearchParams.set('tagIDs', 'tag1;tag2')
|
|
mockSearchParams.set('keywords', 'test')
|
|
mockSearchParams.set('isCreatedByMe', 'true')
|
|
|
|
const { result } = renderHook(() => useAppsQueryState())
|
|
|
|
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 via setQuery', () => {
|
|
const { result } = renderHook(() => useAppsQueryState())
|
|
|
|
act(() => {
|
|
result.current.setQuery({ keywords: 'new search' })
|
|
})
|
|
|
|
expect(result.current.query.keywords).toBe('new search')
|
|
})
|
|
|
|
it('should update tagIDs via setQuery', () => {
|
|
const { result } = renderHook(() => useAppsQueryState())
|
|
|
|
act(() => {
|
|
result.current.setQuery({ tagIDs: ['tag1', 'tag2'] })
|
|
})
|
|
|
|
expect(result.current.query.tagIDs).toEqual(['tag1', 'tag2'])
|
|
})
|
|
|
|
it('should update isCreatedByMe via setQuery', () => {
|
|
const { result } = renderHook(() => useAppsQueryState())
|
|
|
|
act(() => {
|
|
result.current.setQuery({ isCreatedByMe: true })
|
|
})
|
|
|
|
expect(result.current.query.isCreatedByMe).toBe(true)
|
|
})
|
|
|
|
it('should support partial updates via callback', () => {
|
|
const { result } = renderHook(() => useAppsQueryState())
|
|
|
|
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', async () => {
|
|
const { result } = renderHook(() => useAppsQueryState())
|
|
|
|
act(() => {
|
|
result.current.setQuery({ keywords: 'search' })
|
|
})
|
|
|
|
// Wait for useEffect to run
|
|
await act(async () => {
|
|
await new Promise(resolve => setTimeout(resolve, 0))
|
|
})
|
|
|
|
expect(mockPush).toHaveBeenCalledWith(
|
|
expect.stringContaining('keywords=search'),
|
|
{ scroll: false },
|
|
)
|
|
})
|
|
|
|
it('should sync tagIDs to URL with semicolon separator', async () => {
|
|
const { result } = renderHook(() => useAppsQueryState())
|
|
|
|
act(() => {
|
|
result.current.setQuery({ tagIDs: ['tag1', 'tag2'] })
|
|
})
|
|
|
|
await act(async () => {
|
|
await new Promise(resolve => setTimeout(resolve, 0))
|
|
})
|
|
|
|
expect(mockPush).toHaveBeenCalledWith(
|
|
expect.stringContaining('tagIDs=tag1%3Btag2'),
|
|
{ scroll: false },
|
|
)
|
|
})
|
|
|
|
it('should sync isCreatedByMe to URL', async () => {
|
|
const { result } = renderHook(() => useAppsQueryState())
|
|
|
|
act(() => {
|
|
result.current.setQuery({ isCreatedByMe: true })
|
|
})
|
|
|
|
await act(async () => {
|
|
await new Promise(resolve => setTimeout(resolve, 0))
|
|
})
|
|
|
|
expect(mockPush).toHaveBeenCalledWith(
|
|
expect.stringContaining('isCreatedByMe=true'),
|
|
{ scroll: false },
|
|
)
|
|
})
|
|
|
|
it('should remove keywords from URL when empty', async () => {
|
|
mockSearchParams.set('keywords', 'existing')
|
|
|
|
const { result } = renderHook(() => useAppsQueryState())
|
|
|
|
act(() => {
|
|
result.current.setQuery({ keywords: '' })
|
|
})
|
|
|
|
await act(async () => {
|
|
await new Promise(resolve => setTimeout(resolve, 0))
|
|
})
|
|
|
|
// Should be called without keywords param
|
|
expect(mockPush).toHaveBeenCalled()
|
|
})
|
|
|
|
it('should remove tagIDs from URL when empty array', async () => {
|
|
mockSearchParams.set('tagIDs', 'tag1;tag2')
|
|
|
|
const { result } = renderHook(() => useAppsQueryState())
|
|
|
|
act(() => {
|
|
result.current.setQuery({ tagIDs: [] })
|
|
})
|
|
|
|
await act(async () => {
|
|
await new Promise(resolve => setTimeout(resolve, 0))
|
|
})
|
|
|
|
expect(mockPush).toHaveBeenCalled()
|
|
})
|
|
|
|
it('should remove isCreatedByMe from URL when false', async () => {
|
|
mockSearchParams.set('isCreatedByMe', 'true')
|
|
|
|
const { result } = renderHook(() => useAppsQueryState())
|
|
|
|
act(() => {
|
|
result.current.setQuery({ isCreatedByMe: false })
|
|
})
|
|
|
|
await act(async () => {
|
|
await new Promise(resolve => setTimeout(resolve, 0))
|
|
})
|
|
|
|
expect(mockPush).toHaveBeenCalled()
|
|
})
|
|
})
|
|
|
|
describe('Edge cases', () => {
|
|
it('should handle empty tagIDs string in URL', () => {
|
|
// NOTE: This test documents current behavior where ''.split(';') returns ['']
|
|
// This could potentially cause filtering issues as it's treated as a tag with empty name
|
|
// rather than absence of tags. Consider updating parseParams if this is problematic.
|
|
mockSearchParams.set('tagIDs', '')
|
|
|
|
const { result } = renderHook(() => useAppsQueryState())
|
|
|
|
expect(result.current.query.tagIDs).toEqual([''])
|
|
})
|
|
|
|
it('should handle empty keywords', () => {
|
|
mockSearchParams.set('keywords', '')
|
|
|
|
const { result } = renderHook(() => useAppsQueryState())
|
|
|
|
expect(result.current.query.keywords).toBeUndefined()
|
|
})
|
|
|
|
it('should handle undefined tagIDs', () => {
|
|
const { result } = renderHook(() => useAppsQueryState())
|
|
|
|
act(() => {
|
|
result.current.setQuery({ tagIDs: undefined })
|
|
})
|
|
|
|
expect(result.current.query.tagIDs).toBeUndefined()
|
|
})
|
|
|
|
it('should handle special characters in keywords', () => {
|
|
// Use URLSearchParams constructor to properly simulate URL decoding behavior
|
|
// URLSearchParams.get() decodes URL-encoded characters
|
|
mockSearchParams = new URLSearchParams('keywords=test%20with%20spaces')
|
|
|
|
const { result } = renderHook(() => useAppsQueryState())
|
|
|
|
expect(result.current.query.keywords).toBe('test with spaces')
|
|
})
|
|
})
|
|
|
|
describe('Memoization', () => {
|
|
it('should return memoized object reference when query unchanged', () => {
|
|
const { result, rerender } = renderHook(() => useAppsQueryState())
|
|
|
|
const firstResult = result.current
|
|
rerender()
|
|
const secondResult = result.current
|
|
|
|
expect(firstResult.query).toBe(secondResult.query)
|
|
})
|
|
|
|
it('should return new object reference when query changes', () => {
|
|
const { result } = renderHook(() => useAppsQueryState())
|
|
|
|
const firstQuery = result.current.query
|
|
|
|
act(() => {
|
|
result.current.setQuery({ keywords: 'changed' })
|
|
})
|
|
|
|
expect(result.current.query).not.toBe(firstQuery)
|
|
})
|
|
})
|
|
|
|
describe('Integration scenarios', () => {
|
|
it('should handle sequential updates', async () => {
|
|
const { result } = renderHook(() => useAppsQueryState())
|
|
|
|
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)
|
|
})
|
|
|
|
it('should clear all filters', () => {
|
|
mockSearchParams.set('tagIDs', 'tag1;tag2')
|
|
mockSearchParams.set('keywords', 'search')
|
|
mockSearchParams.set('isCreatedByMe', 'true')
|
|
|
|
const { result } = renderHook(() => useAppsQueryState())
|
|
|
|
act(() => {
|
|
result.current.setQuery({
|
|
tagIDs: undefined,
|
|
keywords: undefined,
|
|
isCreatedByMe: false,
|
|
})
|
|
})
|
|
|
|
expect(result.current.query.tagIDs).toBeUndefined()
|
|
expect(result.current.query.keywords).toBeUndefined()
|
|
expect(result.current.query.isCreatedByMe).toBe(false)
|
|
})
|
|
})
|
|
})
|