openpencil/packages/scene-graph/tests/plugin-data/fields.test.ts

84 lines
3.2 KiB
TypeScript
Raw Normal View History

refactor: define OpenPencil plugin data in one typed registry (#878) * fix: validate parsed JSON at untrusted boundaries with Valibot Clipboard HTML, library revisions from shared storage, MCP and automation WebSocket messages, the MCP discovery file, sidecar output and AI/MCP tool arguments were JSON.parse'd and cast to their expected types, so a malformed payload reached the document or crashed paste. They now go through v.pipe(v.string(), v.parseJson(), Schema), which reports bad JSON and a wrong shape as the same validation failure. The path_set tool rejects an invalid VectorNetwork and shares its parser with create_vector. The CLI library catalog validates its files and runs revisions through the same size, identity and content-hash checks as the app; reading image bytes as index-keyed records also stops them coming back empty. Hand-rolled typeof readers for plugin data, document metadata, caches and preferences become schemas with their behaviour preserved, and readCacheJSON takes a schema for its payload. open-pencil/no-unvalidated-json-parse rejects type assertions on JSON.parse results other than `as unknown` in src and packages/*/src. * refactor: validate parsed JSON in tests and tooling Extend open-pencil/no-unvalidated-json-parse beyond source: tests, helpers and repo tooling now parse JSON through Valibot schemas instead of asserting a type. The shared fixture reader returns a validated object; its old array annotation never matched the fixtures. * refactor: define OpenPencil plugin data in one typed registry Every plugin-data key OpenPencil writes is now a field of OPEN_PENCIL_PLUGIN_DATA in scene-graph, with the Valibot schema that reads it; readPluginData and withPluginData replace per-key constants, JSON.parse and hand-matched pluginId/key filters across fig, core, and vue. Moving OkHCL onto it fixes picking a colour rewriting the layer's other plugin data as OkHCL entries.
2026-10-04 17:32:18 +00:00
import { describe, expect, test } from 'bun:test'
import {
OPEN_PENCIL_PLUGIN_DATA,
OPEN_PENCIL_PLUGIN_ID,
readAllPluginData,
readPluginData,
SceneGraph,
withPluginData,
type PluginDataEntry
} from '@open-pencil/scene-graph'
import {
clearNodeFillOkHCL,
getNodeOkHCLPayloads,
setNodeFillOkHCL,
setNodeStrokeOkHCL
} from '@open-pencil/scene-graph/color'
const { exportSettings, okhcl, textDirection } = OPEN_PENCIL_PLUGIN_DATA
const OTHER_PLUGIN: PluginDataEntry = { pluginId: 'other-plugin', key: 'okhcl', value: 'theirs' }
function entry(key: string, value: string): PluginDataEntry {
return { pluginId: OPEN_PENCIL_PLUGIN_ID, key, value }
}
describe('OpenPencil plugin data', () => {
test('gives every field its own key', () => {
const keys = Object.values(OPEN_PENCIL_PLUGIN_DATA).map((field) => field.key)
expect(new Set(keys).size).toBe(keys.length)
})
test('reads a value that is not JSON or has the wrong shape as missing', () => {
for (const value of ['{not json', '{"scale":2}', '[{"scale":2,"format":"gif"}]']) {
expect(readPluginData([entry('exportSettings', value)], exportSettings)).toBeUndefined()
}
expect(readPluginData([entry('textDirection', 'SIDEWAYS')], textDirection)).toBeUndefined()
expect(readPluginData([entry('textDirection', 'RTL')], textDirection)).toBe('RTL')
})
test('ignores the same key written by another plugin', () => {
const entries = [{ ...entry('textDirection', 'RTL'), pluginId: 'other-plugin' }]
expect(readPluginData(entries, textDirection)).toBeUndefined()
})
test('replaces its own entry and keeps everything else', () => {
const settings = [{ scale: 2, format: 'png' as const }]
const entries = withPluginData(
[entry('exportSettings', '[]'), OTHER_PLUGIN, entry('textDirection', 'LTR')],
exportSettings,
settings
)
expect(entries).toEqual([
OTHER_PLUGIN,
entry('textDirection', 'LTR'),
entry('exportSettings', JSON.stringify(settings))
])
expect(readPluginData(entries, exportSettings)).toEqual(settings)
expect(withPluginData(entries, exportSettings, undefined)).toEqual(entries.slice(0, 2))
})
test('picking an OkHCL color keeps the node’s other plugin data', () => {
const graph = new SceneGraph()
const paint = { type: 'SOLID' as const, visible: true, opacity: 1, color: { r: 1, g: 0, b: 0, a: 1 } }
const node = graph.createNode('FRAME', graph.getPages()[0].id, {
fills: [paint],
strokes: [{ ...paint, weight: 1, align: 'INSIDE' as const }],
pluginData: [OTHER_PLUGIN, entry('exportSettings', '[{"scale":1,"format":"png"}]')]
})
const color = { h: 30, c: 0.1, l: 0.6, a: 1 }
graph.updateNode(node.id, setNodeFillOkHCL(node, 0, color))
graph.updateNode(node.id, setNodeStrokeOkHCL(node, 0, color))
graph.updateNode(node.id, clearNodeFillOkHCL(node, 0))
const pluginData = graph.getNode(node.id)?.pluginData ?? []
expect(pluginData.slice(0, 2)).toEqual([
OTHER_PLUGIN,
entry('exportSettings', '[{"scale":1,"format":"png"}]')
])
expect(readAllPluginData(pluginData, okhcl).map(({ kind }) => kind)).toEqual(['stroke'])
expect(getNodeOkHCLPayloads({ ...node, pluginData })).toHaveLength(1)
})
})