* feat(code): isolate Design JSX execution - Transform JSX with the existing Sucrase dependency and execute it in a disposable opaque-origin iframe worker - Block ambient network capabilities and enforce source, timeout, output, depth, and element limits - Validate that only bounded plain structured data returns to the application * feat(code): add editable Design JSX - Add a lazy CodeMirror editor with JSX syntax support, completion, diagnostics, and bounded scrolling - Convert validated sandbox output into trusted Design JSX helpers before rendering - Apply or insert JSX as one undoable graph transaction while preserving dirty drafts * feat(code): localize JSX editor actions - Add translated-message fallbacks for editing, applying, inserting, and draft state - Document the editable JSX workflow in the unreleased changelog * fix(code): satisfy typed sandbox validation - Keep the sandbox document terminator literal and preserve optional selection handling under type-aware lint * test(code): resolve sandbox probes through app aliases * fix(code): bound sandbox results before cloning - Enforce string, array, object, depth, element, and byte limits inside the disposable worker - Retain host-side validation as a second structured-data boundary * fix(code): keep JSX and HTML editing modes separate - Switch generated Tailwind output back to OpenPencil JSX before editing - Close the JSX editor when opening HTML/CSS import and avoid stacking both editors * feat(code): support authored Design JSX programs - Allow local constants, function components, arrays, conditionals, fragments, and multiple roots - Preserve replacement positions for multiple selected roots and reject mixed-parent or locked selections - Cover multi-root undo, redo, and all-or-nothing rollback * feat(code): add explicit JSX view mode - Let authors leave CodeMirror without applying a draft - Keep the editable surface and HTML/CSS importer mutually exclusive * feat(code): diagnose unknown JSX vocabulary - Warn on OpenPencil elements and properties that are absent from the canonical schema - Surface diagnostics inline through CodeMirror lint markers * test(code): accept WebKit isolation diagnostics - Cover the sandbox architecture against Playwright WebKit - Accept engine-specific wording while preserving the same unavailable-window assertion * refactor(code): consolidate Design JSX vocabulary - Drive renderer warnings, completion, and diagnostics from one supported-property schema - Recognize locally declared components and add focused schema and transform tests - Replace CodeMirror basicSetup with an explicit feature set and remove the umbrella package * fix(code): support Design JSX variable helpers * refactor(code): unify JSX sandbox validation * fix(code): account for complete sandbox output * fix(code): preserve locked JSX descendants * fix(code): preserve JSX sibling order * fix(code): recompute JSX parent layouts * feat(code): add live code previews * test(code): centralize graph assertions * fix(code): harden live preview sessions * docs: describe live code editing * fix(code): update editor accessibility labels * fix(code): use theme-aware error colors * fix(dev): stop Vite disconnect error loops * fix(code): clarify live preview status * fix(ui): avoid tooltip attribute warnings * test(code): assert semantic preview status * refactor(code): remove obsolete editor messages * fix(code): harden preview concurrency and isolation * fix(code): cancel stale reset previews
107 lines
3.9 KiB
TypeScript
107 lines
3.9 KiB
TypeScript
import { describe, expect, test } from 'bun:test'
|
|
|
|
import { convertDesignJSXRoots } from '@/app/code/sandbox/convert'
|
|
import {
|
|
DESIGN_JSX_MAX_ARRAY_LENGTH,
|
|
DESIGN_JSX_MAX_DEPTH,
|
|
DESIGN_JSX_MAX_ELEMENTS,
|
|
DESIGN_JSX_MAX_OBJECT_KEYS,
|
|
DESIGN_JSX_MAX_OUTPUT_BYTES,
|
|
DESIGN_JSX_MAX_STRING_LENGTH,
|
|
resolveDesignJSXValidationLimits
|
|
} from '@/app/code/sandbox/types'
|
|
import { validateDesignJSXOutput } from '@/app/code/sandbox/validate'
|
|
|
|
const limits = resolveDesignJSXValidationLimits({})
|
|
|
|
function frame(props: Record<string, unknown> = {}, children: unknown[] = []) {
|
|
return { type: 'frame', props, children }
|
|
}
|
|
|
|
describe('Design JSX sandbox output', () => {
|
|
test('resolves one complete validation contract', () => {
|
|
expect(limits).toEqual({
|
|
outputBytes: DESIGN_JSX_MAX_OUTPUT_BYTES,
|
|
elements: DESIGN_JSX_MAX_ELEMENTS,
|
|
depth: DESIGN_JSX_MAX_DEPTH,
|
|
arrayLength: DESIGN_JSX_MAX_ARRAY_LENGTH,
|
|
objectKeys: DESIGN_JSX_MAX_OBJECT_KEYS,
|
|
stringLength: DESIGN_JSX_MAX_STRING_LENGTH
|
|
})
|
|
})
|
|
|
|
test('validates and converts the same serialized element contract', () => {
|
|
const roots = validateDesignJSXOutput(
|
|
frame({ fill: { __openPencilHelper: 'solid', args: ['#ff0000'] } }, [
|
|
{ type: 'text', props: {}, children: ['Hello ', 2] }
|
|
]),
|
|
limits
|
|
)
|
|
expect(convertDesignJSXRoots(roots)).toMatchObject([
|
|
{
|
|
type: 'frame',
|
|
props: { fill: { type: 'SOLID' } },
|
|
children: [{ type: 'text', children: ['Hello ', '2'] }]
|
|
}
|
|
])
|
|
})
|
|
|
|
test('rejects empty output and malformed elements before rendering', () => {
|
|
expect(() => validateDesignJSXOutput(undefined, limits)).toThrow(
|
|
'must return an OpenPencil element'
|
|
)
|
|
expect(() => validateDesignJSXOutput([], limits)).toThrow('must return an OpenPencil element')
|
|
expect(() => convertDesignJSXRoots([{ type: 'frame', props: {}, children: null }])).toThrow(
|
|
'must return an OpenPencil element'
|
|
)
|
|
})
|
|
|
|
test('rejects blocked keys and non-plain prototypes', () => {
|
|
const blocked = Object.fromEntries([['safe', true]])
|
|
Object.defineProperty(blocked, 'constructor', { value: 'blocked', enumerable: true })
|
|
expect(() => validateDesignJSXOutput(frame(blocked), limits)).toThrow('blocked key')
|
|
expect(() => validateDesignJSXOutput(frame({ value: new Date() }), limits)).toThrow(
|
|
'plain structured data only'
|
|
)
|
|
})
|
|
|
|
test('enforces array, object, string, and depth limits on the host', () => {
|
|
expect(() =>
|
|
validateDesignJSXOutput(
|
|
frame({}, ['a', 'b']),
|
|
resolveDesignJSXValidationLimits({ arrayLength: 1 })
|
|
)
|
|
).toThrow('array')
|
|
expect(() =>
|
|
validateDesignJSXOutput(
|
|
frame({ a: 1, b: 2 }),
|
|
resolveDesignJSXValidationLimits({ objectKeys: 1 })
|
|
)
|
|
).toThrow('object properties')
|
|
expect(() =>
|
|
validateDesignJSXOutput('abc', resolveDesignJSXValidationLimits({ stringLength: 2 }))
|
|
).toThrow('string')
|
|
expect(() =>
|
|
validateDesignJSXOutput([[['x']]], resolveDesignJSXValidationLimits({ depth: 2 }))
|
|
).toThrow('deeply nested')
|
|
})
|
|
|
|
test('counts container and primitive data toward the output limit', () => {
|
|
const tiny = resolveDesignJSXValidationLimits({ outputBytes: 1 })
|
|
expect(() => validateDesignJSXOutput([], tiny)).toThrow('must return an OpenPencil element')
|
|
expect(() => validateDesignJSXOutput(true, tiny)).toThrow('too large')
|
|
expect(() => validateDesignJSXOutput(1, tiny)).toThrow('too large')
|
|
expect(() => validateDesignJSXOutput(frame(), tiny)).toThrow('too large')
|
|
})
|
|
|
|
test('rejects unknown helper descriptors during trusted conversion', () => {
|
|
for (const helperName of ['unknown', 'toString', 'constructor', 'valueOf']) {
|
|
const roots = validateDesignJSXOutput(
|
|
frame({ fill: { __openPencilHelper: helperName, args: [] } }),
|
|
limits
|
|
)
|
|
expect(() => convertDesignJSXRoots(roots)).toThrow('Unknown Design JSX helper')
|
|
}
|
|
})
|
|
})
|