openpencil/packages/scene-graph/tests/plugin-data/fields.test.ts
Danila Poyarkov 69dba7002f
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

84 lines
3.2 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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)
})
})