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)
+ })
+})