1507 lines
50 KiB
TypeScript
1507 lines
50 KiB
TypeScript
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
|
import {
|
|
QueryClient,
|
|
QueryObserver,
|
|
dehydrate,
|
|
hydrate,
|
|
noop,
|
|
onlineManager,
|
|
} from '@tanstack/query-core'
|
|
import { fireEvent, render } from '@solidjs/testing-library'
|
|
import { createLocalStorage } from '@solid-primitives/storage'
|
|
import { Devtools } from '../Devtools'
|
|
import { PiPProvider, QueryDevtoolsContext, ThemeContext } from '../contexts'
|
|
import type { QueryDevtoolsProps } from '../contexts'
|
|
|
|
// `solid-transition-group` internally imports from
|
|
// `@solid-primitives/transition-group`, whose `exports` field points at
|
|
// `src/index.ts` (not published) under a `@solid-primitives/source` condition
|
|
// that Vite can't fall through, so we stub it with a transparent pass-through.
|
|
vi.mock('solid-transition-group', () => ({
|
|
TransitionGroup: (props: { children: unknown }) => props.children,
|
|
}))
|
|
|
|
// `goober` compiles every `css\`...\`` template literal at mount time
|
|
// (template parsing + class hashing + style serialization), which
|
|
// dominates mount cost and produces no value for label/role-based
|
|
// assertions, so we replace it with a no-op factory.
|
|
vi.mock('goober', () => {
|
|
let counter = 0
|
|
const css = Object.assign(() => `tsqd-${++counter}`, {
|
|
bind: () => css,
|
|
})
|
|
return { css, glob: () => {}, setup: () => {} }
|
|
})
|
|
|
|
describe('Devtools', () => {
|
|
const storage: { [key: string]: string } = {}
|
|
let queryClient: QueryClient
|
|
let previousRootFontSize = ''
|
|
|
|
beforeEach(() => {
|
|
vi.useFakeTimers()
|
|
previousRootFontSize = document.documentElement.style.fontSize
|
|
vi.stubGlobal('localStorage', {
|
|
getItem: (key: string) =>
|
|
Object.prototype.hasOwnProperty.call(storage, key)
|
|
? storage[key]
|
|
: null,
|
|
setItem: (key: string, value: string) => {
|
|
storage[key] = value
|
|
},
|
|
removeItem: (key: string) => {
|
|
delete storage[key]
|
|
},
|
|
clear: () => {
|
|
Object.keys(storage).forEach((key) => delete storage[key])
|
|
},
|
|
})
|
|
vi.stubGlobal(
|
|
'matchMedia',
|
|
vi.fn().mockImplementation((query: string) => ({
|
|
matches: false,
|
|
media: query,
|
|
onchange: null,
|
|
addEventListener: vi.fn(),
|
|
removeEventListener: vi.fn(),
|
|
addListener: vi.fn(),
|
|
removeListener: vi.fn(),
|
|
dispatchEvent: vi.fn(),
|
|
})),
|
|
)
|
|
vi.stubGlobal(
|
|
'ResizeObserver',
|
|
class {
|
|
callback: ResizeObserverCallback
|
|
constructor(callback: ResizeObserverCallback) {
|
|
this.callback = callback
|
|
}
|
|
observe = vi.fn((target: Element) => {
|
|
this.callback(
|
|
[
|
|
{
|
|
target,
|
|
contentRect: { width: 1000, height: 500 } as DOMRectReadOnly,
|
|
} as ResizeObserverEntry,
|
|
],
|
|
this as unknown as ResizeObserver,
|
|
)
|
|
})
|
|
unobserve = vi.fn()
|
|
disconnect = vi.fn()
|
|
},
|
|
)
|
|
queryClient = new QueryClient()
|
|
document.documentElement.style.fontSize = '16px'
|
|
})
|
|
|
|
afterEach(() => {
|
|
vi.unstubAllGlobals()
|
|
Object.keys(storage).forEach((key) => delete storage[key])
|
|
queryClient.clear()
|
|
onlineManager.setOnline(true)
|
|
document.documentElement.style.fontSize = previousRootFontSize
|
|
vi.useRealTimers()
|
|
})
|
|
|
|
function renderDevtools(
|
|
overrides: Partial<QueryDevtoolsProps> = {},
|
|
initialStorage: Record<string, string> = {},
|
|
) {
|
|
Object.entries(initialStorage).forEach(([key, value]) => {
|
|
localStorage.setItem(key, value)
|
|
})
|
|
return render(() => {
|
|
const [localStore, setLocalStore] = createLocalStorage({
|
|
prefix: 'TanstackQueryDevtools',
|
|
})
|
|
return (
|
|
<QueryDevtoolsContext.Provider
|
|
value={{
|
|
client: queryClient,
|
|
queryFlavor: 'TanStack Query',
|
|
version: '5',
|
|
onlineManager,
|
|
...overrides,
|
|
}}
|
|
>
|
|
<PiPProvider localStore={localStore} setLocalStore={setLocalStore}>
|
|
<ThemeContext.Provider value={() => 'dark'}>
|
|
<Devtools localStore={localStore} setLocalStore={setLocalStore} />
|
|
</ThemeContext.Provider>
|
|
</PiPProvider>
|
|
</QueryDevtoolsContext.Provider>
|
|
)
|
|
})
|
|
}
|
|
|
|
describe('initial state', () => {
|
|
it('should render the open devtools button', () => {
|
|
const rendered = renderDevtools()
|
|
|
|
expect(
|
|
rendered.getByLabelText('Open Tanstack query devtools'),
|
|
).toBeInTheDocument()
|
|
})
|
|
|
|
it('should not render the panel by default', () => {
|
|
const rendered = renderDevtools()
|
|
|
|
expect(
|
|
rendered.queryByLabelText('Tanstack query devtools'),
|
|
).not.toBeInTheDocument()
|
|
})
|
|
|
|
it('should render the panel when "initialIsOpen" is "true"', () => {
|
|
const rendered = renderDevtools({ initialIsOpen: true })
|
|
|
|
expect(
|
|
rendered.getByLabelText('Tanstack query devtools'),
|
|
).toBeInTheDocument()
|
|
})
|
|
|
|
it('should render the panel when "localStore.open" is "true"', () => {
|
|
const rendered = renderDevtools(
|
|
{},
|
|
{ 'TanstackQueryDevtools.open': 'true' },
|
|
)
|
|
|
|
expect(
|
|
rendered.getByLabelText('Tanstack query devtools'),
|
|
).toBeInTheDocument()
|
|
})
|
|
|
|
it('should not render the panel when "localStore.open" is "false" even if "initialIsOpen" is "true"', () => {
|
|
const rendered = renderDevtools(
|
|
{ initialIsOpen: true },
|
|
{ 'TanstackQueryDevtools.open': 'false' },
|
|
)
|
|
|
|
expect(
|
|
rendered.queryByLabelText('Tanstack query devtools'),
|
|
).not.toBeInTheDocument()
|
|
})
|
|
})
|
|
|
|
describe('open and close', () => {
|
|
it('should render the panel when the open button is clicked', () => {
|
|
const rendered = renderDevtools()
|
|
|
|
fireEvent.click(rendered.getByLabelText('Open Tanstack query devtools'))
|
|
|
|
expect(
|
|
rendered.getByLabelText('Tanstack query devtools'),
|
|
).toBeInTheDocument()
|
|
})
|
|
|
|
it('should hide the open button when the panel is open', () => {
|
|
const rendered = renderDevtools()
|
|
|
|
fireEvent.click(rendered.getByLabelText('Open Tanstack query devtools'))
|
|
|
|
expect(
|
|
rendered.queryByLabelText('Open Tanstack query devtools'),
|
|
).not.toBeInTheDocument()
|
|
})
|
|
|
|
it('should hide the panel when the close button is clicked', () => {
|
|
const rendered = renderDevtools({ initialIsOpen: true })
|
|
|
|
fireEvent.click(rendered.getByLabelText('Close tanstack query devtools'))
|
|
|
|
expect(
|
|
rendered.queryByLabelText('Tanstack query devtools'),
|
|
).not.toBeInTheDocument()
|
|
})
|
|
|
|
it('should render the open button after the panel is closed', () => {
|
|
const rendered = renderDevtools({ initialIsOpen: true })
|
|
|
|
fireEvent.click(rendered.getByLabelText('Close tanstack query devtools'))
|
|
|
|
expect(
|
|
rendered.getByLabelText('Open Tanstack query devtools'),
|
|
).toBeInTheDocument()
|
|
})
|
|
|
|
it('should persist "open" as "true" to "localStorage" when the open button is clicked', () => {
|
|
const rendered = renderDevtools()
|
|
|
|
fireEvent.click(rendered.getByLabelText('Open Tanstack query devtools'))
|
|
|
|
expect(localStorage.getItem('TanstackQueryDevtools.open')).toBe('true')
|
|
})
|
|
|
|
it('should persist "open" as "false" to "localStorage" when the close button is clicked', () => {
|
|
const rendered = renderDevtools({ initialIsOpen: true })
|
|
|
|
fireEvent.click(rendered.getByLabelText('Close tanstack query 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)
|
|
}
|
|
})
|
|
|
|
it('should render a query row when a hydrated query uses a custom hash function', async () => {
|
|
queryClient.query({
|
|
queryKey: ['posts'],
|
|
queryFn: () => [{ id: 1 }],
|
|
queryKeyHashFn: () => 'custom-posts-hash',
|
|
})
|
|
await vi.advanceTimersByTimeAsync(0)
|
|
const dehydratedState = dehydrate(queryClient)
|
|
|
|
queryClient = new QueryClient()
|
|
hydrate(queryClient, dehydratedState)
|
|
|
|
const rendered = renderDevtools({ initialIsOpen: true })
|
|
|
|
expect(
|
|
rendered.getByLabelText(/Query key custom-posts-hash/),
|
|
).toBeInTheDocument()
|
|
})
|
|
})
|
|
|
|
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()
|
|
})
|
|
|
|
it('should render an idle mutation that has been built but not executed', async () => {
|
|
const rendered = renderDevtools({ initialIsOpen: true })
|
|
|
|
fireEvent.click(rendered.getByText('Mutations'))
|
|
|
|
queryClient.getMutationCache().build(queryClient, {
|
|
mutationKey: ['idle-mut'],
|
|
mutationFn: () => Promise.resolve('ok'),
|
|
})
|
|
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()
|
|
})
|
|
|
|
it('should render a "static" indicator for a query with "staleTime: \'static\'"', () => {
|
|
const query = queryClient.getQueryCache().build(queryClient, {
|
|
queryKey: ['static-q'],
|
|
queryFn: () => 'x',
|
|
})
|
|
const observer = new QueryObserver(queryClient, {
|
|
queryKey: ['static-q'],
|
|
queryFn: () => 'x',
|
|
staleTime: 'static',
|
|
})
|
|
query.addObserver(observer)
|
|
query.setState({ ...query.state, data: 'x' })
|
|
const rendered = renderDevtools({ initialIsOpen: true })
|
|
|
|
expect(rendered.getByText('static')).toBeInTheDocument()
|
|
expect(rendered.getByLabelText(/, static/)).toBeInTheDocument()
|
|
})
|
|
|
|
it('should render a query row when an object query key is mutated in place', () => {
|
|
const filters = { page: 1 }
|
|
queryClient.setQueryData(['mutable-key', filters], 'x')
|
|
filters.page = 2
|
|
|
|
const rendered = renderDevtools({ initialIsOpen: true })
|
|
|
|
expect(
|
|
rendered.getByLabelText(/Query key \["mutable-key",\{"page":1\}\]/),
|
|
).toBeInTheDocument()
|
|
})
|
|
})
|
|
|
|
describe('picture-in-picture', () => {
|
|
type FakePipWindow = Pick<
|
|
Window,
|
|
| 'document'
|
|
| 'innerWidth'
|
|
| 'innerHeight'
|
|
| 'addEventListener'
|
|
| 'removeEventListener'
|
|
| 'close'
|
|
>
|
|
|
|
function stubPipWindow(
|
|
overrides: Partial<
|
|
Pick<FakePipWindow, 'innerWidth' | 'innerHeight'>
|
|
> = {},
|
|
) {
|
|
const pipDocument = document.implementation.createHTMLDocument('PiP')
|
|
const listeners = new Map<string, EventListener>()
|
|
const fakeWindow: FakePipWindow = {
|
|
document: pipDocument,
|
|
innerWidth: 800,
|
|
innerHeight: 600,
|
|
addEventListener: vi.fn((event: string, handler: EventListener) => {
|
|
listeners.set(event, handler)
|
|
}),
|
|
removeEventListener: vi.fn((event: string) => {
|
|
listeners.delete(event)
|
|
}),
|
|
close: vi.fn(),
|
|
...overrides,
|
|
}
|
|
const open = vi.fn(() => fakeWindow)
|
|
vi.stubGlobal('open', open)
|
|
return {
|
|
pipDocument,
|
|
fakeWindow,
|
|
open,
|
|
fire: (event: string) => {
|
|
listeners.get(event)?.(new Event(event))
|
|
},
|
|
}
|
|
}
|
|
|
|
it('should open a PiP window when the picture-in-picture button is clicked', () => {
|
|
const { open } = stubPipWindow()
|
|
const rendered = renderDevtools({ initialIsOpen: true })
|
|
|
|
fireEvent.click(
|
|
rendered.getByLabelText('Open in picture-in-picture mode'),
|
|
)
|
|
|
|
expect(open).toHaveBeenCalledWith(
|
|
'',
|
|
'TSQD-Devtools-Panel',
|
|
expect.stringMatching(/^width=\d+,height=\d+,popup$/),
|
|
)
|
|
expect(localStorage.getItem('TanstackQueryDevtools.pip_open')).toBe(
|
|
'true',
|
|
)
|
|
})
|
|
|
|
it('should hide the in-page panel while a PiP window is open', () => {
|
|
stubPipWindow()
|
|
const rendered = renderDevtools({ initialIsOpen: true })
|
|
|
|
fireEvent.click(
|
|
rendered.getByLabelText('Open in picture-in-picture mode'),
|
|
)
|
|
|
|
expect(
|
|
rendered.container.querySelector('.tsqd-main-panel-container'),
|
|
).toBeNull()
|
|
})
|
|
|
|
it('should restore the in-page panel when the PiP window is closed', () => {
|
|
const { fire } = stubPipWindow()
|
|
const rendered = renderDevtools({ initialIsOpen: true })
|
|
|
|
fireEvent.click(
|
|
rendered.getByLabelText('Open in picture-in-picture mode'),
|
|
)
|
|
fire('pagehide')
|
|
|
|
expect(
|
|
rendered.getByLabelText('Open in picture-in-picture mode'),
|
|
).toBeInTheDocument()
|
|
})
|
|
|
|
it('should render the PiP panel with the narrow layout when the PiP window is below the second breakpoint', () => {
|
|
// secondBreakpoint = 796; pick a width comfortably below it so the
|
|
// PiP panel evaluates its narrow (`flex-direction: column`) branch.
|
|
const { pipDocument } = stubPipWindow({ innerWidth: 500 })
|
|
queryClient.setQueryData(['narrow-pip-query'], { hello: 'world' })
|
|
const rendered = renderDevtools({ initialIsOpen: true })
|
|
|
|
fireEvent.click(
|
|
rendered.getByLabelText('Open in picture-in-picture mode'),
|
|
)
|
|
|
|
expect(
|
|
pipDocument.querySelector(
|
|
'[aria-label="Close Tanstack query devtools"]',
|
|
),
|
|
).not.toBeNull()
|
|
expect(
|
|
pipDocument.querySelector('[aria-label*="narrow-pip-query"]'),
|
|
).not.toBeNull()
|
|
})
|
|
})
|
|
|
|
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()
|
|
})
|
|
})
|
|
|
|
describe('query details', () => {
|
|
it('should open the query details panel when a query row is clicked', () => {
|
|
queryClient.setQueryData(['posts'], [{ id: 1 }])
|
|
const rendered = renderDevtools({ initialIsOpen: true })
|
|
|
|
fireEvent.click(rendered.getByLabelText(/Query key \["posts"\]/))
|
|
|
|
expect(rendered.getByText('Query Details')).toBeInTheDocument()
|
|
})
|
|
|
|
it('should close the query details panel when the same row is clicked again', () => {
|
|
queryClient.setQueryData(['details-toggle'], [{ id: 1 }])
|
|
const rendered = renderDevtools({ initialIsOpen: true })
|
|
|
|
fireEvent.click(rendered.getByLabelText(/Query key \["details-toggle"\]/))
|
|
fireEvent.click(rendered.getByLabelText(/Query key \["details-toggle"\]/))
|
|
|
|
expect(rendered.queryByText('Query Details')).not.toBeInTheDocument()
|
|
})
|
|
})
|
|
|
|
describe('query actions', () => {
|
|
it('should remove the query when the "Remove" button is clicked', () => {
|
|
queryClient.setQueryData(['action-remove'], [{ id: 1 }])
|
|
const rendered = renderDevtools({ initialIsOpen: true })
|
|
|
|
fireEvent.click(rendered.getByLabelText(/Query key \["action-remove"\]/))
|
|
fireEvent.click(rendered.getByText('Remove'))
|
|
|
|
expect(
|
|
rendered.queryByLabelText(/Query key \["action-remove"\]/),
|
|
).not.toBeInTheDocument()
|
|
})
|
|
|
|
it('should reset the query when the "Reset" button is clicked', () => {
|
|
queryClient.setQueryData(['action-reset'], [{ id: 1 }])
|
|
const rendered = renderDevtools({ initialIsOpen: true })
|
|
|
|
fireEvent.click(rendered.getByLabelText(/Query key \["action-reset"\]/))
|
|
fireEvent.click(rendered.getByText('Reset'))
|
|
|
|
expect(queryClient.getQueryData(['action-reset'])).toBeUndefined()
|
|
})
|
|
|
|
it('should invalidate the query when the "Invalidate" button is clicked', () => {
|
|
queryClient.setQueryData(['action-invalidate'], [{ id: 1 }])
|
|
const rendered = renderDevtools({ initialIsOpen: true })
|
|
|
|
fireEvent.click(
|
|
rendered.getByLabelText(/Query key \["action-invalidate"\]/),
|
|
)
|
|
fireEvent.click(rendered.getByText('Invalidate'))
|
|
|
|
expect(
|
|
queryClient.getQueryState(['action-invalidate'])?.isInvalidated,
|
|
).toBe(true)
|
|
})
|
|
|
|
it('should dispatch a "REFETCH" event when "Refetch" is clicked', () => {
|
|
queryClient.setQueryData(['action-refetch'], [{ id: 1 }])
|
|
const rendered = renderDevtools({ initialIsOpen: true })
|
|
|
|
const listener = vi.fn()
|
|
window.addEventListener('@tanstack/query-devtools-event', listener)
|
|
|
|
try {
|
|
fireEvent.click(
|
|
rendered.getByLabelText(/Query key \["action-refetch"\]/),
|
|
)
|
|
fireEvent.click(rendered.getByText('Refetch'))
|
|
|
|
const dispatched = listener.mock.calls.some(
|
|
([e]) => (e as CustomEvent).detail.type === 'REFETCH',
|
|
)
|
|
expect(dispatched).toBe(true)
|
|
} finally {
|
|
window.removeEventListener('@tanstack/query-devtools-event', listener)
|
|
}
|
|
})
|
|
|
|
it('should set the query status to "error" when "Trigger Error" is clicked', () => {
|
|
queryClient.setQueryData(['action-error'], [{ id: 1 }])
|
|
const rendered = renderDevtools({ initialIsOpen: true })
|
|
|
|
fireEvent.click(rendered.getByLabelText(/Query key \["action-error"\]/))
|
|
fireEvent.click(rendered.getByText('Trigger Error'))
|
|
|
|
expect(queryClient.getQueryState(['action-error'])?.status).toBe('error')
|
|
})
|
|
|
|
it('should restore the query status when "Restore Error" is clicked after "Trigger Error"', () => {
|
|
queryClient.setQueryData(['action-restore-error'], [{ id: 1 }])
|
|
const rendered = renderDevtools({ initialIsOpen: true })
|
|
|
|
fireEvent.click(
|
|
rendered.getByLabelText(/Query key \["action-restore-error"\]/),
|
|
)
|
|
fireEvent.click(rendered.getByText('Trigger Error'))
|
|
fireEvent.click(rendered.getByText('Restore Error'))
|
|
|
|
expect(queryClient.getQueryState(['action-restore-error'])?.status).toBe(
|
|
'pending',
|
|
)
|
|
})
|
|
|
|
it('should restore the previous query options when "Restore Loading" is clicked after "Trigger Loading"', async () => {
|
|
const queryFn = vi.fn(() => Promise.resolve('original'))
|
|
void queryClient
|
|
.query({
|
|
queryKey: ['action-restore-loading'],
|
|
queryFn,
|
|
})
|
|
.catch(noop)
|
|
await vi.advanceTimersByTimeAsync(0)
|
|
expect(queryFn).toHaveBeenCalledTimes(1)
|
|
|
|
const rendered = renderDevtools({ initialIsOpen: true })
|
|
|
|
fireEvent.click(
|
|
rendered.getByLabelText(/Query key \["action-restore-loading"\]/),
|
|
)
|
|
|
|
// First click puts the query into a pending state with `data: undefined`
|
|
// and stashes the original options in `fetchMeta.__previousQueryOptions`.
|
|
fireEvent.click(rendered.getByText('Trigger Loading'))
|
|
expect(
|
|
queryClient.getQueryState(['action-restore-loading'])?.status,
|
|
).toBe('pending')
|
|
|
|
// Second click runs `restoreQueryAfterLoadingOrError`, which cancels the
|
|
// never-resolving fetch and refetches with the stashed options.
|
|
fireEvent.click(rendered.getByText('Restore Loading'))
|
|
await vi.advanceTimersByTimeAsync(0)
|
|
|
|
expect(queryFn).toHaveBeenCalledTimes(2)
|
|
expect(queryClient.getQueryData(['action-restore-loading'])).toBe(
|
|
'original',
|
|
)
|
|
})
|
|
})
|
|
|
|
describe('mutation details', () => {
|
|
it('should open the mutation details panel when a mutation row is clicked', async () => {
|
|
const rendered = renderDevtools({ initialIsOpen: true })
|
|
|
|
fireEvent.click(rendered.getByText('Mutations'))
|
|
|
|
const mutation = queryClient.getMutationCache().build(queryClient, {
|
|
mutationKey: ['mutation-detail'],
|
|
mutationFn: () => Promise.resolve('ok'),
|
|
})
|
|
mutation.execute({})
|
|
await vi.advanceTimersByTimeAsync(0)
|
|
|
|
fireEvent.click(rendered.getByLabelText(/Mutation submitted at/))
|
|
|
|
expect(rendered.getByText('Mutation Details')).toBeInTheDocument()
|
|
})
|
|
})
|
|
|
|
describe('mutation sort order', () => {
|
|
it('should toggle the mutation sort order in the mutations view', () => {
|
|
const rendered = renderDevtools({ initialIsOpen: true })
|
|
fireEvent.click(rendered.getByText('Mutations'))
|
|
|
|
fireEvent.click(rendered.getByLabelText(/Sort order/))
|
|
const afterFirstToggle = localStorage.getItem(
|
|
'TanstackQueryDevtools.mutationSortOrder',
|
|
)
|
|
expect(afterFirstToggle).not.toBeNull()
|
|
|
|
fireEvent.click(rendered.getByLabelText(/Sort order/))
|
|
const afterSecondToggle = localStorage.getItem(
|
|
'TanstackQueryDevtools.mutationSortOrder',
|
|
)
|
|
expect(afterSecondToggle).not.toBe(afterFirstToggle)
|
|
})
|
|
})
|
|
|
|
describe('mutation filter', () => {
|
|
it('should filter mutations by their "mutationKey"', async () => {
|
|
const rendered = renderDevtools({ initialIsOpen: true })
|
|
fireEvent.click(rendered.getByText('Mutations'))
|
|
|
|
const matching = queryClient.getMutationCache().build(queryClient, {
|
|
mutationKey: ['filter-match'],
|
|
mutationFn: () => Promise.resolve('ok'),
|
|
})
|
|
const other = queryClient.getMutationCache().build(queryClient, {
|
|
mutationKey: ['filter-other'],
|
|
mutationFn: () => Promise.resolve('ok'),
|
|
})
|
|
matching.execute({})
|
|
other.execute({})
|
|
await vi.advanceTimersByTimeAsync(0)
|
|
|
|
expect(rendered.getAllByLabelText(/Mutation submitted at/)).toHaveLength(
|
|
2,
|
|
)
|
|
|
|
fireEvent.input(rendered.getByLabelText('Filter queries by query key'), {
|
|
target: { value: 'filter-match' },
|
|
})
|
|
|
|
expect(rendered.getAllByLabelText(/Mutation submitted at/)).toHaveLength(
|
|
1,
|
|
)
|
|
})
|
|
})
|
|
|
|
describe('data edit', () => {
|
|
it('should switch to data editor when "Bulk Edit Data" is clicked', () => {
|
|
queryClient.setQueryData(['edit-data'], { name: 'a' })
|
|
const rendered = renderDevtools({ initialIsOpen: true })
|
|
|
|
fireEvent.click(rendered.getByLabelText(/Query key \["edit-data"\]/))
|
|
fireEvent.click(rendered.getByLabelText('Bulk Edit Data'))
|
|
|
|
expect(
|
|
rendered.getByLabelText('Edit query data as JSON'),
|
|
).toBeInTheDocument()
|
|
})
|
|
|
|
it('should save the edited data when the form is submitted', () => {
|
|
queryClient.setQueryData(['edit-save'], { name: 'a' })
|
|
const rendered = renderDevtools({ initialIsOpen: true })
|
|
|
|
fireEvent.click(rendered.getByLabelText(/Query key \["edit-save"\]/))
|
|
fireEvent.click(rendered.getByLabelText('Bulk Edit Data'))
|
|
|
|
const textarea = rendered.getByLabelText('Edit query data as JSON')
|
|
fireEvent.input(textarea, {
|
|
target: { value: JSON.stringify({ name: 'b' }) },
|
|
})
|
|
fireEvent.submit(textarea.closest('form')!)
|
|
|
|
expect(queryClient.getQueryData(['edit-save'])).toEqual({ name: 'b' })
|
|
})
|
|
|
|
it('should set an error state when the edited data is invalid JSON', () => {
|
|
queryClient.setQueryData(['edit-invalid'], { name: 'a' })
|
|
const rendered = renderDevtools({ initialIsOpen: true })
|
|
|
|
fireEvent.click(rendered.getByLabelText(/Query key \["edit-invalid"\]/))
|
|
fireEvent.click(rendered.getByLabelText('Bulk Edit Data'))
|
|
|
|
const textarea = rendered.getByLabelText('Edit query data as JSON')
|
|
fireEvent.input(textarea, { target: { value: 'not json' } })
|
|
fireEvent.submit(textarea.closest('form')!)
|
|
|
|
expect(rendered.getByText('Invalid Value')).toBeInTheDocument()
|
|
})
|
|
|
|
it('should clear the error state when the textarea is focused after a submit failure', () => {
|
|
queryClient.setQueryData(['edit-refocus'], { name: 'a' })
|
|
const rendered = renderDevtools({ initialIsOpen: true })
|
|
|
|
fireEvent.click(rendered.getByLabelText(/Query key \["edit-refocus"\]/))
|
|
fireEvent.click(rendered.getByLabelText('Bulk Edit Data'))
|
|
|
|
const textarea = rendered.getByLabelText('Edit query data as JSON')
|
|
fireEvent.input(textarea, { target: { value: 'not json' } })
|
|
fireEvent.submit(textarea.closest('form')!)
|
|
|
|
expect(rendered.getByText('Invalid Value')).toBeInTheDocument()
|
|
|
|
fireEvent.focus(textarea)
|
|
|
|
expect(rendered.queryByText('Invalid Value')).toBeNull()
|
|
})
|
|
|
|
it('should return to the data view when the editor "Cancel" button is clicked', () => {
|
|
queryClient.setQueryData(['edit-cancel'], { name: 'a' })
|
|
const rendered = renderDevtools({ initialIsOpen: true })
|
|
|
|
fireEvent.click(rendered.getByLabelText(/Query key \["edit-cancel"\]/))
|
|
fireEvent.click(rendered.getByLabelText('Bulk Edit Data'))
|
|
|
|
expect(
|
|
rendered.getByLabelText('Edit query data as JSON'),
|
|
).toBeInTheDocument()
|
|
|
|
fireEvent.click(rendered.getByText('Cancel'))
|
|
|
|
expect(rendered.queryByLabelText('Edit query data as JSON')).toBeNull()
|
|
expect(rendered.getByLabelText('Bulk Edit Data')).toBeInTheDocument()
|
|
})
|
|
})
|
|
|
|
describe('error type select', () => {
|
|
it('should render the error type select when "errorTypes" is provided', () => {
|
|
queryClient.setQueryData(['error-select'], [{ id: 1 }])
|
|
const rendered = renderDevtools({
|
|
initialIsOpen: true,
|
|
errorTypes: [
|
|
{
|
|
name: 'NetworkError',
|
|
initializer: () => new Error('Network'),
|
|
},
|
|
],
|
|
})
|
|
|
|
fireEvent.click(rendered.getByLabelText(/Query key \["error-select"\]/))
|
|
|
|
expect(
|
|
rendered.getByLabelText('Select error type to trigger'),
|
|
).toBeInTheDocument()
|
|
})
|
|
|
|
it('should trigger error when an error type is selected', () => {
|
|
queryClient.setQueryData(['error-select-trigger'], [{ id: 1 }])
|
|
const rendered = renderDevtools({
|
|
initialIsOpen: true,
|
|
errorTypes: [
|
|
{
|
|
name: 'NetworkError',
|
|
initializer: () => new Error('Network'),
|
|
},
|
|
],
|
|
})
|
|
|
|
fireEvent.click(
|
|
rendered.getByLabelText(/Query key \["error-select-trigger"\]/),
|
|
)
|
|
const select = rendered.getByLabelText('Select error type to trigger')
|
|
fireEvent.change(select, { target: { value: 'NetworkError' } })
|
|
|
|
expect(queryClient.getQueryState(['error-select-trigger'])?.status).toBe(
|
|
'error',
|
|
)
|
|
})
|
|
})
|
|
|
|
describe('sort by', () => {
|
|
it('should change sort key when the sort dropdown is changed in queries view', () => {
|
|
const rendered = renderDevtools({ initialIsOpen: true })
|
|
|
|
fireEvent.change(rendered.getByLabelText('Sort queries by'), {
|
|
target: { value: 'last updated' },
|
|
})
|
|
|
|
expect(localStorage.getItem('TanstackQueryDevtools.sort')).toBe(
|
|
'last updated',
|
|
)
|
|
})
|
|
|
|
it('should change sort key when the sort dropdown is changed in mutations view', () => {
|
|
const rendered = renderDevtools({ initialIsOpen: true })
|
|
fireEvent.click(rendered.getByText('Mutations'))
|
|
|
|
fireEvent.change(rendered.getByLabelText('Sort mutations by'), {
|
|
target: { value: 'last updated' },
|
|
})
|
|
|
|
expect(localStorage.getItem('TanstackQueryDevtools.mutationSort')).toBe(
|
|
'last updated',
|
|
)
|
|
})
|
|
|
|
it('should hide disabled queries when "hideDisabledQueries" is enabled in localStorage', () => {
|
|
const disabled = new QueryObserver(queryClient, {
|
|
queryKey: ['hide-test-disabled'],
|
|
queryFn: () => 'x',
|
|
enabled: false,
|
|
})
|
|
const unsubscribe = disabled.subscribe(() => {})
|
|
queryClient.setQueryData(['hide-test-disabled'], 'x')
|
|
queryClient.setQueryData(['hide-test-active'], 'y')
|
|
|
|
try {
|
|
const rendered = renderDevtools(
|
|
{ initialIsOpen: true },
|
|
{ 'TanstackQueryDevtools.hideDisabledQueries': 'true' },
|
|
)
|
|
|
|
expect(
|
|
rendered.queryByLabelText(/Query key \["hide-test-disabled"\]/),
|
|
).not.toBeInTheDocument()
|
|
expect(
|
|
rendered.getByLabelText(/Query key \["hide-test-active"\]/),
|
|
).toBeInTheDocument()
|
|
} finally {
|
|
unsubscribe()
|
|
}
|
|
})
|
|
})
|
|
|
|
describe('sort order', () => {
|
|
it('should toggle the sort order when the sort order button is clicked', () => {
|
|
const rendered = renderDevtools({ initialIsOpen: true })
|
|
|
|
fireEvent.click(rendered.getByLabelText(/Sort order/))
|
|
const afterFirstToggle = localStorage.getItem(
|
|
'TanstackQueryDevtools.sortOrder',
|
|
)
|
|
expect(['1', '-1']).toContain(afterFirstToggle)
|
|
|
|
fireEvent.click(rendered.getByLabelText(/Sort order/))
|
|
const afterSecondToggle = localStorage.getItem(
|
|
'TanstackQueryDevtools.sortOrder',
|
|
)
|
|
expect(afterSecondToggle).toBe(afterFirstToggle === '1' ? '-1' : '1')
|
|
})
|
|
})
|
|
|
|
describe('settings menu', () => {
|
|
it('should show "Position" sub-trigger when the settings menu is opened', () => {
|
|
const rendered = renderDevtools({ initialIsOpen: true })
|
|
|
|
fireEvent.keyDown(rendered.getByLabelText('Open settings menu'), {
|
|
key: 'Enter',
|
|
})
|
|
|
|
// The menu is rendered through a Portal mounted on `document.body`,
|
|
// outside `rendered.container`, so look it up via `document` directly.
|
|
expect(
|
|
document.querySelector('.tsqd-settings-menu-sub-trigger-position'),
|
|
).not.toBeNull()
|
|
})
|
|
|
|
it('should open "Position" sub-menu when the sub-trigger is activated', () => {
|
|
const rendered = renderDevtools({ initialIsOpen: true })
|
|
|
|
fireEvent.keyDown(rendered.getByLabelText('Open settings menu'), {
|
|
key: 'Enter',
|
|
})
|
|
|
|
const subTrigger = document.querySelector<HTMLElement>(
|
|
'.tsqd-settings-menu-sub-trigger-position',
|
|
)
|
|
expect(subTrigger).not.toBeNull()
|
|
fireEvent.keyDown(subTrigger!, { key: 'ArrowRight' })
|
|
|
|
expect(
|
|
document.querySelector('[aria-label="Position settings"]'),
|
|
).not.toBeNull()
|
|
})
|
|
|
|
it('should persist "position" when a position radio item is selected', () => {
|
|
const rendered = renderDevtools({ initialIsOpen: true })
|
|
|
|
fireEvent.keyDown(rendered.getByLabelText('Open settings menu'), {
|
|
key: 'Enter',
|
|
})
|
|
|
|
const subTrigger = document.querySelector<HTMLElement>(
|
|
'.tsqd-settings-menu-sub-trigger-position',
|
|
)
|
|
expect(subTrigger).not.toBeNull()
|
|
fireEvent.keyDown(subTrigger!, { key: 'ArrowRight' })
|
|
|
|
const topItem = document.querySelector<HTMLElement>(
|
|
'.tsqd-settings-menu-position-btn-top',
|
|
)
|
|
expect(topItem).not.toBeNull()
|
|
fireEvent.keyDown(topItem!, { key: 'Enter' })
|
|
|
|
expect(localStorage.getItem('TanstackQueryDevtools.position')).toBe('top')
|
|
})
|
|
|
|
it('should open "Theme" sub-menu when the sub-trigger is activated', () => {
|
|
const rendered = renderDevtools({ initialIsOpen: true })
|
|
|
|
fireEvent.keyDown(rendered.getByLabelText('Open settings menu'), {
|
|
key: 'Enter',
|
|
})
|
|
|
|
const themeTrigger = document.querySelector<HTMLElement>(
|
|
'.tsqd-settings-menu-sub-trigger-theme',
|
|
)
|
|
expect(themeTrigger).not.toBeNull()
|
|
expect(themeTrigger).not.toBe(
|
|
document.querySelector('.tsqd-settings-menu-sub-trigger-position'),
|
|
)
|
|
fireEvent.keyDown(themeTrigger!, { key: 'ArrowRight' })
|
|
|
|
expect(
|
|
document.querySelector('[aria-label="Theme preference"]'),
|
|
).not.toBeNull()
|
|
})
|
|
|
|
it('should persist "theme_preference" when a theme radio item is selected', () => {
|
|
const rendered = renderDevtools({ initialIsOpen: true })
|
|
|
|
fireEvent.keyDown(rendered.getByLabelText('Open settings menu'), {
|
|
key: 'Enter',
|
|
})
|
|
|
|
const themeTrigger = document.querySelector<HTMLElement>(
|
|
'.tsqd-settings-menu-sub-trigger-theme',
|
|
)
|
|
expect(themeTrigger).not.toBeNull()
|
|
fireEvent.keyDown(themeTrigger!, { key: 'ArrowRight' })
|
|
|
|
const themeMenu = document.querySelector(
|
|
'[aria-label="Theme preference"]',
|
|
)
|
|
const lightItem = Array.from(
|
|
themeMenu?.querySelectorAll<HTMLElement>('[role="menuitemradio"]') ??
|
|
[],
|
|
).find((el) => String(el.textContent).includes('Light'))
|
|
expect(lightItem).not.toBeUndefined()
|
|
fireEvent.keyDown(lightItem!, { key: 'Enter' })
|
|
|
|
expect(
|
|
localStorage.getItem('TanstackQueryDevtools.theme_preference'),
|
|
).toBe('light')
|
|
})
|
|
|
|
it('should open "Hide disabled queries" sub-menu when the sub-trigger is activated', () => {
|
|
const rendered = renderDevtools({ initialIsOpen: true })
|
|
|
|
fireEvent.keyDown(rendered.getByLabelText('Open settings menu'), {
|
|
key: 'Enter',
|
|
})
|
|
|
|
const hideTrigger = document.querySelector<HTMLElement>(
|
|
'.tsqd-settings-menu-sub-trigger-disabled-queries',
|
|
)
|
|
expect(hideTrigger).not.toBeNull()
|
|
fireEvent.keyDown(hideTrigger!, { key: 'ArrowRight' })
|
|
|
|
expect(
|
|
document.querySelector('[aria-label="Hide disabled queries setting"]'),
|
|
).not.toBeNull()
|
|
})
|
|
|
|
it('should persist "hideDisabledQueries" when a hide-disabled radio item is selected', () => {
|
|
const rendered = renderDevtools({ initialIsOpen: true })
|
|
|
|
fireEvent.keyDown(rendered.getByLabelText('Open settings menu'), {
|
|
key: 'Enter',
|
|
})
|
|
|
|
const hideTrigger = document.querySelector<HTMLElement>(
|
|
'.tsqd-settings-menu-sub-trigger-disabled-queries',
|
|
)
|
|
expect(hideTrigger).not.toBeNull()
|
|
fireEvent.keyDown(hideTrigger!, { key: 'ArrowRight' })
|
|
|
|
const hideItem = document.querySelector<HTMLElement>(
|
|
'.tsqd-settings-menu-position-btn-hide',
|
|
)
|
|
expect(hideItem).not.toBeNull()
|
|
fireEvent.keyDown(hideItem!, { key: 'Enter' })
|
|
|
|
expect(
|
|
localStorage.getItem('TanstackQueryDevtools.hideDisabledQueries'),
|
|
).toBe('true')
|
|
})
|
|
})
|
|
|
|
describe('resize handle', () => {
|
|
it('should increase height when "ArrowUp" is pressed on the resize handle in "bottom" position', () => {
|
|
const rendered = renderDevtools(
|
|
{ position: 'bottom', initialIsOpen: true },
|
|
{ 'TanstackQueryDevtools.height': '500' },
|
|
)
|
|
|
|
const handle = rendered.getByLabelText('Resize devtools panel')
|
|
fireEvent.keyDown(handle, { key: 'ArrowUp' })
|
|
|
|
expect(
|
|
Number(localStorage.getItem('TanstackQueryDevtools.height')),
|
|
).toBeGreaterThan(500)
|
|
})
|
|
|
|
it('should decrease height when "ArrowDown" is pressed on the resize handle in "bottom" position', () => {
|
|
const rendered = renderDevtools(
|
|
{ position: 'bottom', initialIsOpen: true },
|
|
{ 'TanstackQueryDevtools.height': '500' },
|
|
)
|
|
|
|
const handle = rendered.getByLabelText('Resize devtools panel')
|
|
fireEvent.keyDown(handle, { key: 'ArrowDown' })
|
|
|
|
// Assert the value exists before parsing — `Number(null)` is `0`,
|
|
// which would falsely satisfy `toBeLessThan(500)` if the write was missing.
|
|
const nextHeight = localStorage.getItem('TanstackQueryDevtools.height')
|
|
expect(nextHeight).not.toBeNull()
|
|
expect(Number(nextHeight)).toBeLessThan(500)
|
|
})
|
|
|
|
it('should increase width when "ArrowLeft" is pressed on the resize handle in "right" position', () => {
|
|
const rendered = renderDevtools(
|
|
{ position: 'right', initialIsOpen: true },
|
|
{ 'TanstackQueryDevtools.width': '500' },
|
|
)
|
|
|
|
const handle = rendered.getByLabelText('Resize devtools panel')
|
|
fireEvent.keyDown(handle, { key: 'ArrowLeft' })
|
|
|
|
expect(
|
|
Number(localStorage.getItem('TanstackQueryDevtools.width')),
|
|
).toBeGreaterThan(500)
|
|
})
|
|
|
|
it('should decrease width when "ArrowRight" is pressed on the resize handle in "right" position', () => {
|
|
const rendered = renderDevtools(
|
|
{ position: 'right', initialIsOpen: true },
|
|
{ 'TanstackQueryDevtools.width': '500' },
|
|
)
|
|
|
|
const handle = rendered.getByLabelText('Resize devtools panel')
|
|
fireEvent.keyDown(handle, { key: 'ArrowRight' })
|
|
|
|
const nextWidth = localStorage.getItem('TanstackQueryDevtools.width')
|
|
expect(nextWidth).not.toBeNull()
|
|
expect(Number(nextWidth)).toBeLessThan(500)
|
|
})
|
|
|
|
it('should increase height while dragging up in "bottom" position', () => {
|
|
const initialHeight = 500
|
|
const rendered = renderDevtools(
|
|
{ position: 'bottom', initialIsOpen: true },
|
|
{ 'TanstackQueryDevtools.height': String(initialHeight) },
|
|
)
|
|
|
|
const handle = rendered.getByLabelText('Resize devtools panel')
|
|
// jsdom returns zeros for `getBoundingClientRect`; stub the panel size so
|
|
// that drag math starts from `initialHeight` instead of 0.
|
|
// Only `height` is read by the production code; other fields are unused.
|
|
const panel = handle.parentElement
|
|
expect(panel).toBeInstanceOf(HTMLElement)
|
|
vi.spyOn(panel!, 'getBoundingClientRect').mockReturnValue({
|
|
height: initialHeight,
|
|
width: 0,
|
|
x: 0,
|
|
y: 0,
|
|
top: 0,
|
|
right: 0,
|
|
bottom: 0,
|
|
left: 0,
|
|
toJSON: () => ({}),
|
|
})
|
|
|
|
// Move the cursor up by 50px (`clientY` 100 → 50), which adds 50px to the
|
|
// drag base of `initialHeight`.
|
|
fireEvent.mouseDown(handle, { clientX: 0, clientY: 100 })
|
|
fireEvent(
|
|
document,
|
|
new MouseEvent('mousemove', { clientX: 0, clientY: 50 }),
|
|
)
|
|
fireEvent(document, new MouseEvent('mouseup'))
|
|
|
|
expect(
|
|
Number(localStorage.getItem('TanstackQueryDevtools.height')),
|
|
).toBeGreaterThan(initialHeight)
|
|
})
|
|
|
|
it('should increase width while dragging left in "right" position', () => {
|
|
const initialWidth = 500
|
|
const rendered = renderDevtools(
|
|
{ position: 'right', initialIsOpen: true },
|
|
{ 'TanstackQueryDevtools.width': String(initialWidth) },
|
|
)
|
|
|
|
const handle = rendered.getByLabelText('Resize devtools panel')
|
|
// `width` is read twice during drag: once as the base size, and again to
|
|
// detect when the panel hits its minimum. Returning the same value both
|
|
// times keeps the "minimum reached" branch from firing.
|
|
const panel = handle.parentElement
|
|
expect(panel).toBeInstanceOf(HTMLElement)
|
|
vi.spyOn(panel!, 'getBoundingClientRect').mockReturnValue({
|
|
height: 0,
|
|
width: initialWidth,
|
|
x: 0,
|
|
y: 0,
|
|
top: 0,
|
|
right: 0,
|
|
bottom: 0,
|
|
left: 0,
|
|
toJSON: () => ({}),
|
|
})
|
|
|
|
// Move the cursor left by 50px (`clientX` 100 → 50); in `right` position,
|
|
// moving left grows the panel by 50px from the drag base of `initialWidth`.
|
|
fireEvent.mouseDown(handle, { clientX: 100, clientY: 0 })
|
|
fireEvent(
|
|
document,
|
|
new MouseEvent('mousemove', { clientX: 50, clientY: 0 }),
|
|
)
|
|
fireEvent(document, new MouseEvent('mouseup'))
|
|
|
|
expect(
|
|
Number(localStorage.getItem('TanstackQueryDevtools.width')),
|
|
).toBeGreaterThan(initialWidth)
|
|
})
|
|
|
|
it('should clamp the width to the minimum when dragging shrinks the panel below the minimum width', () => {
|
|
const initialWidth = 200
|
|
const rendered = renderDevtools(
|
|
{ position: 'left', initialIsOpen: true },
|
|
{ 'TanstackQueryDevtools.width': String(initialWidth) },
|
|
)
|
|
|
|
const handle = rendered.getByLabelText('Resize devtools panel')
|
|
const panel = handle.parentElement
|
|
expect(panel).toBeInstanceOf(HTMLElement)
|
|
// `width` is read twice during drag: once as the base size, and again
|
|
// after the clamp to detect when the panel has hit its minimum. The
|
|
// first call returns `initialWidth`; the second returns `0` so the
|
|
// `localStore.width < newWidth` restore branch stays inactive and only
|
|
// the `newSize < minWidth` clamp is observed.
|
|
const getBoundingClientRect = vi
|
|
.spyOn(panel!, 'getBoundingClientRect')
|
|
.mockReturnValueOnce({
|
|
height: 0,
|
|
width: initialWidth,
|
|
x: 0,
|
|
y: 0,
|
|
top: 0,
|
|
right: 0,
|
|
bottom: 0,
|
|
left: 0,
|
|
toJSON: () => ({}),
|
|
})
|
|
getBoundingClientRect.mockReturnValue({
|
|
height: 0,
|
|
width: 0,
|
|
x: 0,
|
|
y: 0,
|
|
top: 0,
|
|
right: 0,
|
|
bottom: 0,
|
|
left: 0,
|
|
toJSON: () => ({}),
|
|
})
|
|
|
|
// In `left` position, dragging the cursor left (`clientX` 100 → 0)
|
|
// shrinks the panel by 100px, which lands well under the 192px minimum.
|
|
fireEvent.mouseDown(handle, { clientX: 100, clientY: 0 })
|
|
fireEvent(
|
|
document,
|
|
new MouseEvent('mousemove', { clientX: 0, clientY: 0 }),
|
|
)
|
|
fireEvent(document, new MouseEvent('mouseup'))
|
|
|
|
expect(Number(localStorage.getItem('TanstackQueryDevtools.width'))).toBe(
|
|
192,
|
|
)
|
|
})
|
|
|
|
it('should restore "width" to the rendered minimum when the panel is dragged below its content width', () => {
|
|
const initialWidth = 400
|
|
// The panel uses `min-width: min-content`, so the rendered width may
|
|
// exceed the 192px `minWidth` constant when its children are wider; any
|
|
// value > 192 here triggers the `localStore.width < newWidth` restore
|
|
// branch.
|
|
const renderedMinWidth = 250
|
|
const rendered = renderDevtools(
|
|
{ position: 'left', initialIsOpen: true },
|
|
{ 'TanstackQueryDevtools.width': String(initialWidth) },
|
|
)
|
|
|
|
const handle = rendered.getByLabelText('Resize devtools panel')
|
|
const panel = handle.parentElement
|
|
expect(panel).toBeInstanceOf(HTMLElement)
|
|
const getBoundingClientRect = vi
|
|
.spyOn(panel!, 'getBoundingClientRect')
|
|
.mockReturnValueOnce({
|
|
height: 0,
|
|
width: initialWidth,
|
|
x: 0,
|
|
y: 0,
|
|
top: 0,
|
|
right: 0,
|
|
bottom: 0,
|
|
left: 0,
|
|
toJSON: () => ({}),
|
|
})
|
|
getBoundingClientRect.mockReturnValue({
|
|
height: 0,
|
|
width: renderedMinWidth,
|
|
x: 0,
|
|
y: 0,
|
|
top: 0,
|
|
right: 0,
|
|
bottom: 0,
|
|
left: 0,
|
|
toJSON: () => ({}),
|
|
})
|
|
|
|
// Drag 300px left shrinks the panel to 100 — below `minWidth`, so the
|
|
// clamp + restore branches both fire.
|
|
fireEvent.mouseDown(handle, { clientX: 300, clientY: 0 })
|
|
fireEvent(
|
|
document,
|
|
new MouseEvent('mousemove', { clientX: 0, clientY: 0 }),
|
|
)
|
|
fireEvent(document, new MouseEvent('mouseup'))
|
|
|
|
expect(Number(localStorage.getItem('TanstackQueryDevtools.width'))).toBe(
|
|
renderedMinWidth,
|
|
)
|
|
})
|
|
|
|
it('should close the query details panel when dragging shrinks the panel below the minimum height', () => {
|
|
queryClient.setQueryData(['shrink-below-min-height'], [{ id: 1 }])
|
|
const rendered = renderDevtools({
|
|
position: 'bottom',
|
|
initialIsOpen: true,
|
|
})
|
|
|
|
// Open the query details so `selectedQueryHash` is set.
|
|
fireEvent.click(
|
|
rendered.getByLabelText(/Query key \["shrink-below-min-height"\]/),
|
|
)
|
|
expect(rendered.getByText('Query Details')).toBeInTheDocument()
|
|
|
|
const handle = rendered.getByLabelText('Resize devtools panel')
|
|
const panel = handle.parentElement
|
|
expect(panel).toBeInstanceOf(HTMLElement)
|
|
// Stub the base size to a value just above the 56px (`3.5rem`) minimum so
|
|
// a small downward drag pushes `newSize` below `minHeight` and triggers
|
|
// the clamp branch that also resets `selectedQueryHash`.
|
|
vi.spyOn(panel!, 'getBoundingClientRect').mockReturnValue({
|
|
height: 60,
|
|
width: 0,
|
|
x: 0,
|
|
y: 0,
|
|
top: 0,
|
|
right: 0,
|
|
bottom: 0,
|
|
left: 0,
|
|
toJSON: () => ({}),
|
|
})
|
|
|
|
// In `bottom` position, dragging the cursor down (`clientY` 100 → 200)
|
|
// shrinks the panel by 100px, which is well under the 56px minimum.
|
|
fireEvent.mouseDown(handle, { clientX: 0, clientY: 100 })
|
|
fireEvent(
|
|
document,
|
|
new MouseEvent('mousemove', { clientX: 0, clientY: 200 }),
|
|
)
|
|
fireEvent(document, new MouseEvent('mouseup'))
|
|
|
|
expect(rendered.queryByText('Query Details')).not.toBeInTheDocument()
|
|
})
|
|
})
|
|
|
|
describe('online toggle', () => {
|
|
it('should swap the toggle label after the offline button is clicked', () => {
|
|
const rendered = renderDevtools({ initialIsOpen: true })
|
|
|
|
fireEvent.click(rendered.getByLabelText('Mock offline behavior'))
|
|
|
|
expect(
|
|
rendered.getByLabelText('Unset offline mocking behavior'),
|
|
).toBeInTheDocument()
|
|
})
|
|
})
|
|
|
|
describe('logo close', () => {
|
|
it('should hide the panel when the TanStack logo is clicked', () => {
|
|
const rendered = renderDevtools({ initialIsOpen: true })
|
|
|
|
fireEvent.click(rendered.getByLabelText('Close Tanstack query devtools'))
|
|
|
|
expect(
|
|
rendered.queryByLabelText('Tanstack query devtools'),
|
|
).not.toBeInTheDocument()
|
|
})
|
|
})
|
|
})
|