From a52fbd79a1041ecbdad58d85b3c44c62e481ce3f Mon Sep 17 00:00:00 2001 From: Harry Whorlow Date: Mon, 11 May 2026 12:16:08 +0200 Subject: [PATCH] fix(devtools ui): fix json tree nan values --- .changeset/fix-nan-tree-rendering.md | 5 + packages/devtools-ui/src/components/tree.tsx | 4 +- packages/devtools-ui/tests/tree.tsx | 237 +++++++++++++++++++ packages/devtools-ui/vite.config.ts | 2 + 4 files changed, 247 insertions(+), 1 deletion(-) create mode 100644 .changeset/fix-nan-tree-rendering.md create mode 100644 packages/devtools-ui/tests/tree.tsx diff --git a/.changeset/fix-nan-tree-rendering.md b/.changeset/fix-nan-tree-rendering.md new file mode 100644 index 00000000..e4d79a01 --- /dev/null +++ b/.changeset/fix-nan-tree-rendering.md @@ -0,0 +1,5 @@ +--- +'@tanstack/devtools-ui': patch +--- + +Fix `NaN` rendering in `JsonTree`, previously rendered null, now correctly displays `NaN` diff --git a/packages/devtools-ui/src/components/tree.tsx b/packages/devtools-ui/src/components/tree.tsx index bcbe7870..225b34a8 100644 --- a/packages/devtools-ui/src/components/tree.tsx +++ b/packages/devtools-ui/src/components/tree.tsx @@ -71,7 +71,9 @@ function JsonValue(props: { } if (typeof props.value === 'number') { - return {props.value} + return ( + {String(props.value)} + ) } if (typeof props.value === 'boolean') { diff --git a/packages/devtools-ui/tests/tree.tsx b/packages/devtools-ui/tests/tree.tsx new file mode 100644 index 00000000..7a7e14c0 --- /dev/null +++ b/packages/devtools-ui/tests/tree.tsx @@ -0,0 +1,237 @@ +/** @jsxImportSource solid-js */ +import { afterEach, describe, expect, it } from 'vitest' +import { render } from 'solid-js/web' +import { JsonTree } from '../src/components/tree' +import { ThemeContextProvider } from '../src/components/theme' + +// types +import type { CollapsiblePaths } from '../src/utils/deep-keys' + +let container: HTMLDivElement +let dispose: () => void + +function renderTree>( + value: any, + extraProps: { + defaultExpansionDepth?: number + collapsePaths?: Array + config?: { dateFormat?: string } + copyable?: boolean + } = {}, +) { + container = document.createElement('div') + document.body.appendChild(container) + dispose = render( + () => ( + + + + ), + container, + ) + return container +} + +afterEach(() => { + dispose() + container.remove() +}) + +describe('JsonTree', () => { + describe('string', () => { + it('renders a string value wrapped in quotes', () => { + const el = renderTree('hello world') + expect(el.textContent).toContain('"hello world"') + }) + + it('renders an empty string as ""', () => { + const el = renderTree('') + expect(el.textContent).toContain('""') + }) + + it('renders a string with special characters', () => { + const el = renderTree('foo & ') + expect(el.textContent).toContain('foo & ') + }) + }) + + describe('number', () => { + it('renders a positive integer', () => { + const el = renderTree(42) + expect(el.textContent).toContain('42') + }) + + it('renders a negative number', () => { + const el = renderTree(-7.5) + expect(el.textContent).toContain('-7.5') + }) + + it('renders zero', () => { + const el = renderTree(0) + expect(el.textContent).toContain('0') + }) + + it('renders NaN as "NaN"', () => { + const el = renderTree(NaN) + expect(el.textContent).toContain('NaN') + }) + }) + + describe('boolean', () => { + it('renders true as the string "true"', () => { + const el = renderTree(true) + expect(el.textContent).toContain('true') + }) + + it('renders false as the string "false"', () => { + const el = renderTree(false) + expect(el.textContent).toContain('false') + }) + }) + + describe('null', () => { + it('renders null as the string "null"', () => { + const el = renderTree(null) + expect(el.textContent).toContain('null') + }) + }) + + describe('undefined', () => { + it('renders undefined as the string "undefined"', () => { + const el = renderTree(undefined) + expect(el.textContent).toContain('undefined') + }) + }) + + describe('function', () => { + it('renders a named function as its string representation', () => { + function myFunc() { + return 1 + } + const el = renderTree(myFunc) + expect(el.textContent).toContain('myFunc') + }) + + it('renders an arrow function as its string representation', () => { + const arrow = () => 'result' + const el = renderTree(arrow) + expect(el.textContent).toContain('=>') + }) + }) + + describe('array', () => { + it('renders an empty array as []', () => { + const el = renderTree([]) + expect(el.textContent).toContain('[]') + }) + + it('renders an expanded array with its items visible', () => { + const el = renderTree([1, 2, 3]) + expect(el.textContent).toContain('[') + expect(el.textContent).toContain(']') + expect(el.textContent).toContain('1') + expect(el.textContent).toContain('2') + expect(el.textContent).toContain('3') + }) + + it('renders an array of strings with quoted values', () => { + const el = renderTree(['alpha', 'beta']) + expect(el.textContent).toContain('"alpha"') + expect(el.textContent).toContain('"beta"') + }) + + it('shows item count when array is nested inside an object', () => { + const el = renderTree({ list: [1, 2, 3] }) + expect(el.textContent).toContain('3 items') + }) + + it('renders a mixed-type array', () => { + const el = renderTree([1, 'two', true, null]) + expect(el.textContent).toContain('1') + expect(el.textContent).toContain('"two"') + expect(el.textContent).toContain('true') + expect(el.textContent).toContain('null') + }) + }) + + describe('object', () => { + it('renders an empty object as {}', () => { + const el = renderTree({}) + expect(el.textContent).toContain('{}') + }) + + it('renders object keys and their values', () => { + const el = renderTree({ name: 'Alice', age: 30 }) + expect(el.textContent).toContain('"name"') + expect(el.textContent).toContain('"Alice"') + expect(el.textContent).toContain('"age"') + expect(el.textContent).toContain('30') + }) + + it('renders nested objects when within expansion depth', () => { + const el = renderTree({ a: { b: 'deep' } }, { defaultExpansionDepth: 2 }) + expect(el.textContent).toContain('"a"') + expect(el.textContent).toContain('"b"') + expect(el.textContent).toContain('"deep"') + }) + + it('shows item count for nested objects', () => { + const el = renderTree({ meta: { x: 1, y: 2 } }) + expect(el.textContent).toContain('2 items') + }) + }) + + describe('Date', () => { + it('renders a Date with the default DDMMMYY format', () => { + const date = new Date('2024-01-15T12:00:00Z') + const el = renderTree(date) + expect(el.textContent).toContain('Jan') + }) + + it('renders a Date with a custom dateFormat', () => { + const date = new Date('2024-06-20T00:00:00Z') + const el = renderTree(date, { config: { dateFormat: 'YYYY-MM-DD' } }) + expect(el.textContent).toContain('2024-06-20') + }) + }) + + describe('expansion depth', () => { + it('collapses deeply nested objects beyond defaultExpansionDepth', () => { + const el = renderTree( + { a: { b: { c: 'deep' } } }, + { defaultExpansionDepth: 1 }, + ) + expect(el.textContent).not.toContain('"c"') + expect(el.textContent).not.toContain('"deep"') + }) + + it('expands all levels within defaultExpansionDepth', () => { + const el = renderTree({ a: { b: 'value' } }, { defaultExpansionDepth: 2 }) + expect(el.textContent).toContain('"value"') + }) + + it('collapses paths listed in collapsePaths', () => { + const el = renderTree( + { user: { name: 'Bob', address: { city: 'NY' } } }, + { defaultExpansionDepth: 3, collapsePaths: ['user.address'] as any }, + ) + expect(el.textContent).toContain('"name"') + expect(el.textContent).not.toContain('"city"') + }) + }) + + describe('key rendering', () => { + it('renders quoted key names for primitive values', () => { + const el = renderTree({ count: 5, label: 'test' }) + expect(el.textContent).toContain('"count"') + expect(el.textContent).toContain('"label"') + }) + + it('renders array brackets around array values', () => { + const el = renderTree([10, 20]) + const text = String(el.textContent) + expect(text.indexOf('[')).toBeLessThan(text.indexOf('10')) + expect(text.indexOf('10')).toBeLessThan(text.indexOf(']')) + }) + }) +}) diff --git a/packages/devtools-ui/vite.config.ts b/packages/devtools-ui/vite.config.ts index 6f3f1659..72e16ea8 100644 --- a/packages/devtools-ui/vite.config.ts +++ b/packages/devtools-ui/vite.config.ts @@ -9,6 +9,8 @@ const config = defineConfig({ test: { name: packageJson.name, dir: './', + include: ['tests/**/*.{ts,tsx}'], + exclude: ['tests/test-setup.ts', '**/node_modules/**'], watch: false, environment: 'jsdom', setupFiles: ['./tests/test-setup.ts'],