Skip to content

Commit a52fbd7

Browse files
committed
fix(devtools ui): fix json tree nan values
1 parent 58e66f5 commit a52fbd7

4 files changed

Lines changed: 247 additions & 1 deletion

File tree

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@tanstack/devtools-ui': patch
3+
---
4+
5+
Fix `NaN` rendering in `JsonTree`, previously rendered null, now correctly displays `NaN`

packages/devtools-ui/src/components/tree.tsx

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,9 @@ function JsonValue(props: {
7171
}
7272

7373
if (typeof props.value === 'number') {
74-
return <span class={styles().tree.valueNumber}>{props.value}</span>
74+
return (
75+
<span class={styles().tree.valueNumber}>{String(props.value)}</span>
76+
)
7577
}
7678

7779
if (typeof props.value === 'boolean') {
Lines changed: 237 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,237 @@
1+
/** @jsxImportSource solid-js */
2+
import { afterEach, describe, expect, it } from 'vitest'
3+
import { render } from 'solid-js/web'
4+
import { JsonTree } from '../src/components/tree'
5+
import { ThemeContextProvider } from '../src/components/theme'
6+
7+
// types
8+
import type { CollapsiblePaths } from '../src/utils/deep-keys'
9+
10+
let container: HTMLDivElement
11+
let dispose: () => void
12+
13+
function renderTree<TData, TName extends CollapsiblePaths<TData>>(
14+
value: any,
15+
extraProps: {
16+
defaultExpansionDepth?: number
17+
collapsePaths?: Array<TName>
18+
config?: { dateFormat?: string }
19+
copyable?: boolean
20+
} = {},
21+
) {
22+
container = document.createElement('div')
23+
document.body.appendChild(container)
24+
dispose = render(
25+
() => (
26+
<ThemeContextProvider theme="dark">
27+
<JsonTree value={value} {...extraProps} />
28+
</ThemeContextProvider>
29+
),
30+
container,
31+
)
32+
return container
33+
}
34+
35+
afterEach(() => {
36+
dispose()
37+
container.remove()
38+
})
39+
40+
describe('JsonTree', () => {
41+
describe('string', () => {
42+
it('renders a string value wrapped in quotes', () => {
43+
const el = renderTree('hello world')
44+
expect(el.textContent).toContain('"hello world"')
45+
})
46+
47+
it('renders an empty string as ""', () => {
48+
const el = renderTree('')
49+
expect(el.textContent).toContain('""')
50+
})
51+
52+
it('renders a string with special characters', () => {
53+
const el = renderTree('foo & <bar>')
54+
expect(el.textContent).toContain('foo & <bar>')
55+
})
56+
})
57+
58+
describe('number', () => {
59+
it('renders a positive integer', () => {
60+
const el = renderTree(42)
61+
expect(el.textContent).toContain('42')
62+
})
63+
64+
it('renders a negative number', () => {
65+
const el = renderTree(-7.5)
66+
expect(el.textContent).toContain('-7.5')
67+
})
68+
69+
it('renders zero', () => {
70+
const el = renderTree(0)
71+
expect(el.textContent).toContain('0')
72+
})
73+
74+
it('renders NaN as "NaN"', () => {
75+
const el = renderTree(NaN)
76+
expect(el.textContent).toContain('NaN')
77+
})
78+
})
79+
80+
describe('boolean', () => {
81+
it('renders true as the string "true"', () => {
82+
const el = renderTree(true)
83+
expect(el.textContent).toContain('true')
84+
})
85+
86+
it('renders false as the string "false"', () => {
87+
const el = renderTree(false)
88+
expect(el.textContent).toContain('false')
89+
})
90+
})
91+
92+
describe('null', () => {
93+
it('renders null as the string "null"', () => {
94+
const el = renderTree(null)
95+
expect(el.textContent).toContain('null')
96+
})
97+
})
98+
99+
describe('undefined', () => {
100+
it('renders undefined as the string "undefined"', () => {
101+
const el = renderTree(undefined)
102+
expect(el.textContent).toContain('undefined')
103+
})
104+
})
105+
106+
describe('function', () => {
107+
it('renders a named function as its string representation', () => {
108+
function myFunc() {
109+
return 1
110+
}
111+
const el = renderTree(myFunc)
112+
expect(el.textContent).toContain('myFunc')
113+
})
114+
115+
it('renders an arrow function as its string representation', () => {
116+
const arrow = () => 'result'
117+
const el = renderTree(arrow)
118+
expect(el.textContent).toContain('=>')
119+
})
120+
})
121+
122+
describe('array', () => {
123+
it('renders an empty array as []', () => {
124+
const el = renderTree([])
125+
expect(el.textContent).toContain('[]')
126+
})
127+
128+
it('renders an expanded array with its items visible', () => {
129+
const el = renderTree([1, 2, 3])
130+
expect(el.textContent).toContain('[')
131+
expect(el.textContent).toContain(']')
132+
expect(el.textContent).toContain('1')
133+
expect(el.textContent).toContain('2')
134+
expect(el.textContent).toContain('3')
135+
})
136+
137+
it('renders an array of strings with quoted values', () => {
138+
const el = renderTree(['alpha', 'beta'])
139+
expect(el.textContent).toContain('"alpha"')
140+
expect(el.textContent).toContain('"beta"')
141+
})
142+
143+
it('shows item count when array is nested inside an object', () => {
144+
const el = renderTree({ list: [1, 2, 3] })
145+
expect(el.textContent).toContain('3 items')
146+
})
147+
148+
it('renders a mixed-type array', () => {
149+
const el = renderTree([1, 'two', true, null])
150+
expect(el.textContent).toContain('1')
151+
expect(el.textContent).toContain('"two"')
152+
expect(el.textContent).toContain('true')
153+
expect(el.textContent).toContain('null')
154+
})
155+
})
156+
157+
describe('object', () => {
158+
it('renders an empty object as {}', () => {
159+
const el = renderTree({})
160+
expect(el.textContent).toContain('{}')
161+
})
162+
163+
it('renders object keys and their values', () => {
164+
const el = renderTree({ name: 'Alice', age: 30 })
165+
expect(el.textContent).toContain('"name"')
166+
expect(el.textContent).toContain('"Alice"')
167+
expect(el.textContent).toContain('"age"')
168+
expect(el.textContent).toContain('30')
169+
})
170+
171+
it('renders nested objects when within expansion depth', () => {
172+
const el = renderTree({ a: { b: 'deep' } }, { defaultExpansionDepth: 2 })
173+
expect(el.textContent).toContain('"a"')
174+
expect(el.textContent).toContain('"b"')
175+
expect(el.textContent).toContain('"deep"')
176+
})
177+
178+
it('shows item count for nested objects', () => {
179+
const el = renderTree({ meta: { x: 1, y: 2 } })
180+
expect(el.textContent).toContain('2 items')
181+
})
182+
})
183+
184+
describe('Date', () => {
185+
it('renders a Date with the default DDMMMYY format', () => {
186+
const date = new Date('2024-01-15T12:00:00Z')
187+
const el = renderTree(date)
188+
expect(el.textContent).toContain('Jan')
189+
})
190+
191+
it('renders a Date with a custom dateFormat', () => {
192+
const date = new Date('2024-06-20T00:00:00Z')
193+
const el = renderTree(date, { config: { dateFormat: 'YYYY-MM-DD' } })
194+
expect(el.textContent).toContain('2024-06-20')
195+
})
196+
})
197+
198+
describe('expansion depth', () => {
199+
it('collapses deeply nested objects beyond defaultExpansionDepth', () => {
200+
const el = renderTree(
201+
{ a: { b: { c: 'deep' } } },
202+
{ defaultExpansionDepth: 1 },
203+
)
204+
expect(el.textContent).not.toContain('"c"')
205+
expect(el.textContent).not.toContain('"deep"')
206+
})
207+
208+
it('expands all levels within defaultExpansionDepth', () => {
209+
const el = renderTree({ a: { b: 'value' } }, { defaultExpansionDepth: 2 })
210+
expect(el.textContent).toContain('"value"')
211+
})
212+
213+
it('collapses paths listed in collapsePaths', () => {
214+
const el = renderTree(
215+
{ user: { name: 'Bob', address: { city: 'NY' } } },
216+
{ defaultExpansionDepth: 3, collapsePaths: ['user.address'] as any },
217+
)
218+
expect(el.textContent).toContain('"name"')
219+
expect(el.textContent).not.toContain('"city"')
220+
})
221+
})
222+
223+
describe('key rendering', () => {
224+
it('renders quoted key names for primitive values', () => {
225+
const el = renderTree({ count: 5, label: 'test' })
226+
expect(el.textContent).toContain('"count"')
227+
expect(el.textContent).toContain('"label"')
228+
})
229+
230+
it('renders array brackets around array values', () => {
231+
const el = renderTree([10, 20])
232+
const text = String(el.textContent)
233+
expect(text.indexOf('[')).toBeLessThan(text.indexOf('10'))
234+
expect(text.indexOf('10')).toBeLessThan(text.indexOf(']'))
235+
})
236+
})
237+
})

packages/devtools-ui/vite.config.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@ const config = defineConfig({
99
test: {
1010
name: packageJson.name,
1111
dir: './',
12+
include: ['tests/**/*.{ts,tsx}'],
13+
exclude: ['tests/test-setup.ts', '**/node_modules/**'],
1214
watch: false,
1315
environment: 'jsdom',
1416
setupFiles: ['./tests/test-setup.ts'],

0 commit comments

Comments
 (0)