Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
225 changes: 225 additions & 0 deletions packages/query-devtools/src/__tests__/Devtools.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -101,6 +102,7 @@ describe('Devtools', () => {
})

afterEach(() => {
vi.useRealTimers()
vi.unstubAllGlobals()
Object.keys(storage).forEach((key) => delete storage[key])
queryClient.clear()
Expand Down Expand Up @@ -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()
Comment on lines +339 to +353
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚑ Quick win

Use an async DOM assertion instead of a single microtask tick

await Promise.resolve() is timing-sensitive and can make this test flaky when mutation notifications land later than one tick. Assert via findBy.../waitFor on the rendered UI state.

Proposed fix
     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 Promise.resolve()
-
-      expect(
-        rendered.getByLabelText(/Mutation submitted at/),
-      ).toBeInTheDocument()
+      void mutation.execute({})
+      expect(
+        await rendered.findByLabelText(/Mutation submitted at/),
+      ).toBeInTheDocument()
     })
πŸ“ Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
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 Promise.resolve()
expect(
rendered.getByLabelText(/Mutation submitted at/),
).toBeInTheDocument()
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'),
})
void mutation.execute({})
expect(
await rendered.findByLabelText(/Mutation submitted at/),
).toBeInTheDocument()
πŸ€– Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/query-devtools/src/__tests__/Devtools.test.tsx` around lines 336 -
350, The test is using a single microtask tick (await Promise.resolve()) which
is flaky; replace that with an async DOM assertion such as awaiting
rendered.findByLabelText(/Mutation submitted at/) (or wrap the expect inside
waitFor) after calling mutation.execute({}) so the test waits for the mutation
UI to render; update the assertion that currently uses rendered.getByLabelText
to use await rendered.findByLabelText (or waitFor(() =>
expect(rendered.getByLabelText(...)).toBeInTheDocument())) and keep references
to renderDevtools, queryClient, and mutation.execute to locate the change.

})
})

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