mirror of
https://github.com/rowboatlabs/rowboat.git
synced 2026-07-24 21:41:08 +02:00
test(x): cover chart fence parsing; document y arrays in doc-collab
parseChartSource extracted as a pure function with tests for the three render states (streaming / invalid / parsed), the label/value alias mapping, and multi-series y arrays. doc-collab's chart block reference now mentions the array form of y.
This commit is contained in:
parent
099a47ac36
commit
7bf735d6c9
3 changed files with 81 additions and 21 deletions
52
apps/x/apps/renderer/src/components/chart-renderer.test.ts
Normal file
52
apps/x/apps/renderer/src/components/chart-renderer.test.ts
Normal file
|
|
@ -0,0 +1,52 @@
|
|||
import { describe, expect, it } from 'vitest'
|
||||
import { parseChartSource } from './chart-renderer'
|
||||
|
||||
const valid = {
|
||||
chart: 'line',
|
||||
x: 'day',
|
||||
y: 'pct',
|
||||
data: [{ day: 'Mon', pct: 0.1 }],
|
||||
}
|
||||
|
||||
describe('parseChartSource', () => {
|
||||
it('parses a valid config', () => {
|
||||
const { config, invalid } = parseChartSource(JSON.stringify(valid))
|
||||
expect(invalid).toBe(false)
|
||||
expect(config?.chart).toBe('line')
|
||||
})
|
||||
|
||||
it('treats incomplete JSON as streaming, not invalid', () => {
|
||||
const partial = JSON.stringify(valid).slice(0, 25)
|
||||
expect(parseChartSource(partial)).toEqual({ config: null, invalid: false })
|
||||
})
|
||||
|
||||
it('flags complete-but-schema-invalid JSON as invalid', () => {
|
||||
const { config, invalid } = parseChartSource(
|
||||
JSON.stringify({ chart: 'line', data: [] }),
|
||||
)
|
||||
expect(config).toBeNull()
|
||||
expect(invalid).toBe(true)
|
||||
})
|
||||
|
||||
it('maps the label/value pie aliases to x/y', () => {
|
||||
// Seen live: models emit pie configs with label/value field names.
|
||||
const { config, invalid } = parseChartSource(
|
||||
JSON.stringify({
|
||||
chart: 'pie',
|
||||
label: 'index',
|
||||
value: 'drop',
|
||||
data: [{ index: 'Nasdaq', drop: 2.4 }],
|
||||
}),
|
||||
)
|
||||
expect(invalid).toBe(false)
|
||||
expect(config?.x).toBe('index')
|
||||
expect(config?.y).toBe('drop')
|
||||
})
|
||||
|
||||
it('accepts a y array for multi-series charts', () => {
|
||||
const { config } = parseChartSource(
|
||||
JSON.stringify({ ...valid, y: ['a', 'b'] }),
|
||||
)
|
||||
expect(config?.y).toEqual(['a', 'b'])
|
||||
})
|
||||
})
|
||||
|
|
@ -21,6 +21,33 @@ interface ChartRendererProps {
|
|||
source: string
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse a chart fence body. Distinguishes the three states the renderer
|
||||
* shows: `streaming` (JSON incomplete — the model is still writing),
|
||||
* `invalid` (complete JSON that fails the schema), and a parsed config.
|
||||
* Models occasionally reach for pie-flavored field names despite the
|
||||
* skill's schema; the predictable label/value aliases map to x/y.
|
||||
*/
|
||||
export function parseChartSource(
|
||||
source: string,
|
||||
): { config: blocks.ChartBlock | null; invalid: boolean } {
|
||||
let parsed: unknown
|
||||
try {
|
||||
parsed = JSON.parse(source)
|
||||
} catch {
|
||||
return { config: null, invalid: false }
|
||||
}
|
||||
if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) {
|
||||
const obj = parsed as Record<string, unknown>
|
||||
if (obj.x === undefined && typeof obj.label === 'string') obj.x = obj.label
|
||||
if (obj.y === undefined && typeof obj.value === 'string') obj.y = obj.value
|
||||
}
|
||||
const result = blocks.ChartBlockSchema.safeParse(parsed)
|
||||
return result.success
|
||||
? { config: result.data, invalid: false }
|
||||
: { config: null, invalid: true }
|
||||
}
|
||||
|
||||
/**
|
||||
* Inline chart for chat messages: renders a ```chart fenced block (same
|
||||
* ChartBlockSchema the notes chart block uses) via recharts. Invalid or
|
||||
|
|
@ -33,26 +60,7 @@ export function ChartRenderer({ source }: ChartRendererProps) {
|
|||
const gridStroke = resolvedTheme === 'dark' ? '#3a3a38' : '#e4e4e0'
|
||||
const textColor = resolvedTheme === 'dark' ? '#c3c2b7' : '#52514e'
|
||||
|
||||
const { config, invalid } = useMemo(() => {
|
||||
let parsed: unknown
|
||||
try {
|
||||
parsed = JSON.parse(source)
|
||||
} catch {
|
||||
// Incomplete JSON — the fence body is still streaming in.
|
||||
return { config: null, invalid: false }
|
||||
}
|
||||
// Models occasionally reach for pie-flavored field names despite the
|
||||
// skill's schema; map the predictable aliases before validating.
|
||||
if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) {
|
||||
const obj = parsed as Record<string, unknown>
|
||||
if (obj.x === undefined && typeof obj.label === 'string') obj.x = obj.label
|
||||
if (obj.y === undefined && typeof obj.value === 'string') obj.y = obj.value
|
||||
}
|
||||
const result = blocks.ChartBlockSchema.safeParse(parsed)
|
||||
return result.success
|
||||
? { config: result.data, invalid: false }
|
||||
: { config: null, invalid: true }
|
||||
}, [source])
|
||||
const { config, invalid } = useMemo(() => parseChartSource(source), [source])
|
||||
|
||||
// Complete JSON that fails the schema is a real error — say so instead of
|
||||
// showing the streaming placeholder forever.
|
||||
|
|
|
|||
|
|
@ -224,7 +224,7 @@ Renders a chart from inline data.
|
|||
- \`data\` (optional): Array of objects with the data points
|
||||
- \`source\` (optional): Relative path to a JSON file containing the data array (alternative to inline data)
|
||||
- \`x\` (required): Key name for the x-axis / label field
|
||||
- \`y\` (required): Key name for the y-axis / value field
|
||||
- \`y\` (required): Key name for the y-axis / value field — or an array of key names to plot several series on one chart (each row then carries one key per series)
|
||||
|
||||
### Table Block
|
||||
Renders a styled table from structured data.
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue