diff --git a/packages/query-devtools/src/__tests__/Devtools.test.tsx b/packages/query-devtools/src/__tests__/Devtools.test.tsx index 924be5aad5..f1254d80c3 100644 --- a/packages/query-devtools/src/__tests__/Devtools.test.tsx +++ b/packages/query-devtools/src/__tests__/Devtools.test.tsx @@ -32,6 +32,7 @@ describe('Devtools', () => { let previousRootFontSize = '' beforeEach(() => { + vi.useFakeTimers() previousRootFontSize = document.documentElement.style.fontSize // jsdom doesn't implement `PointerEvent`; the DropdownMenu trigger checks // `e.pointerType !== 'touch'` on pointerdown to decide whether to open, @@ -101,6 +102,7 @@ describe('Devtools', () => { }) afterEach(() => { + vi.useRealTimers() vi.unstubAllGlobals() Object.keys(storage).forEach((key) => delete storage[key]) queryClient.clear() @@ -244,4 +246,227 @@ describe('Devtools', () => { expect(localStorage.getItem('TanstackQueryDevtools.open')).toBe('false') }) }) + + describe('query list', () => { + it('should render a row for each query in the cache', () => { + queryClient.setQueryData(['posts'], [{ id: 1 }]) + queryClient.setQueryData(['users', 'me'], { id: 'u1' }) + const rendered = renderDevtools({ initialIsOpen: true }) + + expect( + rendered.getByLabelText(/Query key \["posts"\]/), + ).toBeInTheDocument() + expect( + rendered.getByLabelText(/Query key \["users","me"\]/), + ).toBeInTheDocument() + }) + + it('should reflect a newly added query reactively', () => { + const rendered = renderDevtools({ initialIsOpen: true }) + + expect( + rendered.queryByLabelText(/Query key \["new"\]/), + ).not.toBeInTheDocument() + + queryClient.setQueryData(['new'], 'hello') + + expect(rendered.getByLabelText(/Query key \["new"\]/)).toBeInTheDocument() + }) + + it('should filter queries by "queryHash"', () => { + queryClient.setQueryData(['posts'], []) + queryClient.setQueryData(['users'], []) + const rendered = renderDevtools({ initialIsOpen: true }) + + fireEvent.input(rendered.getByLabelText('Filter queries by query key'), { + target: { value: 'posts' }, + }) + + expect( + rendered.getByLabelText(/Query key \["posts"\]/), + ).toBeInTheDocument() + expect( + rendered.queryByLabelText(/Query key \["users"\]/), + ).not.toBeInTheDocument() + }) + + it('should clear all queries when the clear cache button is clicked', () => { + queryClient.setQueryData(['posts'], []) + queryClient.setQueryData(['users'], []) + const rendered = renderDevtools({ initialIsOpen: true }) + + fireEvent.click(rendered.getByLabelText('Clear query cache')) + + expect( + rendered.queryByLabelText(/Query key \["posts"\]/), + ).not.toBeInTheDocument() + expect( + rendered.queryByLabelText(/Query key \["users"\]/), + ).not.toBeInTheDocument() + }) + + it('should dispatch a "CLEAR_MUTATION_CACHE" event when clear cache is clicked in mutations view', () => { + const rendered = renderDevtools({ initialIsOpen: true }) + fireEvent.click(rendered.getByText('Mutations')) + + const listener = vi.fn() + window.addEventListener('@tanstack/query-devtools-event', listener) + + try { + fireEvent.click(rendered.getByLabelText('Clear query cache')) + + const dispatched = listener.mock.calls.some( + ([e]) => (e as CustomEvent).detail.type === 'CLEAR_MUTATION_CACHE', + ) + expect(dispatched).toBe(true) + } finally { + window.removeEventListener('@tanstack/query-devtools-event', listener) + } + }) + }) + + describe('view toggle', () => { + it('should switch to mutations view when the mutations toggle is clicked', () => { + const rendered = renderDevtools({ initialIsOpen: true }) + + fireEvent.click(rendered.getByText('Mutations')) + + expect( + rendered.container.querySelector('.tsqd-mutations-container'), + ).not.toBeNull() + }) + + it('should render mutations in the mutations view', async () => { + const rendered = renderDevtools({ initialIsOpen: true }) + + fireEvent.click(rendered.getByText('Mutations')) + + const mutation = queryClient.getMutationCache().build(queryClient, { + mutationKey: ['add-post'], + mutationFn: () => Promise.resolve('ok'), + }) + mutation.execute({}) + await vi.advanceTimersByTimeAsync(0) + + expect( + rendered.getByLabelText(/Mutation submitted at/), + ).toBeInTheDocument() + }) + }) + + describe('disabled and static queries', () => { + it('should mark a disabled query in the row label', () => { + const observer = queryClient.getQueryCache().build(queryClient, { + queryKey: ['disabled-q'], + queryFn: () => 'x', + }) + observer.setOptions({ + ...observer.options, + enabled: false, + } as typeof observer.options) + observer.setState({ ...observer.state, data: 'x' }) + const rendered = renderDevtools({ initialIsOpen: true }) + + expect(rendered.getByLabelText(/disabled/)).toBeInTheDocument() + }) + }) + + describe('status counts', () => { + it('should render status count badges', () => { + const rendered = renderDevtools({ initialIsOpen: true }) + + expect(rendered.getByLabelText(/Fresh: \d+/)).toBeInTheDocument() + expect(rendered.getByLabelText(/Stale: \d+/)).toBeInTheDocument() + expect(rendered.getByLabelText(/Fetching: \d+/)).toBeInTheDocument() + expect(rendered.getByLabelText(/Paused: \d+/)).toBeInTheDocument() + expect(rendered.getByLabelText(/Inactive: \d+/)).toBeInTheDocument() + }) + + it('should reflect the inactive count when a query is added without observers', () => { + const rendered = renderDevtools({ initialIsOpen: true }) + + expect(rendered.getByLabelText('Inactive: 0')).toBeInTheDocument() + + queryClient.setQueryData(['posts'], [{ id: 1 }]) + + expect(rendered.getByLabelText('Inactive: 1')).toBeInTheDocument() + }) + }) + + describe('status tooltip', () => { + it('should show the tooltip on mouse enter and hide it on mouse leave when the panel is narrow', () => { + // Re-stub ResizeObserver with a narrow width (< secondBreakpoint = 796) + // so `showLabel()` is false and the tooltip is rendered conditionally on + // hover/focus. + vi.stubGlobal( + 'ResizeObserver', + class { + callback: ResizeObserverCallback + constructor(callback: ResizeObserverCallback) { + this.callback = callback + } + observe = vi.fn((target: Element) => { + this.callback( + [ + { + target, + contentRect: { width: 500, height: 500 } as DOMRectReadOnly, + } as ResizeObserverEntry, + ], + this as unknown as ResizeObserver, + ) + }) + unobserve = vi.fn() + disconnect = vi.fn() + }, + ) + + const rendered = renderDevtools({ initialIsOpen: true }) + const fresh = rendered.getByLabelText('Fresh: 0') + + expect(rendered.queryByRole('tooltip')).not.toBeInTheDocument() + + fireEvent.mouseEnter(fresh) + expect(rendered.getByRole('tooltip')).toBeInTheDocument() + + fireEvent.mouseLeave(fresh) + expect(rendered.queryByRole('tooltip')).not.toBeInTheDocument() + }) + + it('should show the tooltip on focus and hide it on blur when the panel is narrow', () => { + vi.stubGlobal( + 'ResizeObserver', + class { + callback: ResizeObserverCallback + constructor(callback: ResizeObserverCallback) { + this.callback = callback + } + observe = vi.fn((target: Element) => { + this.callback( + [ + { + target, + contentRect: { width: 500, height: 500 } as DOMRectReadOnly, + } as ResizeObserverEntry, + ], + this as unknown as ResizeObserver, + ) + }) + unobserve = vi.fn() + disconnect = vi.fn() + }, + ) + + const rendered = renderDevtools({ initialIsOpen: true }) + const fresh = rendered.getByLabelText('Fresh: 0') + + expect(rendered.queryByRole('tooltip')).not.toBeInTheDocument() + + fireEvent.focus(fresh) + expect(rendered.getByRole('tooltip')).toBeInTheDocument() + + fireEvent.blur(fresh) + expect(rendered.queryByRole('tooltip')).not.toBeInTheDocument() + }) + }) })