Skip to content

Commit 4b1e711

Browse files
authored
test(devtools): Add base tests for react-query devtools (TanStack#2433)
* test: Add base setup for devtools tests Add the basic test for opening and closing devtools * (test): ✅ Add tests for query states * ✅ Add test for filtering and opening query details * ✅ Add sorting tests * ✅ Add test for sorting order * 🚨 Fix linter warnings * ♻️ Code review updates * 🚨 Fix linter warnings
1 parent e6da990 commit 4b1e711

File tree

3 files changed

+392
-1
lines changed

3 files changed

+392
-1
lines changed

src/devtools/devtools.tsx

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -240,6 +240,7 @@ export function ReactQueryDevtools({
240240
{isResolvedOpen ? (
241241
<Button
242242
type="button"
243+
aria-label="Close React Query Devtools"
243244
{...otherCloseButtonProps}
244245
onClick={() => {
245246
setIsOpen(false)
@@ -332,7 +333,8 @@ const sortFns = {
332333
? 1
333334
: -1,
334335
'Query Hash': (a, b) => (a.queryHash > b.queryHash ? 1 : -1),
335-
'Last Updated': (a, b) => (a.state.updatedAt < b.state.updatedAt ? 1 : -1),
336+
'Last Updated': (a, b) =>
337+
a.state.dataUpdatedAt < b.state.dataUpdatedAt ? 1 : -1,
336338
}
337339

338340
export const ReactQueryDevtoolsPanel = React.forwardRef(
@@ -551,6 +553,7 @@ export const ReactQueryDevtoolsPanel = React.forwardRef(
551553
>
552554
<Input
553555
placeholder="Filter"
556+
aria-label="Filter by queryhash"
554557
value={filter ?? ''}
555558
onChange={e => setFilter(e.target.value)}
556559
onKeyDown={e => {
@@ -564,6 +567,7 @@ export const ReactQueryDevtoolsPanel = React.forwardRef(
564567
{!filter ? (
565568
<>
566569
<Select
570+
aria-label="Sort queries"
567571
value={sort}
568572
onChange={e => setSort(e.target.value)}
569573
style={{
@@ -603,6 +607,8 @@ export const ReactQueryDevtoolsPanel = React.forwardRef(
603607
<div
604608
suppressHydrationWarning
605609
key={query.queryHash || i}
610+
role="button"
611+
aria-label={`Open query details for ${query.queryHash}`}
606612
onClick={() =>
607613
setActiveQueryHash(
608614
activeQueryHash === query.queryHash ? '' : query.queryHash
Lines changed: 311 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,311 @@
1+
import React from 'react'
2+
import {
3+
fireEvent,
4+
screen,
5+
waitFor,
6+
waitForElementToBeRemoved,
7+
} from '@testing-library/react'
8+
import { useQuery } from '../..'
9+
import {
10+
getByTextContent,
11+
renderWithClient,
12+
sleep,
13+
createQueryClient,
14+
} from './utils'
15+
16+
describe('ReactQueryDevtools', () => {
17+
it('should be able to open and close devtools', async () => {
18+
const { queryClient } = createQueryClient()
19+
20+
function Page() {
21+
const { data = 'default' } = useQuery('check', async () => {
22+
await sleep(10)
23+
return 'test'
24+
})
25+
26+
return (
27+
<div>
28+
<h1>{data}</h1>
29+
</div>
30+
)
31+
}
32+
33+
renderWithClient(queryClient, <Page />, { initialIsOpen: false })
34+
35+
const closeButton = screen.queryByRole('button', {
36+
name: /close react query devtools/i,
37+
})
38+
expect(closeButton).toBeNull()
39+
fireEvent.click(
40+
screen.getByRole('button', { name: /open react query devtools/i })
41+
)
42+
43+
await waitForElementToBeRemoved(() =>
44+
screen.queryByRole('button', { name: /open react query devtools/i })
45+
)
46+
fireEvent.click(
47+
screen.getByRole('button', { name: /close react query devtools/i })
48+
)
49+
50+
await screen.findByRole('button', { name: /open react query devtools/i })
51+
})
52+
53+
it('should display the correct query states', async () => {
54+
const { queryClient, queryCache } = createQueryClient()
55+
56+
function Page() {
57+
const { data = 'default' } = useQuery(
58+
'check',
59+
async () => {
60+
await sleep(100)
61+
return 'test'
62+
},
63+
{ staleTime: 300 }
64+
)
65+
66+
return (
67+
<div>
68+
<h1>{data}</h1>
69+
</div>
70+
)
71+
}
72+
73+
function PageParent() {
74+
const [isPageVisible, togglePageVisible] = React.useReducer(
75+
visible => !visible,
76+
true
77+
)
78+
79+
return (
80+
<div>
81+
<button
82+
type="button"
83+
aria-label="Toggle page visibility"
84+
onClick={togglePageVisible}
85+
>
86+
Toggle Page
87+
</button>
88+
{isPageVisible && <Page />}
89+
</div>
90+
)
91+
}
92+
93+
renderWithClient(queryClient, <PageParent />)
94+
95+
fireEvent.click(
96+
screen.getByRole('button', { name: /open react query devtools/i })
97+
)
98+
99+
const currentQuery = queryCache.find('check')
100+
101+
// When the query is fetching then expect number of
102+
// fetching queries to be 1
103+
expect(currentQuery?.isFetching()).toEqual(true)
104+
await screen.findByText(
105+
getByTextContent('fresh (0) fetching (1) stale (0) inactive (0)')
106+
)
107+
108+
// When we are done fetching the query doesn't go stale
109+
// until 300ms after, so expect the number of fresh
110+
// queries to be 1
111+
await waitFor(() => {
112+
expect(currentQuery?.isFetching()).toEqual(false)
113+
})
114+
await screen.findByText(
115+
getByTextContent('fresh (1) fetching (0) stale (0) inactive (0)')
116+
)
117+
118+
// Then wait for the query to go stale and then
119+
// expect the number of stale queries to be 1
120+
await waitFor(() => {
121+
expect(currentQuery?.isStale()).toEqual(false)
122+
})
123+
await screen.findByText(
124+
getByTextContent('fresh (0) fetching (0) stale (1) inactive (0)')
125+
)
126+
127+
// Unmount the page component thus making the query inactive
128+
// and expect number of inactive queries to be 1
129+
fireEvent.click(
130+
screen.getByRole('button', { name: /toggle page visibility/i })
131+
)
132+
await screen.findByText(
133+
getByTextContent('fresh (0) fetching (0) stale (0) inactive (1)')
134+
)
135+
})
136+
137+
it('should display the query hash and open the query details', async () => {
138+
const { queryClient, queryCache } = createQueryClient()
139+
140+
function Page() {
141+
const { data = 'default' } = useQuery('check', async () => {
142+
await sleep(10)
143+
return 'test'
144+
})
145+
146+
return (
147+
<div>
148+
<h1>{data}</h1>
149+
</div>
150+
)
151+
}
152+
153+
renderWithClient(queryClient, <Page />)
154+
155+
fireEvent.click(
156+
screen.getByRole('button', { name: /open react query devtools/i })
157+
)
158+
159+
const currentQuery = queryCache.find('check')
160+
161+
await screen.findByText(getByTextContent(`1${currentQuery?.queryHash}`))
162+
163+
fireEvent.click(
164+
screen.getByRole('button', {
165+
name: `Open query details for ${currentQuery?.queryHash}`,
166+
})
167+
)
168+
169+
await screen.findByText(/query details/i)
170+
})
171+
172+
it('should filter the queries via the query hash', async () => {
173+
const { queryClient, queryCache } = createQueryClient()
174+
175+
function Page() {
176+
const fooResult = useQuery('foo', async () => {
177+
await sleep(10)
178+
return 'foo-result'
179+
})
180+
181+
const barResult = useQuery('bar', async () => {
182+
await sleep(10)
183+
return 'bar-result'
184+
})
185+
186+
const bazResult = useQuery('baz', async () => {
187+
await sleep(10)
188+
return 'baz-result'
189+
})
190+
191+
return (
192+
<div>
193+
<h1>
194+
{barResult.data} {fooResult.data} {bazResult.data}
195+
</h1>
196+
</div>
197+
)
198+
}
199+
200+
renderWithClient(queryClient, <Page />)
201+
202+
fireEvent.click(
203+
screen.getByRole('button', { name: /open react query devtools/i })
204+
)
205+
206+
const fooQueryHash = queryCache.find('foo')?.queryHash ?? 'invalid hash'
207+
const barQueryHash = queryCache.find('bar')?.queryHash ?? 'invalid hash'
208+
const bazQueryHash = queryCache.find('baz')?.queryHash ?? 'invalid hash'
209+
210+
await screen.findByText(fooQueryHash)
211+
screen.getByText(barQueryHash)
212+
screen.getByText(bazQueryHash)
213+
214+
const filterInput = screen.getByLabelText(/filter by queryhash/i)
215+
fireEvent.change(filterInput, { target: { value: 'fo' } })
216+
217+
await screen.findByText(fooQueryHash)
218+
const barItem = screen.queryByText(barQueryHash)
219+
const bazItem = screen.queryByText(bazQueryHash)
220+
expect(barItem).toBeNull()
221+
expect(bazItem).toBeNull()
222+
223+
fireEvent.change(filterInput, { target: { value: '' } })
224+
})
225+
226+
it('should sort the queries according to the sorting filter', async () => {
227+
const { queryClient, queryCache } = createQueryClient()
228+
229+
function Page() {
230+
const query1Result = useQuery('query-1', async () => {
231+
await sleep(20)
232+
return 'query-1-result'
233+
})
234+
235+
const query2Result = useQuery('query-2', async () => {
236+
await sleep(60)
237+
return 'query-2-result'
238+
})
239+
240+
const query3Result = useQuery(
241+
'query-3',
242+
async () => {
243+
await sleep(40)
244+
return 'query-3-result'
245+
},
246+
{ staleTime: Infinity }
247+
)
248+
249+
return (
250+
<div>
251+
<h1>
252+
{query1Result.data} {query2Result.data} {query3Result.data}
253+
</h1>
254+
</div>
255+
)
256+
}
257+
258+
renderWithClient(queryClient, <Page />)
259+
260+
fireEvent.click(
261+
screen.getByRole('button', { name: /open react query devtools/i })
262+
)
263+
264+
const query1Hash = queryCache.find('query-1')?.queryHash ?? 'invalid hash'
265+
const query2Hash = queryCache.find('query-2')?.queryHash ?? 'invalid hash'
266+
const query3Hash = queryCache.find('query-3')?.queryHash ?? 'invalid hash'
267+
268+
const sortSelect = screen.getByLabelText(/sort queries/i)
269+
let queries = []
270+
271+
// When sorted by query hash the queries get sorted according
272+
// to just the number, with the order being -> query-1, query-2, query-3
273+
fireEvent.change(sortSelect, { target: { value: 'Query Hash' } })
274+
/** To check the order of the queries we can use regex to find
275+
* all the row items in an array and then compare the items
276+
* one by one in the order we expect it
277+
* @reference https://github.com/testing-library/react-testing-library/issues/313#issuecomment-625294327
278+
*/
279+
queries = await screen.findAllByText(/\["query-[1-3]"\]/)
280+
expect(queries[0]?.textContent).toEqual(query1Hash)
281+
expect(queries[1]?.textContent).toEqual(query2Hash)
282+
expect(queries[2]?.textContent).toEqual(query3Hash)
283+
284+
// When sorted by the last updated date the queries are sorted by the time
285+
// they were updated and since the query-2 takes longest time to complete
286+
// and query-1 the shortest, so the order is -> query-2, query-3, query-1
287+
fireEvent.change(sortSelect, { target: { value: 'Last Updated' } })
288+
queries = await screen.findAllByText(/\["query-[1-3]"\]/)
289+
expect(queries[0]?.textContent).toEqual(query2Hash)
290+
expect(queries[1]?.textContent).toEqual(query3Hash)
291+
expect(queries[2]?.textContent).toEqual(query1Hash)
292+
293+
// When sorted by the status and then last updated date the queries
294+
// query-3 takes precedence because its stale time being infinity, it
295+
// always remains fresh, the rest of the queries are sorted by their last
296+
// updated time, so the resulting order is -> query-3, query-2, query-1
297+
fireEvent.change(sortSelect, { target: { value: 'Status > Last Updated' } })
298+
queries = await screen.findAllByText(/\["query-[1-3]"\]/)
299+
expect(queries[0]?.textContent).toEqual(query3Hash)
300+
expect(queries[1]?.textContent).toEqual(query2Hash)
301+
expect(queries[2]?.textContent).toEqual(query1Hash)
302+
303+
// Switch the order form ascending to descending and expect the
304+
// query order to be reversed from previous state
305+
fireEvent.click(screen.getByRole('button', { name: / asc/i }))
306+
queries = await screen.findAllByText(/\["query-[1-3]"\]/)
307+
expect(queries[0]?.textContent).toEqual(query1Hash)
308+
expect(queries[1]?.textContent).toEqual(query2Hash)
309+
expect(queries[2]?.textContent).toEqual(query3Hash)
310+
})
311+
})

0 commit comments

Comments
 (0)