Skip to content

Commit e49bb58

Browse files
committed
test(component): React adapter portal wiring test
1 parent b5ce00d commit e49bb58

2 files changed

Lines changed: 103 additions & 1 deletion

File tree

packages/react-devtools/package.json

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,11 +55,14 @@
5555
},
5656
"devDependencies": {
5757
"@eslint-react/eslint-plugin": "^1.53.1",
58+
"@testing-library/react": "^16.3.0",
5859
"@types/react": "^19.2.0",
60+
"@types/react-dom": "^19.2.0",
5961
"@vitejs/plugin-react": "^6.0.1",
6062
"eslint-plugin-react-compiler": "19.1.0-rc.2",
6163
"eslint-plugin-react-hooks": "^7.0.1",
62-
"react": "^19.2.0"
64+
"react": "^19.2.0",
65+
"react-dom": "^19.2.0"
6366
},
6467
"peerDependencies": {
6568
"@types/react": ">=16.8",
Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
import '@testing-library/jest-dom/vitest'
2+
import React from 'react'
3+
import { act, render } from '@testing-library/react'
4+
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
5+
import { TanStackDevtoolsCore } from '@tanstack/devtools'
6+
import { TanStackDevtools } from '../src/devtools'
7+
import type {
8+
TanStackDevtoolsPlugin,
9+
TanStackDevtoolsPluginProps,
10+
} from '@tanstack/devtools'
11+
12+
// The real core does a full Solid DOM mount which is heavy in jsdom. Mock it
13+
// down to the methods the React adapter actually calls (constructor + mount +
14+
// unmount + setConfig), so we can drive the portal wiring in isolation.
15+
vi.mock('@tanstack/devtools', () => ({
16+
TanStackDevtoolsCore: vi.fn().mockImplementation(() => ({
17+
mount: vi.fn(),
18+
unmount: vi.fn(),
19+
setConfig: vi.fn(),
20+
})),
21+
}))
22+
23+
const CoreMock = vi.mocked(TanStackDevtoolsCore)
24+
25+
describe('TanStackDevtools (React adapter)', () => {
26+
beforeEach(() => {
27+
CoreMock.mockClear()
28+
})
29+
30+
afterEach(() => {
31+
document.querySelectorAll('#p').forEach((el) => el.remove())
32+
})
33+
34+
it('constructs the core, mounts into the ref div, portals the plugin element, and unmounts', () => {
35+
let unmount: () => void = () => {}
36+
37+
act(() => {
38+
const result = render(
39+
<TanStackDevtools
40+
plugins={[{ id: 'p', name: 'P', render: <div data-testid="x" /> }]}
41+
/>,
42+
)
43+
unmount = result.unmount
44+
})
45+
46+
// Constructed exactly once.
47+
expect(CoreMock).toHaveBeenCalledTimes(1)
48+
49+
const coreInstance = CoreMock.mock.results[0]!.value as {
50+
mount: ReturnType<typeof vi.fn>
51+
unmount: ReturnType<typeof vi.fn>
52+
setConfig: ReturnType<typeof vi.fn>
53+
}
54+
55+
// mount() was called with the ref div the adapter renders.
56+
expect(coreInstance.mount).toHaveBeenCalledTimes(1)
57+
const mountTarget = coreInstance.mount.mock.calls[0]![0] as HTMLElement
58+
expect(mountTarget).toBeInstanceOf(HTMLDivElement)
59+
60+
// Grab the plugins the adapter passed to the core constructor.
61+
const ctorArg = CoreMock.mock.calls[0]![0] as {
62+
plugins: Array<TanStackDevtoolsPlugin>
63+
}
64+
const corePlugin = ctorArg.plugins[0]!
65+
expect(corePlugin.id).toBe('p')
66+
67+
// The container is only registered if the element is findable by id in the
68+
// owner document (adapter does `e.ownerDocument.getElementById(id)`), so the
69+
// fake element must carry id='p' and live in the document.
70+
const fakeEl = document.createElement('div')
71+
fakeEl.setAttribute('id', 'p')
72+
document.body.appendChild(fakeEl)
73+
74+
const props: TanStackDevtoolsPluginProps = {
75+
theme: 'dark',
76+
devtoolsOpen: true,
77+
}
78+
79+
// Drive the adapter's render callback: convertRender -> setPluginContainers
80+
// + setPluginComponents, which then portals the React element into fakeEl.
81+
act(() => {
82+
;(corePlugin.render as (el: HTMLElement, props: any) => void)(
83+
fakeEl,
84+
props,
85+
)
86+
})
87+
88+
// The plugin React element is portaled into the core-provided container.
89+
expect(fakeEl.querySelector('[data-testid="x"]')).toBeInTheDocument()
90+
91+
// Unmounting tears down the core and throws nothing.
92+
expect(() => {
93+
act(() => {
94+
unmount()
95+
})
96+
}).not.toThrow()
97+
expect(coreInstance.unmount).toHaveBeenCalledTimes(1)
98+
})
99+
})

0 commit comments

Comments
 (0)