diff --git a/packages/query-devtools/package.json b/packages/query-devtools/package.json
index a848954962..87caaf1206 100644
--- a/packages/query-devtools/package.json
+++ b/packages/query-devtools/package.json
@@ -67,6 +67,7 @@
"@solid-primitives/keyed": "^1.2.2",
"@solid-primitives/resize-observer": "^2.0.26",
"@solid-primitives/storage": "^1.3.11",
+ "@solidjs/testing-library": "^0.8.10",
"@tanstack/match-sorter-utils": "^8.19.4",
"@tanstack/query-core": "workspace:*",
"clsx": "^2.1.1",
diff --git a/packages/query-devtools/src/__tests__/DevtoolsComponent.test.tsx b/packages/query-devtools/src/__tests__/DevtoolsComponent.test.tsx
new file mode 100644
index 0000000000..b3f7ee5d9e
--- /dev/null
+++ b/packages/query-devtools/src/__tests__/DevtoolsComponent.test.tsx
@@ -0,0 +1,76 @@
+import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
+import { QueryClient, onlineManager } from '@tanstack/query-core'
+import { render } from '@solidjs/testing-library'
+import DevtoolsComponent from '../DevtoolsComponent'
+
+// `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,
+}))
+
+describe('DevtoolsComponent', () => {
+ const storage: { [key: string]: string } = {}
+ let queryClient: QueryClient
+
+ beforeEach(() => {
+ 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 {
+ observe = vi.fn()
+ unobserve = vi.fn()
+ disconnect = vi.fn()
+ },
+ )
+ queryClient = new QueryClient()
+ })
+
+ afterEach(() => {
+ vi.unstubAllGlobals()
+ Object.keys(storage).forEach((key) => delete storage[key])
+ queryClient.clear()
+ })
+
+ it('should render without throwing', () => {
+ expect(() =>
+ render(() => (
+
+ )),
+ ).not.toThrow()
+ })
+})
diff --git a/packages/query-devtools/src/__tests__/DevtoolsPanelComponent.test.tsx b/packages/query-devtools/src/__tests__/DevtoolsPanelComponent.test.tsx
new file mode 100644
index 0000000000..1cb7c4d461
--- /dev/null
+++ b/packages/query-devtools/src/__tests__/DevtoolsPanelComponent.test.tsx
@@ -0,0 +1,124 @@
+import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
+import { QueryClient, onlineManager } from '@tanstack/query-core'
+import { render } from '@solidjs/testing-library'
+import DevtoolsPanelComponent from '../DevtoolsPanelComponent'
+
+// `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('DevtoolsPanelComponent', () => {
+ const storage: { [key: string]: string } = {}
+ let queryClient: QueryClient
+ let previousRootFontSize = ''
+
+ beforeEach(() => {
+ 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 {
+ observe = vi.fn()
+ 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()
+ document.documentElement.style.fontSize = previousRootFontSize
+ })
+
+ it('should render the panel without throwing', () => {
+ expect(() =>
+ render(() => (
+
+ )),
+ ).not.toThrow()
+ })
+
+ it('should not render the open devtools button in panel-only mode', () => {
+ const rendered = render(() => (
+
+ ))
+
+ expect(
+ rendered.queryByLabelText('Open Tanstack query devtools'),
+ ).not.toBeInTheDocument()
+ })
+
+ it('should call "onClose" when the close button is clicked', () => {
+ const onClose = vi.fn()
+ const rendered = render(() => (
+
+ ))
+
+ rendered.getByLabelText('Close Tanstack query devtools').click()
+
+ expect(onClose).toHaveBeenCalledTimes(1)
+ })
+})
diff --git a/packages/query-devtools/test-setup.ts b/packages/query-devtools/test-setup.ts
new file mode 100644
index 0000000000..fb70ad9f20
--- /dev/null
+++ b/packages/query-devtools/test-setup.ts
@@ -0,0 +1,5 @@
+import '@testing-library/jest-dom/vitest'
+import { cleanup } from '@solidjs/testing-library'
+import { afterEach } from 'vitest'
+
+afterEach(() => cleanup())
diff --git a/packages/query-devtools/tsconfig.json b/packages/query-devtools/tsconfig.json
index c9589abaae..7f3c629b17 100644
--- a/packages/query-devtools/tsconfig.json
+++ b/packages/query-devtools/tsconfig.json
@@ -6,6 +6,12 @@
"jsx": "preserve",
"jsxImportSource": "solid-js"
},
- "include": ["src", "*.config.ts", "*.config.js", "package.json"],
+ "include": [
+ "src",
+ "test-setup.ts",
+ "*.config.ts",
+ "*.config.js",
+ "package.json"
+ ],
"references": [{ "path": "../query-core" }]
}
diff --git a/packages/query-devtools/vite.config.ts b/packages/query-devtools/vite.config.ts
index cf53690321..9678f07069 100644
--- a/packages/query-devtools/vite.config.ts
+++ b/packages/query-devtools/vite.config.ts
@@ -29,5 +29,6 @@ export default defineConfig({
},
typecheck: { enabled: true },
restoreMocks: true,
+ setupFiles: ['test-setup.ts'],
},
})
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index a19f7ce4b9..8a8c9a73d6 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -2730,6 +2730,9 @@ importers:
'@solid-primitives/storage':
specifier: ^1.3.11
version: 1.3.11(solid-js@1.9.12)
+ '@solidjs/testing-library':
+ specifier: ^0.8.10
+ version: 0.8.10(@solidjs/router@0.15.4(solid-js@1.9.12))(solid-js@1.9.12)
'@tanstack/match-sorter-utils':
specifier: ^8.19.4
version: 8.19.4