chore(tests): remove targeted non-null assertions
- Add shared test helpers for required nodes and child ids - Clean non-null assertions from noisy render, nudge, text undo, and fig roundtrip tests - Enforce the non-null assertion ban for the cleaned test files - Remove stale eslint disables around empty functions and broad mock transport typing
This commit is contained in:
parent
4d3e16c0a2
commit
51eae14e4b
|
|
@ -17,6 +17,17 @@
|
|||
"rules": {
|
||||
"typescript/no-explicit-any": "off"
|
||||
}
|
||||
},
|
||||
{
|
||||
"files": [
|
||||
"tests/engine/fig-roundtrip.test.ts",
|
||||
"tests/engine/nudge.test.ts",
|
||||
"tests/engine/render.test.ts",
|
||||
"tests/engine/text-edit-undo.test.ts"
|
||||
],
|
||||
"rules": {
|
||||
"typescript/no-non-null-assertion": "error"
|
||||
}
|
||||
}
|
||||
],
|
||||
"rules": {
|
||||
|
|
|
|||
|
|
@ -68,8 +68,11 @@ export default defineCommand({
|
|||
const graph = await loadDocument(file)
|
||||
const figma = new FigmaAPI(graph)
|
||||
|
||||
// eslint-disable-next-line no-empty-function -- needed to get AsyncFunction constructor
|
||||
const AsyncFunction = Object.getPrototypeOf(async function () {}).constructor
|
||||
type AsyncFunctionConstructor = new (
|
||||
...args: string[]
|
||||
) => (...args: unknown[]) => Promise<unknown>
|
||||
const AsyncFunction = Object.getPrototypeOf(async () => undefined)
|
||||
.constructor as AsyncFunctionConstructor
|
||||
const wrappedCode = code.trim().startsWith('return')
|
||||
? code
|
||||
: `return (async () => { ${code} })()`
|
||||
|
|
|
|||
|
|
@ -22,6 +22,8 @@ import type {
|
|||
} from '#core/scene-graph'
|
||||
import type { Rect, Vector } from '#core/types'
|
||||
|
||||
const noop = () => undefined
|
||||
|
||||
export { FigmaNodeProxy } from './proxy'
|
||||
export type { FigmaFont, FigmaFontName } from './proxy'
|
||||
|
||||
|
|
@ -385,14 +387,16 @@ export class FigmaAPI implements NodeProxyHost {
|
|||
|
||||
notify(message: string): { cancel: () => void } {
|
||||
if (typeof console !== 'undefined') console.warn(`[figma.notify] ${message}`)
|
||||
// eslint-disable-next-line no-empty-function
|
||||
return { cancel() {} }
|
||||
return { cancel: noop }
|
||||
}
|
||||
|
||||
// eslint-disable-next-line no-empty-function
|
||||
commitUndo(): void {}
|
||||
// eslint-disable-next-line no-empty-function
|
||||
triggerUndo(): void {}
|
||||
commitUndo(): void {
|
||||
return undefined
|
||||
}
|
||||
|
||||
triggerUndo(): void {
|
||||
return undefined
|
||||
}
|
||||
|
||||
exportImage?: (
|
||||
nodeIds: string[],
|
||||
|
|
|
|||
|
|
@ -21,7 +21,6 @@ export class DrawCallCounter {
|
|||
for (const method of DRAW_METHODS) {
|
||||
const original = gl[method].bind(gl) as DrawFunction
|
||||
this.originals.set(method, original)
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment -- monkey-patching overloaded WebGL draw methods
|
||||
;(gl[method] as DrawFunction) = (...args: unknown[]) => {
|
||||
this.count++
|
||||
original(...args)
|
||||
|
|
|
|||
|
|
@ -9,12 +9,18 @@ export const evalCode = defineTool({
|
|||
},
|
||||
mutates: true,
|
||||
execute: async (figma, { code }) => {
|
||||
// eslint-disable-next-line no-empty-function
|
||||
const AsyncFunction = Object.getPrototypeOf(async function () {}).constructor
|
||||
type AsyncFunctionConstructor = new (
|
||||
...args: string[]
|
||||
) => (...args: unknown[]) => Promise<unknown>
|
||||
const AsyncFunction = Object.getPrototypeOf(async () => undefined)
|
||||
.constructor as AsyncFunctionConstructor
|
||||
const wrapped = code.trim().startsWith('return') ? code : `return (async () => { ${code} })()`
|
||||
const fn = new AsyncFunction('figma', wrapped)
|
||||
const result = await fn(figma)
|
||||
if (result && typeof result === 'object' && 'toJSON' in result) return result.toJSON()
|
||||
if (result && typeof result === 'object') {
|
||||
const toJSON = Reflect.get(result, 'toJSON')
|
||||
if (typeof toJSON === 'function') return toJSON.call(result)
|
||||
}
|
||||
if (result !== undefined && result !== null) return result
|
||||
return { ok: true, message: 'Code executed (no return value)' }
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ import { ACP_AGENTS } from '@open-pencil/core/constants'
|
|||
|
||||
import type { getActiveEditorStore } from '@/app/editor/active-store'
|
||||
import type { ACPAgentID, AIProviderID } from '@open-pencil/core/constants'
|
||||
import type { UIMessage } from 'ai'
|
||||
import type { ChatTransport, UIMessage } from 'ai'
|
||||
import type { ComputedRef, Ref } from 'vue'
|
||||
|
||||
type EditorStore = ReturnType<typeof getActiveEditorStore>
|
||||
|
|
@ -110,7 +110,7 @@ export function createToolLoopTransport({
|
|||
}
|
||||
})
|
||||
|
||||
return new DirectChatTransport({ agent })
|
||||
return new DirectChatTransport({ agent }) as ChatTransport<UIMessage>
|
||||
}
|
||||
|
||||
export function createChatSessionManager({
|
||||
|
|
@ -130,8 +130,7 @@ export function createChatSessionManager({
|
|||
let currentChatMessages = new WeakMap<EditorStore, UIMessage[]>()
|
||||
let chat: Chat<UIMessage> | null = null
|
||||
let acpTransportInstance: { destroy(): Promise<void> } | null = null
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- test-only mock transports don't implement full generics
|
||||
let overrideTransport: (() => any) | null = null
|
||||
let overrideTransport: (() => ChatTransport<UIMessage>) | null = null
|
||||
|
||||
function markTransportDirty() {
|
||||
transportDirty = true
|
||||
|
|
@ -143,7 +142,7 @@ export function createChatSessionManager({
|
|||
await acpTransportInstance?.destroy()
|
||||
const transport = await createACPTransport(providerID.value)
|
||||
acpTransportInstance = transport
|
||||
return transport
|
||||
return transport as ChatTransport<UIMessage>
|
||||
}
|
||||
|
||||
function createTransport(store: EditorStore) {
|
||||
|
|
@ -174,7 +173,7 @@ export function createChatSessionManager({
|
|||
|
||||
if (!chat || transportDirty || currentChatStore !== store) {
|
||||
const messages = currentChatMessages.get(store)
|
||||
const transport = isACPProvider.value
|
||||
const transport: ChatTransport<UIMessage> = isACPProvider.value
|
||||
? await createActiveACPTransport()
|
||||
: createTransport(store)
|
||||
chat = new Chat<UIMessage>({ transport, messages })
|
||||
|
|
@ -191,7 +190,7 @@ export function createChatSessionManager({
|
|||
transportDirty = false
|
||||
}
|
||||
|
||||
function setOverrideTransport(factory: (() => unknown) | null) {
|
||||
function setOverrideTransport(factory: (() => ChatTransport<UIMessage>) | null) {
|
||||
overrideTransport = factory
|
||||
markTransportDirty()
|
||||
}
|
||||
|
|
|
|||
3
src/global.d.ts
vendored
3
src/global.d.ts
vendored
|
|
@ -1,4 +1,5 @@
|
|||
import type { EditorStore } from '@/app/editor/session/create'
|
||||
import type { ChatTransport, UIMessage } from 'ai'
|
||||
|
||||
declare global {
|
||||
interface Uint8ArrayConstructor {
|
||||
|
|
@ -38,7 +39,7 @@ declare global {
|
|||
blob(): Promise<Blob>
|
||||
}[]
|
||||
>
|
||||
__OPEN_PENCIL_SET_TRANSPORT__?(factory: () => unknown): void
|
||||
__OPEN_PENCIL_SET_TRANSPORT__?(factory: () => ChatTransport<UIMessage>): void
|
||||
__OPEN_PENCIL_STORE__?: EditorStore
|
||||
__TEST_WRITE_COUNT__?(): number
|
||||
__TEST_MOCK_HANDLE__?: FileSystemFileHandle
|
||||
|
|
|
|||
|
|
@ -16,6 +16,7 @@ import {
|
|||
type Fill
|
||||
} from '@open-pencil/core'
|
||||
|
||||
import { expectDefined } from '../helpers/assert'
|
||||
import {
|
||||
childMatching,
|
||||
childNamed,
|
||||
|
|
@ -567,7 +568,7 @@ describe('roundtrip: export → re-import', () => {
|
|||
test('preserves fills', () => {
|
||||
const headerBg = reImportedNodes.find((n) => n.name === 'Header BG')
|
||||
expect(headerBg).toBeDefined()
|
||||
expect(headerBg!.fills).toHaveLength(2)
|
||||
expect(expectDefined(headerBg, 'headerBg').fills).toHaveLength(2)
|
||||
expect(headerBg.fills[0].type).toBe('SOLID')
|
||||
expect(headerBg.fills[0].color.r).toBeCloseTo(0.2, 1)
|
||||
expect(headerBg.fills[1].type).toBe('GRADIENT_LINEAR')
|
||||
|
|
@ -578,13 +579,13 @@ describe('roundtrip: export → re-import', () => {
|
|||
test('preserves text content', () => {
|
||||
const title = reImportedNodes.find((n) => n.name === 'Title')
|
||||
expect(title).toBeDefined()
|
||||
expect(title!.text).toBe('Hello World')
|
||||
expect(expectDefined(title, 'title').text).toBe('Hello World')
|
||||
})
|
||||
|
||||
test('preserves text properties', () => {
|
||||
const title = reImportedNodes.find((n) => n.name === 'Title')
|
||||
expect(title).toBeDefined()
|
||||
expect(title!.fontSize).toBe(18)
|
||||
expect(expectDefined(title, 'title').fontSize).toBe(18)
|
||||
expect(title.fontFamily).toBe('Inter')
|
||||
expect(title.fontWeight).toBe(700)
|
||||
expect(title.textAlignHorizontal).toBe('CENTER')
|
||||
|
|
@ -593,13 +594,13 @@ describe('roundtrip: export → re-import', () => {
|
|||
test('preserves layout mode', () => {
|
||||
const container = reImportedNodes.find((n) => n.name === 'Container')
|
||||
expect(container).toBeDefined()
|
||||
expect(container!.layoutMode).toBe('VERTICAL')
|
||||
expect(expectDefined(container, 'container').layoutMode).toBe('VERTICAL')
|
||||
})
|
||||
|
||||
test('preserves layout spacing', () => {
|
||||
const container = reImportedNodes.find((n) => n.name === 'Container')
|
||||
expect(container).toBeDefined()
|
||||
expect(container!.itemSpacing).toBe(16)
|
||||
expect(expectDefined(container, 'container').itemSpacing).toBe(16)
|
||||
expect(container.paddingTop).toBe(24)
|
||||
expect(container.paddingRight).toBe(24)
|
||||
expect(container.paddingBottom).toBe(24)
|
||||
|
|
@ -609,13 +610,13 @@ describe('roundtrip: export → re-import', () => {
|
|||
test('preserves corner radius', () => {
|
||||
const container = reImportedNodes.find((n) => n.name === 'Container')
|
||||
expect(container).toBeDefined()
|
||||
expect(container!.cornerRadius).toBe(12)
|
||||
expect(expectDefined(container, 'container').cornerRadius).toBe(12)
|
||||
})
|
||||
|
||||
test('preserves independent corner radii', () => {
|
||||
const headerBg = reImportedNodes.find((n) => n.name === 'Header BG')
|
||||
expect(headerBg).toBeDefined()
|
||||
expect(headerBg!.independentCorners).toBe(true)
|
||||
expect(expectDefined(headerBg, 'headerBg').independentCorners).toBe(true)
|
||||
expect(headerBg.topLeftRadius).toBe(8)
|
||||
expect(headerBg.topRightRadius).toBe(8)
|
||||
expect(headerBg.bottomRightRadius).toBe(0)
|
||||
|
|
@ -625,7 +626,7 @@ describe('roundtrip: export → re-import', () => {
|
|||
test('preserves effects', () => {
|
||||
const headerBg = reImportedNodes.find((n) => n.name === 'Header BG')
|
||||
expect(headerBg).toBeDefined()
|
||||
expect(headerBg!.effects).toHaveLength(1)
|
||||
expect(expectDefined(headerBg, 'headerBg').effects).toHaveLength(1)
|
||||
expect(headerBg.effects[0].type).toBe('DROP_SHADOW')
|
||||
expect(headerBg.effects[0].radius).toBe(8)
|
||||
expect(headerBg.effects[0].offset.y).toBe(4)
|
||||
|
|
@ -634,7 +635,7 @@ describe('roundtrip: export → re-import', () => {
|
|||
test('preserves dimensions', () => {
|
||||
const container = reImportedNodes.find((n) => n.name === 'Container')
|
||||
expect(container).toBeDefined()
|
||||
expect(container!.width).toBe(400)
|
||||
expect(expectDefined(container, 'container').width).toBe(400)
|
||||
expect(container.height).toBe(300)
|
||||
})
|
||||
})
|
||||
|
|
@ -795,7 +796,7 @@ describe('edge cases', () => {
|
|||
const page = graph.getPages()[0]
|
||||
const outerUse = graph.getChildren(page.id).find((n) => n.name === 'OuterUse')
|
||||
expect(outerUse).toBeDefined()
|
||||
expect(outerUse!.type).toBe('INSTANCE')
|
||||
expect(expectDefined(outerUse, 'outerUse').type).toBe('INSTANCE')
|
||||
|
||||
// Walk down: OuterUse > InnerUse clone > Label clone
|
||||
const innerClone = graph.getChildren(outerUse?.id ?? '')[0]
|
||||
|
|
@ -1037,7 +1038,7 @@ describe('text node export', () => {
|
|||
|
||||
const textNode = [...reimported.getAllNodes()].find((n) => n.name === 'Greeting')
|
||||
expect(textNode).toBeDefined()
|
||||
expect(textNode!.type).toBe('TEXT')
|
||||
expect(expectDefined(textNode, 'textNode').type).toBe('TEXT')
|
||||
expect(textNode.text).toBe('Hello World')
|
||||
expect(textNode.fontFamily).toBe('Inter')
|
||||
expect(textNode.fontSize).toBe(16)
|
||||
|
|
@ -1063,7 +1064,7 @@ describe('text node export', () => {
|
|||
|
||||
const textNode = [...reimported.getAllNodes()].find((n) => n.name === 'Multiline')
|
||||
expect(textNode).toBeDefined()
|
||||
expect(textNode!.text).toBe('Line 1\nLine 2\nLine 3')
|
||||
expect(expectDefined(textNode, 'textNode').text).toBe('Line 1\nLine 2\nLine 3')
|
||||
})
|
||||
|
||||
test('derivedTextData fields present in raw binary', async () => {
|
||||
|
|
@ -1221,7 +1222,7 @@ describe('variable roundtrip', () => {
|
|||
const vars = [...reimported.variables.values()]
|
||||
const colorVar = vars.find((v) => v.name === 'color/primary')
|
||||
expect(colorVar).toBeDefined()
|
||||
expect(colorVar!.type).toBe('COLOR')
|
||||
expect(expectDefined(colorVar, 'colorVar').type).toBe('COLOR')
|
||||
const colorVal = Object.values(colorVar.valuesByMode)[0] as {
|
||||
r: number
|
||||
g: number
|
||||
|
|
@ -1232,17 +1233,17 @@ describe('variable roundtrip', () => {
|
|||
|
||||
const floatVar = vars.find((v) => v.name === 'spacing/base')
|
||||
expect(floatVar).toBeDefined()
|
||||
expect(floatVar!.type).toBe('FLOAT')
|
||||
expect(expectDefined(floatVar, 'floatVar').type).toBe('FLOAT')
|
||||
expect(Object.values(floatVar.valuesByMode)[0]).toBe(8)
|
||||
|
||||
const boolVar = vars.find((v) => v.name === 'visible')
|
||||
expect(boolVar).toBeDefined()
|
||||
expect(boolVar!.type).toBe('BOOLEAN')
|
||||
expect(expectDefined(boolVar, 'boolVar').type).toBe('BOOLEAN')
|
||||
expect(Object.values(boolVar.valuesByMode)[0]).toBe(true)
|
||||
|
||||
const strVar = vars.find((v) => v.name === 'label')
|
||||
expect(strVar).toBeDefined()
|
||||
expect(strVar!.type).toBe('STRING')
|
||||
expect(expectDefined(strVar, 'strVar').type).toBe('STRING')
|
||||
expect(Object.values(strVar.valuesByMode)[0]).toBe('Hello')
|
||||
})
|
||||
|
||||
|
|
|
|||
|
|
@ -2,6 +2,8 @@ import { describe, test, expect } from 'bun:test'
|
|||
|
||||
import { createEditor } from '@open-pencil/core/editor'
|
||||
|
||||
import { getNodeOrThrow } from '../helpers/assert'
|
||||
|
||||
describe('nudgeSelected', () => {
|
||||
function setup() {
|
||||
const editor = createEditor()
|
||||
|
|
@ -21,22 +23,22 @@ describe('nudgeSelected', () => {
|
|||
const { editor, rect } = setup()
|
||||
|
||||
editor.nudgeSelected(1, 0)
|
||||
expect(editor.graph.getNode(rect.id)!.x).toBe(101)
|
||||
expect(editor.graph.getNode(rect.id)!.y).toBe(200)
|
||||
expect(getNodeOrThrow(editor.graph, rect.id).x).toBe(101)
|
||||
expect(getNodeOrThrow(editor.graph, rect.id).y).toBe(200)
|
||||
|
||||
editor.nudgeSelected(0, -1)
|
||||
expect(editor.graph.getNode(rect.id)!.x).toBe(101)
|
||||
expect(editor.graph.getNode(rect.id)!.y).toBe(199)
|
||||
expect(getNodeOrThrow(editor.graph, rect.id).x).toBe(101)
|
||||
expect(getNodeOrThrow(editor.graph, rect.id).y).toBe(199)
|
||||
})
|
||||
|
||||
test('shift nudge moves by 10px', () => {
|
||||
const { editor, rect } = setup()
|
||||
|
||||
editor.nudgeSelected(10, 0)
|
||||
expect(editor.graph.getNode(rect.id)!.x).toBe(110)
|
||||
expect(getNodeOrThrow(editor.graph, rect.id).x).toBe(110)
|
||||
|
||||
editor.nudgeSelected(0, 10)
|
||||
expect(editor.graph.getNode(rect.id)!.y).toBe(210)
|
||||
expect(getNodeOrThrow(editor.graph, rect.id).y).toBe(210)
|
||||
})
|
||||
|
||||
test('nudge is undoable as a single entry', () => {
|
||||
|
|
@ -47,15 +49,15 @@ describe('nudgeSelected', () => {
|
|||
editor.nudgeSelected(1, 0)
|
||||
editor.flushNudge()
|
||||
|
||||
expect(editor.graph.getNode(rect.id)!.x).toBe(103)
|
||||
expect(getNodeOrThrow(editor.graph, rect.id).x).toBe(103)
|
||||
expect(editor.undo.canUndo).toBe(true)
|
||||
|
||||
editor.undo.undo()
|
||||
expect(editor.graph.getNode(rect.id)!.x).toBe(100)
|
||||
expect(editor.graph.getNode(rect.id)!.y).toBe(200)
|
||||
expect(getNodeOrThrow(editor.graph, rect.id).x).toBe(100)
|
||||
expect(getNodeOrThrow(editor.graph, rect.id).y).toBe(200)
|
||||
|
||||
editor.undo.redo()
|
||||
expect(editor.graph.getNode(rect.id)!.x).toBe(103)
|
||||
expect(getNodeOrThrow(editor.graph, rect.id).x).toBe(103)
|
||||
})
|
||||
|
||||
test('nudge skips locked nodes', () => {
|
||||
|
|
@ -64,8 +66,8 @@ describe('nudgeSelected', () => {
|
|||
editor.graph.updateNode(rect.id, { locked: true })
|
||||
editor.nudgeSelected(10, 10)
|
||||
editor.flushNudge()
|
||||
expect(editor.graph.getNode(rect.id)!.x).toBe(100)
|
||||
expect(editor.graph.getNode(rect.id)!.y).toBe(200)
|
||||
expect(getNodeOrThrow(editor.graph, rect.id).x).toBe(100)
|
||||
expect(getNodeOrThrow(editor.graph, rect.id).y).toBe(200)
|
||||
expect(editor.undo.canUndo).toBe(false)
|
||||
})
|
||||
|
||||
|
|
@ -93,10 +95,10 @@ describe('nudgeSelected', () => {
|
|||
editor.nudgeSelected(-5, 3)
|
||||
editor.flushNudge()
|
||||
|
||||
expect(editor.graph.getNode(rect.id)!.x).toBe(95)
|
||||
expect(editor.graph.getNode(rect.id)!.y).toBe(203)
|
||||
expect(editor.graph.getNode(rect2.id)!.x).toBe(295)
|
||||
expect(editor.graph.getNode(rect2.id)!.y).toBe(403)
|
||||
expect(getNodeOrThrow(editor.graph, rect.id).x).toBe(95)
|
||||
expect(getNodeOrThrow(editor.graph, rect.id).y).toBe(203)
|
||||
expect(getNodeOrThrow(editor.graph, rect2.id).x).toBe(295)
|
||||
expect(getNodeOrThrow(editor.graph, rect2.id).y).toBe(403)
|
||||
})
|
||||
|
||||
test('separate nudge sequences create separate undo entries', () => {
|
||||
|
|
@ -108,15 +110,15 @@ describe('nudgeSelected', () => {
|
|||
editor.nudgeSelected(0, 5)
|
||||
editor.flushNudge()
|
||||
|
||||
expect(editor.graph.getNode(rect.id)!.x).toBe(105)
|
||||
expect(editor.graph.getNode(rect.id)!.y).toBe(205)
|
||||
expect(getNodeOrThrow(editor.graph, rect.id).x).toBe(105)
|
||||
expect(getNodeOrThrow(editor.graph, rect.id).y).toBe(205)
|
||||
|
||||
editor.undo.undo()
|
||||
expect(editor.graph.getNode(rect.id)!.x).toBe(105)
|
||||
expect(editor.graph.getNode(rect.id)!.y).toBe(200)
|
||||
expect(getNodeOrThrow(editor.graph, rect.id).x).toBe(105)
|
||||
expect(getNodeOrThrow(editor.graph, rect.id).y).toBe(200)
|
||||
|
||||
editor.undo.undo()
|
||||
expect(editor.graph.getNode(rect.id)!.x).toBe(100)
|
||||
expect(editor.graph.getNode(rect.id)!.y).toBe(200)
|
||||
expect(getNodeOrThrow(editor.graph, rect.id).x).toBe(100)
|
||||
expect(getNodeOrThrow(editor.graph, rect.id).y).toBe(200)
|
||||
})
|
||||
})
|
||||
|
|
|
|||
|
|
@ -19,6 +19,7 @@ import {
|
|||
node
|
||||
} from '@open-pencil/core'
|
||||
|
||||
import { expectDefined, getNodeOrThrow, childIdAt } from '../helpers/assert'
|
||||
import { makeSceneGraph } from '../helpers/scene'
|
||||
|
||||
describe('TreeNode builders', () => {
|
||||
|
|
@ -46,7 +47,7 @@ describe('TreeNode builders', () => {
|
|||
]
|
||||
})
|
||||
expect(tree.children.length).toBe(2)
|
||||
expect(isTreeNode(tree.children[0]!)).toBe(true)
|
||||
expect(isTreeNode(expectDefined(tree.children[0], 'first tree child'))).toBe(true)
|
||||
const bg = tree.children[0] as ReturnType<typeof Rectangle>
|
||||
expect(bg.type).toBe('rectangle')
|
||||
expect(bg.props.name).toBe('Bg')
|
||||
|
|
@ -76,11 +77,11 @@ describe('renderTree', () => {
|
|||
expect(result.name).toBe('MyFrame')
|
||||
expect(result.type).toBe('FRAME')
|
||||
|
||||
const node = g.nodes.get(result.id)!
|
||||
const node = getNodeOrThrow(g, result.id)
|
||||
expect(node.width).toBe(200)
|
||||
expect(node.height).toBe(100)
|
||||
expect(node.fills.length).toBe(1)
|
||||
expect(node.fills[0]!.type).toBe('SOLID')
|
||||
expect(expectDefined(node.fills[0], 'first fill').type).toBe('SOLID')
|
||||
})
|
||||
|
||||
it('renders text node with content', async () => {
|
||||
|
|
@ -94,7 +95,7 @@ describe('renderTree', () => {
|
|||
})
|
||||
const result = await renderTree(g, tree)
|
||||
|
||||
const node = g.nodes.get(result.id)!
|
||||
const node = getNodeOrThrow(g, result.id)
|
||||
expect(node.type).toBe('TEXT')
|
||||
expect(node.text).toBe('Hello')
|
||||
expect(node.fontSize).toBe(24)
|
||||
|
|
@ -119,14 +120,14 @@ describe('renderTree', () => {
|
|||
})
|
||||
const result = await renderTree(g, tree)
|
||||
|
||||
const card = g.nodes.get(result.id)!
|
||||
const card = getNodeOrThrow(g, result.id)
|
||||
expect(card.layoutMode).toBe('VERTICAL')
|
||||
expect(card.itemSpacing).toBe(16)
|
||||
expect(card.paddingTop).toBe(24)
|
||||
expect(card.paddingRight).toBe(24)
|
||||
expect(card.childIds.length).toBe(3)
|
||||
|
||||
const title = g.nodes.get(card.childIds[1]!)!
|
||||
const title = getNodeOrThrow(g, childIdAt(card, 1))
|
||||
expect(title.text).toBe('Card Title')
|
||||
expect(title.fontWeight).toBe(700)
|
||||
})
|
||||
|
|
@ -136,14 +137,14 @@ describe('renderTree', () => {
|
|||
const tree = Frame({ name: 'Positioned', w: 100, h: 100 })
|
||||
const result = await renderTree(g, tree, { x: 50, y: 75 })
|
||||
|
||||
const node = g.nodes.get(result.id)!
|
||||
const node = getNodeOrThrow(g, result.id)
|
||||
expect(node.x).toBe(50)
|
||||
expect(node.y).toBe(75)
|
||||
})
|
||||
|
||||
it('renders into a specific parent', async () => {
|
||||
const g = makeSceneGraph()
|
||||
const page = g.getPages()[0]!
|
||||
const page = expectDefined(g.getPages()[0], 'first page')
|
||||
const container = g.createNode('FRAME', page.id, { name: 'Container' })
|
||||
|
||||
const tree = Frame({ name: 'Child', w: 50, h: 50 })
|
||||
|
|
@ -163,7 +164,7 @@ describe('renderTree', () => {
|
|||
wrap: true
|
||||
})
|
||||
const result = await renderTree(g, tree)
|
||||
const node = g.nodes.get(result.id)!
|
||||
const node = getNodeOrThrow(g, result.id)
|
||||
|
||||
expect(node.layoutMode).toBe('HORIZONTAL')
|
||||
expect(node.itemSpacing).toBe(8)
|
||||
|
|
@ -179,7 +180,7 @@ describe('renderTree', () => {
|
|||
Text({ size: 16, color: '#FFFFFF', children: '★' })
|
||||
)
|
||||
const result = await renderTree(g, tree)
|
||||
const n = g.nodes.get(result.id)!
|
||||
const n = getNodeOrThrow(g, result.id)
|
||||
|
||||
expect(n.layoutMode).toBe('VERTICAL')
|
||||
expect(n.primaryAxisAlign).toBe('CENTER')
|
||||
|
|
@ -190,7 +191,7 @@ describe('renderTree', () => {
|
|||
const g = makeSceneGraph()
|
||||
const tree = Frame({ name: 'Padded', px: 16, py: 8, pt: 4 })
|
||||
const result = await renderTree(g, tree)
|
||||
const node = g.nodes.get(result.id)!
|
||||
const node = getNodeOrThrow(g, result.id)
|
||||
|
||||
expect(node.paddingLeft).toBe(16)
|
||||
expect(node.paddingRight).toBe(16)
|
||||
|
|
@ -202,14 +203,14 @@ describe('renderTree', () => {
|
|||
const g = makeSceneGraph()
|
||||
const tree = Frame({ name: 'Rounded', rounded: 12 })
|
||||
const result = await renderTree(g, tree)
|
||||
expect(g.nodes.get(result.id)!.cornerRadius).toBe(12)
|
||||
expect(getNodeOrThrow(g, result.id).cornerRadius).toBe(12)
|
||||
})
|
||||
|
||||
it('handles independent corners', async () => {
|
||||
const g = makeSceneGraph()
|
||||
const tree = Frame({ name: 'Corners', roundedTL: 8, roundedBR: 16 })
|
||||
const result = await renderTree(g, tree)
|
||||
const node = g.nodes.get(result.id)!
|
||||
const node = getNodeOrThrow(g, result.id)
|
||||
|
||||
expect(node.independentCorners).toBe(true)
|
||||
expect(node.topLeftRadius).toBe(8)
|
||||
|
|
@ -220,17 +221,17 @@ describe('renderTree', () => {
|
|||
const g = makeSceneGraph()
|
||||
const tree = Rectangle({ name: 'Bordered', stroke: '#000', strokeWidth: 2 })
|
||||
const result = await renderTree(g, tree)
|
||||
const node = g.nodes.get(result.id)!
|
||||
const node = getNodeOrThrow(g, result.id)
|
||||
|
||||
expect(node.strokes.length).toBe(1)
|
||||
expect(node.strokes[0]!.weight).toBe(2)
|
||||
expect(expectDefined(node.strokes[0], 'first stroke').weight).toBe(2)
|
||||
})
|
||||
|
||||
it('handles opacity and rotation', async () => {
|
||||
const g = makeSceneGraph()
|
||||
const tree = Frame({ name: 'Transformed', opacity: 0.5, rotate: 45 })
|
||||
const result = await renderTree(g, tree)
|
||||
const node = g.nodes.get(result.id)!
|
||||
const node = getNodeOrThrow(g, result.id)
|
||||
|
||||
expect(node.opacity).toBe(0.5)
|
||||
expect(node.rotation).toBe(45)
|
||||
|
|
@ -240,14 +241,14 @@ describe('renderTree', () => {
|
|||
const g = makeSceneGraph()
|
||||
const tree = Frame({ name: 'Clipped', overflow: 'hidden' })
|
||||
const result = await renderTree(g, tree)
|
||||
expect(g.nodes.get(result.id)!.clipsContent).toBe(true)
|
||||
expect(getNodeOrThrow(g, result.id).clipsContent).toBe(true)
|
||||
})
|
||||
|
||||
it('handles hug sizing', async () => {
|
||||
const g = makeSceneGraph()
|
||||
const tree = Frame({ name: 'Hug', w: 'hug', h: 'hug', flex: 'col' })
|
||||
const result = await renderTree(g, tree)
|
||||
const node = g.nodes.get(result.id)!
|
||||
const node = getNodeOrThrow(g, result.id)
|
||||
|
||||
expect(node.primaryAxisSizing).toBe('HUG')
|
||||
expect(node.counterAxisSizing).toBe('HUG')
|
||||
|
|
@ -257,7 +258,7 @@ describe('renderTree', () => {
|
|||
const g = makeSceneGraph()
|
||||
const tree = Frame({ name: 'Fill', w: 'fill' })
|
||||
const result = await renderTree(g, tree)
|
||||
const node = g.nodes.get(result.id)!
|
||||
const node = getNodeOrThrow(g, result.id)
|
||||
|
||||
expect(node.layoutGrow).toBe(1)
|
||||
})
|
||||
|
|
@ -266,22 +267,22 @@ describe('renderTree', () => {
|
|||
const g = makeSceneGraph()
|
||||
const tree = Frame({ name: 'Shadow', shadow: '0 4 12 rgba(0,0,0,0.1)' })
|
||||
const result = await renderTree(g, tree)
|
||||
const node = g.nodes.get(result.id)!
|
||||
const node = getNodeOrThrow(g, result.id)
|
||||
|
||||
expect(node.effects.length).toBe(1)
|
||||
expect(node.effects[0]!.type).toBe('DROP_SHADOW')
|
||||
expect(node.effects[0]!.radius).toBe(12)
|
||||
expect(expectDefined(node.effects[0], 'first effect').type).toBe('DROP_SHADOW')
|
||||
expect(expectDefined(node.effects[0], 'first effect').radius).toBe(12)
|
||||
})
|
||||
|
||||
it('handles blur effect', async () => {
|
||||
const g = makeSceneGraph()
|
||||
const tree = Frame({ name: 'Blurred', blur: 8 })
|
||||
const result = await renderTree(g, tree)
|
||||
const node = g.nodes.get(result.id)!
|
||||
const node = getNodeOrThrow(g, result.id)
|
||||
|
||||
expect(node.effects.length).toBe(1)
|
||||
expect(node.effects[0]!.type).toBe('LAYER_BLUR')
|
||||
expect(node.effects[0]!.radius).toBe(8)
|
||||
expect(expectDefined(node.effects[0], 'first effect').type).toBe('LAYER_BLUR')
|
||||
expect(expectDefined(node.effects[0], 'first effect').radius).toBe(8)
|
||||
})
|
||||
|
||||
it('renders all primitive types', async () => {
|
||||
|
|
@ -298,7 +299,7 @@ describe('renderTree', () => {
|
|||
for (const { fn, expected } of types) {
|
||||
const tree = fn({ name: expected })
|
||||
const result = await renderTree(g, tree)
|
||||
expect(g.nodes.get(result.id)!.type).toBe(expected)
|
||||
expect(getNodeOrThrow(g, result.id).type).toBe(expected)
|
||||
}
|
||||
})
|
||||
|
||||
|
|
@ -322,9 +323,9 @@ describe('renderTreeNode', () => {
|
|||
const result = await renderTreeNode(g, tree)
|
||||
|
||||
expect(result.name).toBe('FromAI')
|
||||
const node = g.nodes.get(result.id)!
|
||||
const node = getNodeOrThrow(g, result.id)
|
||||
expect(node.childIds.length).toBe(1)
|
||||
const label = g.nodes.get(node.childIds[0]!)!
|
||||
const label = getNodeOrThrow(g, childIdAt(node, 0))
|
||||
expect(label.text).toBe('Button')
|
||||
})
|
||||
})
|
||||
|
|
@ -340,11 +341,11 @@ describe('renderJSX (string → scene graph)', () => {
|
|||
const [result] = await renderJSX(g, jsx)
|
||||
|
||||
expect(result.name).toBe('Test')
|
||||
const node = g.nodes.get(result.id)!
|
||||
const node = getNodeOrThrow(g, result.id)
|
||||
expect(node.type).toBe('FRAME')
|
||||
expect(node.childIds.length).toBe(1)
|
||||
|
||||
const text = g.nodes.get(node.childIds[0]!)!
|
||||
const text = getNodeOrThrow(g, childIdAt(node, 0))
|
||||
expect(text.text).toBe('World')
|
||||
})
|
||||
|
||||
|
|
@ -358,7 +359,7 @@ describe('renderJSX (string → scene graph)', () => {
|
|||
</Frame>
|
||||
`
|
||||
const [result] = await renderJSX(g, jsx)
|
||||
const card = g.nodes.get(result.id)!
|
||||
const card = getNodeOrThrow(g, result.id)
|
||||
|
||||
expect(card.layoutMode).toBe('VERTICAL')
|
||||
expect(card.childIds.length).toBe(3)
|
||||
|
|
@ -367,7 +368,7 @@ describe('renderJSX (string → scene graph)', () => {
|
|||
it('renders with position', async () => {
|
||||
const g = makeSceneGraph()
|
||||
const [result] = await renderJSX(g, '<Frame name="At" w={50} h={50} />', { x: 100, y: 200 })
|
||||
const node = g.nodes.get(result.id)!
|
||||
const node = getNodeOrThrow(g, result.id)
|
||||
|
||||
expect(node.x).toBe(100)
|
||||
expect(node.y).toBe(200)
|
||||
|
|
@ -384,7 +385,7 @@ describe('grid layout rendering', () => {
|
|||
Rectangle({ name: 'C', w: 50, h: 50 })
|
||||
)
|
||||
const result = await renderTree(g, tree)
|
||||
const frame = g.nodes.get(result.id)!
|
||||
const frame = getNodeOrThrow(g, result.id)
|
||||
|
||||
expect(frame.layoutMode).toBe('GRID')
|
||||
expect(frame.gridTemplateColumns).toEqual([
|
||||
|
|
@ -398,7 +399,7 @@ describe('grid layout rendering', () => {
|
|||
const g = makeSceneGraph()
|
||||
const tree = Frame({ name: 'Grid', w: 400, h: 200, grid: true, columns: '100 1fr 2fr' })
|
||||
const result = await renderTree(g, tree)
|
||||
const frame = g.nodes.get(result.id)!
|
||||
const frame = getNodeOrThrow(g, result.id)
|
||||
|
||||
expect(frame.gridTemplateColumns).toEqual([
|
||||
{ sizing: 'FIXED', value: 100 },
|
||||
|
|
@ -411,7 +412,7 @@ describe('grid layout rendering', () => {
|
|||
const g = makeSceneGraph()
|
||||
const tree = Frame({ name: 'Grid', w: 300, h: 200, grid: true, columns: 3 })
|
||||
const result = await renderTree(g, tree)
|
||||
const frame = g.nodes.get(result.id)!
|
||||
const frame = getNodeOrThrow(g, result.id)
|
||||
|
||||
expect(frame.gridTemplateColumns).toEqual([
|
||||
{ sizing: 'FR', value: 1 },
|
||||
|
|
@ -432,7 +433,7 @@ describe('grid layout rendering', () => {
|
|||
rowGap: 20
|
||||
})
|
||||
const result = await renderTree(g, tree)
|
||||
const frame = g.nodes.get(result.id)!
|
||||
const frame = getNodeOrThrow(g, result.id)
|
||||
|
||||
expect(frame.gridColumnGap).toBe(10)
|
||||
expect(frame.gridRowGap).toBe(20)
|
||||
|
|
@ -442,7 +443,7 @@ describe('grid layout rendering', () => {
|
|||
const g = makeSceneGraph()
|
||||
const tree = Frame({ name: 'Grid', w: 300, h: 200, grid: true, columns: '1fr 1fr', gap: 16 })
|
||||
const result = await renderTree(g, tree)
|
||||
const frame = g.nodes.get(result.id)!
|
||||
const frame = getNodeOrThrow(g, result.id)
|
||||
|
||||
expect(frame.gridColumnGap).toBe(16)
|
||||
expect(frame.gridRowGap).toBe(16)
|
||||
|
|
@ -452,7 +453,7 @@ describe('grid layout rendering', () => {
|
|||
const g = makeSceneGraph()
|
||||
const tree = Frame({ name: 'Grid', w: 300, h: 200, grid: true, columns: '1fr 1fr', p: 10 })
|
||||
const result = await renderTree(g, tree)
|
||||
const frame = g.nodes.get(result.id)!
|
||||
const frame = getNodeOrThrow(g, result.id)
|
||||
|
||||
expect(frame.paddingTop).toBe(10)
|
||||
expect(frame.paddingRight).toBe(10)
|
||||
|
|
@ -471,7 +472,7 @@ describe('grid layout rendering', () => {
|
|||
rows: '100 1fr'
|
||||
})
|
||||
const result = await renderTree(g, tree)
|
||||
const frame = g.nodes.get(result.id)!
|
||||
const frame = getNodeOrThrow(g, result.id)
|
||||
|
||||
expect(frame.gridTemplateRows).toEqual([
|
||||
{ sizing: 'FIXED', value: 100 },
|
||||
|
|
@ -552,7 +553,7 @@ describe('grid layout rendering', () => {
|
|||
</Frame>
|
||||
`
|
||||
const [result] = await renderJSX(g, jsx)
|
||||
const frame = g.nodes.get(result.id)!
|
||||
const frame = getNodeOrThrow(g, result.id)
|
||||
|
||||
expect(frame.layoutMode).toBe('GRID')
|
||||
expect(frame.gridTemplateColumns).toEqual([
|
||||
|
|
@ -578,11 +579,11 @@ describe('grid layout rendering', () => {
|
|||
Rectangle({ name: 'D', w: 50, h: 60 })
|
||||
)
|
||||
const result = await renderTree(g, tree)
|
||||
const frame = g.nodes.get(result.id)!
|
||||
const frame = getNodeOrThrow(g, result.id)
|
||||
expect(frame.gridTemplateRows).toEqual([])
|
||||
|
||||
computeLayout(g, result.id)
|
||||
const updated = g.nodes.get(result.id)!
|
||||
const updated = getNodeOrThrow(g, result.id)
|
||||
expect(updated.height).toBe(150)
|
||||
})
|
||||
|
||||
|
|
@ -623,7 +624,7 @@ describe('grid layout rendering', () => {
|
|||
Rectangle({ name: 'B', w: 50, h: 50 })
|
||||
)
|
||||
const result = await renderTree(g, tree)
|
||||
const frame = g.nodes.get(result.id)!
|
||||
const frame = getNodeOrThrow(g, result.id)
|
||||
expect(frame.layoutMode).toBe('GRID')
|
||||
})
|
||||
|
||||
|
|
@ -639,7 +640,10 @@ describe('grid layout rendering', () => {
|
|||
`
|
||||
const [result] = await renderJSX(g, jsx)
|
||||
computeAllLayouts(g)
|
||||
const grid = g.getChildren(result.id).find((c) => c.name === 'G')!
|
||||
const grid = expectDefined(
|
||||
g.getChildren(result.id).find((c) => c.name === 'G'),
|
||||
'grid node'
|
||||
)
|
||||
expect(grid.width).toBe(360)
|
||||
expect(grid.layoutMode).toBe('GRID')
|
||||
})
|
||||
|
|
@ -657,7 +661,10 @@ describe('grid layout rendering', () => {
|
|||
`
|
||||
const [result] = await renderJSX(g, jsx)
|
||||
computeAllLayouts(g)
|
||||
const grid = g.getChildren(result.id).find((c) => c.name === 'G')!
|
||||
const grid = expectDefined(
|
||||
g.getChildren(result.id).find((c) => c.name === 'G'),
|
||||
'grid node'
|
||||
)
|
||||
expect(grid.width).toBe(350)
|
||||
})
|
||||
|
||||
|
|
@ -676,8 +683,14 @@ describe('grid layout rendering', () => {
|
|||
`
|
||||
const [result] = await renderJSX(g, jsx)
|
||||
computeAllLayouts(g)
|
||||
const content = g.getChildren(result.id).find((c) => c.name === 'Content')!
|
||||
const grid = g.getChildren(content.id).find((c) => c.name === 'G')!
|
||||
const content = expectDefined(
|
||||
g.getChildren(result.id).find((c) => c.name === 'Content'),
|
||||
'content node'
|
||||
)
|
||||
const grid = expectDefined(
|
||||
g.getChildren(content.id).find((c) => c.name === 'G'),
|
||||
'grid node'
|
||||
)
|
||||
expect(content.width).toBe(780)
|
||||
expect(grid.width).toBe(724)
|
||||
})
|
||||
|
|
@ -695,7 +708,10 @@ describe('grid layout rendering', () => {
|
|||
`
|
||||
const [result] = await renderJSX(g, jsx)
|
||||
computeAllLayouts(g)
|
||||
const grid = g.getChildren(result.id).find((c) => c.name === 'G')!
|
||||
const grid = expectDefined(
|
||||
g.getChildren(result.id).find((c) => c.name === 'G'),
|
||||
'grid node'
|
||||
)
|
||||
expect(grid.height).toBe(460)
|
||||
})
|
||||
})
|
||||
|
|
@ -704,7 +720,7 @@ describe('text props round-trip', () => {
|
|||
it('lineHeight renders and exports', async () => {
|
||||
const g = makeSceneGraph()
|
||||
const [result] = await renderJSX(g, '<Text color="#000" lineHeight={24}>Hello</Text>')
|
||||
const n = g.getNode(result.id)!
|
||||
const n = getNodeOrThrow(g, result.id)
|
||||
expect(n.lineHeight).toBe(24)
|
||||
const jsx = sceneNodeToJSX(n.id, g)
|
||||
expect(jsx).toContain('lineHeight={24}')
|
||||
|
|
@ -713,7 +729,7 @@ describe('text props round-trip', () => {
|
|||
it('letterSpacing renders and exports', async () => {
|
||||
const g = makeSceneGraph()
|
||||
const [result] = await renderJSX(g, '<Text color="#000" letterSpacing={2}>Spaced</Text>')
|
||||
const n = g.getNode(result.id)!
|
||||
const n = getNodeOrThrow(g, result.id)
|
||||
expect(n.letterSpacing).toBe(2)
|
||||
const jsx = sceneNodeToJSX(n.id, g)
|
||||
expect(jsx).toContain('letterSpacing={2}')
|
||||
|
|
@ -722,7 +738,7 @@ describe('text props round-trip', () => {
|
|||
it('textDecoration renders and exports', async () => {
|
||||
const g = makeSceneGraph()
|
||||
const [result] = await renderJSX(g, '<Text color="#000" textDecoration="underline">Link</Text>')
|
||||
const n = g.getNode(result.id)!
|
||||
const n = getNodeOrThrow(g, result.id)
|
||||
expect(n.textDecoration).toBe('UNDERLINE')
|
||||
const jsx = sceneNodeToJSX(n.id, g)
|
||||
expect(jsx).toContain('textDecoration="underline"')
|
||||
|
|
@ -731,7 +747,7 @@ describe('text props round-trip', () => {
|
|||
it('textCase renders and exports', async () => {
|
||||
const g = makeSceneGraph()
|
||||
const [result] = await renderJSX(g, '<Text color="#000" textCase="upper">label</Text>')
|
||||
const n = g.getNode(result.id)!
|
||||
const n = getNodeOrThrow(g, result.id)
|
||||
expect(n.textCase).toBe('UPPER')
|
||||
const jsx = sceneNodeToJSX(n.id, g)
|
||||
expect(jsx).toContain('textCase="upper"')
|
||||
|
|
@ -740,7 +756,7 @@ describe('text props round-trip', () => {
|
|||
it('maxLines renders with truncation', async () => {
|
||||
const g = makeSceneGraph()
|
||||
const [result] = await renderJSX(g, '<Text color="#000" maxLines={2}>Long text here</Text>')
|
||||
const n = g.getNode(result.id)!
|
||||
const n = getNodeOrThrow(g, result.id)
|
||||
expect(n.maxLines).toBe(2)
|
||||
expect(n.textTruncation).toBe('ENDING')
|
||||
const jsx = sceneNodeToJSX(n.id, g)
|
||||
|
|
@ -750,7 +766,7 @@ describe('text props round-trip', () => {
|
|||
it('truncate without maxLines', async () => {
|
||||
const g = makeSceneGraph()
|
||||
const [result] = await renderJSX(g, '<Text color="#000" truncate>Overflow</Text>')
|
||||
const n = g.getNode(result.id)!
|
||||
const n = getNodeOrThrow(g, result.id)
|
||||
expect(n.textTruncation).toBe('ENDING')
|
||||
const jsx = sceneNodeToJSX(n.id, g)
|
||||
expect(jsx).toContain('truncate')
|
||||
|
|
@ -759,7 +775,7 @@ describe('text props round-trip', () => {
|
|||
it('defaults omit text props', async () => {
|
||||
const g = makeSceneGraph()
|
||||
const [result] = await renderJSX(g, '<Text color="#000">Plain</Text>')
|
||||
const n = g.getNode(result.id)!
|
||||
const n = getNodeOrThrow(g, result.id)
|
||||
expect(n.lineHeight).toBeNull()
|
||||
expect(n.letterSpacing).toBe(0)
|
||||
expect(n.textDecoration).toBe('NONE')
|
||||
|
|
@ -784,8 +800,8 @@ describe('text props round-trip', () => {
|
|||
</Frame>
|
||||
`
|
||||
)
|
||||
const card = g.getNode(result.id)!
|
||||
const title = g.getNode(card.childIds[0]!)!
|
||||
const card = getNodeOrThrow(g, result.id)
|
||||
const title = getNodeOrThrow(g, childIdAt(card, 0))
|
||||
expect(title.layoutAlignSelf).toBe('STRETCH')
|
||||
expect(title.textAutoResize).toBe('HEIGHT')
|
||||
const jsx = sceneNodeToJSX(title.id, g)
|
||||
|
|
@ -804,8 +820,8 @@ describe('text props round-trip', () => {
|
|||
</Frame>
|
||||
`
|
||||
)
|
||||
const row = g.getNode(result.id)!
|
||||
const value = g.getNode(row.childIds[1]!)!
|
||||
const row = getNodeOrThrow(g, result.id)
|
||||
const value = getNodeOrThrow(g, childIdAt(row, 1))
|
||||
expect(value.layoutGrow).toBe(1)
|
||||
const jsx = sceneNodeToJSX(value.id, g)
|
||||
expect(jsx).toContain('grow={1}')
|
||||
|
|
|
|||
|
|
@ -3,6 +3,8 @@ import { describe, test, expect } from 'bun:test'
|
|||
import { SceneGraph, TextEditor, UndoManager } from '@open-pencil/core'
|
||||
import { createTextActions } from '@open-pencil/core/editor'
|
||||
|
||||
import { expectDefined, getNodeOrThrow } from '../helpers/assert'
|
||||
|
||||
import type { StyleRun } from '@open-pencil/core'
|
||||
import type { EditorContext, EditorState } from '@open-pencil/core/editor'
|
||||
import type { CanvasKit } from 'canvaskit-wasm'
|
||||
|
|
@ -59,19 +61,21 @@ describe('text edit undo', () => {
|
|||
expect(textEditor.isActive).toBe(true)
|
||||
|
||||
textEditor.insert(' World', textNode)
|
||||
graph.updateNode(textNode.id, { text: textEditor.state!.text })
|
||||
graph.updateNode(textNode.id, {
|
||||
text: expectDefined(textEditor.state, 'text editor state').text
|
||||
})
|
||||
|
||||
actions.commitTextEdit()
|
||||
|
||||
expect(undo.canUndo).toBe(true)
|
||||
expect(undo.undoLabel).toBe('Edit text')
|
||||
expect(graph.getNode(textNode.id)!.text).toBe('Hello World')
|
||||
expect(getNodeOrThrow(graph, textNode.id).text).toBe('Hello World')
|
||||
|
||||
undo.undo()
|
||||
expect(graph.getNode(textNode.id)!.text).toBe('Hello')
|
||||
expect(getNodeOrThrow(graph, textNode.id).text).toBe('Hello')
|
||||
|
||||
undo.redo()
|
||||
expect(graph.getNode(textNode.id)!.text).toBe('Hello World')
|
||||
expect(getNodeOrThrow(graph, textNode.id).text).toBe('Hello World')
|
||||
})
|
||||
|
||||
test('commitTextEdit does not push undo when text unchanged', () => {
|
||||
|
|
@ -89,18 +93,22 @@ describe('text edit undo', () => {
|
|||
actions.startTextEditing(textNode.id)
|
||||
|
||||
textEditor.insert(' Beautiful', textNode)
|
||||
graph.updateNode(textNode.id, { text: textEditor.state!.text })
|
||||
graph.updateNode(textNode.id, {
|
||||
text: expectDefined(textEditor.state, 'text editor state').text
|
||||
})
|
||||
|
||||
textEditor.insert(' World', textNode)
|
||||
graph.updateNode(textNode.id, { text: textEditor.state!.text })
|
||||
graph.updateNode(textNode.id, {
|
||||
text: expectDefined(textEditor.state, 'text editor state').text
|
||||
})
|
||||
|
||||
actions.commitTextEdit()
|
||||
|
||||
expect(graph.getNode(textNode.id)!.text).toBe('Hello Beautiful World')
|
||||
expect(getNodeOrThrow(graph, textNode.id).text).toBe('Hello Beautiful World')
|
||||
expect(undo.canUndo).toBe(true)
|
||||
|
||||
undo.undo()
|
||||
expect(graph.getNode(textNode.id)!.text).toBe('Hello')
|
||||
expect(getNodeOrThrow(graph, textNode.id).text).toBe('Hello')
|
||||
})
|
||||
|
||||
test('sequential edits create separate undo entries', () => {
|
||||
|
|
@ -108,21 +116,25 @@ describe('text edit undo', () => {
|
|||
|
||||
actions.startTextEditing(textNode.id)
|
||||
textEditor.insert('!', textNode)
|
||||
graph.updateNode(textNode.id, { text: textEditor.state!.text })
|
||||
graph.updateNode(textNode.id, {
|
||||
text: expectDefined(textEditor.state, 'text editor state').text
|
||||
})
|
||||
actions.commitTextEdit()
|
||||
|
||||
actions.startTextEditing(textNode.id)
|
||||
textEditor.insert('!', textNode)
|
||||
graph.updateNode(textNode.id, { text: textEditor.state!.text })
|
||||
graph.updateNode(textNode.id, {
|
||||
text: expectDefined(textEditor.state, 'text editor state').text
|
||||
})
|
||||
actions.commitTextEdit()
|
||||
|
||||
expect(graph.getNode(textNode.id)!.text).toBe('Hello!!')
|
||||
expect(getNodeOrThrow(graph, textNode.id).text).toBe('Hello!!')
|
||||
|
||||
undo.undo()
|
||||
expect(graph.getNode(textNode.id)!.text).toBe('Hello!')
|
||||
expect(getNodeOrThrow(graph, textNode.id).text).toBe('Hello!')
|
||||
|
||||
undo.undo()
|
||||
expect(graph.getNode(textNode.id)!.text).toBe('Hello')
|
||||
expect(getNodeOrThrow(graph, textNode.id).text).toBe('Hello')
|
||||
})
|
||||
|
||||
test('undo restores styleRuns when they changed during editing', () => {
|
||||
|
|
@ -138,20 +150,23 @@ describe('text edit undo', () => {
|
|||
{ start: 0, length: 5, style: { fontWeight: 700 } },
|
||||
{ start: 5, length: 6, style: { fontWeight: 400 } }
|
||||
]
|
||||
graph.updateNode(textNode.id, { text: textEditor.state!.text, styleRuns: newRuns })
|
||||
graph.updateNode(textNode.id, {
|
||||
text: expectDefined(textEditor.state, 'text editor state').text,
|
||||
styleRuns: newRuns
|
||||
})
|
||||
|
||||
actions.commitTextEdit()
|
||||
|
||||
expect(graph.getNode(textNode.id)!.styleRuns).toEqual(newRuns)
|
||||
expect(getNodeOrThrow(graph, textNode.id).styleRuns).toEqual(newRuns)
|
||||
expect(undo.canUndo).toBe(true)
|
||||
|
||||
undo.undo()
|
||||
expect(graph.getNode(textNode.id)!.text).toBe('Hello')
|
||||
expect(graph.getNode(textNode.id)!.styleRuns).toEqual([boldRun])
|
||||
expect(getNodeOrThrow(graph, textNode.id).text).toBe('Hello')
|
||||
expect(getNodeOrThrow(graph, textNode.id).styleRuns).toEqual([boldRun])
|
||||
|
||||
undo.redo()
|
||||
expect(graph.getNode(textNode.id)!.text).toBe('Hello World')
|
||||
expect(graph.getNode(textNode.id)!.styleRuns).toEqual(newRuns)
|
||||
expect(getNodeOrThrow(graph, textNode.id).text).toBe('Hello World')
|
||||
expect(getNodeOrThrow(graph, textNode.id).styleRuns).toEqual(newRuns)
|
||||
})
|
||||
|
||||
test('undo entry is pushed when only styleRuns changed', () => {
|
||||
|
|
@ -167,7 +182,7 @@ describe('text edit undo', () => {
|
|||
expect(undo.canUndo).toBe(true)
|
||||
|
||||
undo.undo()
|
||||
expect(graph.getNode(textNode.id)!.styleRuns).toEqual([])
|
||||
expect(getNodeOrThrow(graph, textNode.id).styleRuns).toEqual([])
|
||||
})
|
||||
|
||||
test('no undo entry when neither text nor styleRuns changed', () => {
|
||||
|
|
|
|||
16
tests/helpers/assert.ts
Normal file
16
tests/helpers/assert.ts
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
import type { SceneGraph, SceneNode } from '@open-pencil/core'
|
||||
|
||||
export function expectDefined<T>(value: T | null | undefined, label = 'value'): NonNullable<T> {
|
||||
if (value == null) {
|
||||
throw new Error(`${label} was expected to be defined`)
|
||||
}
|
||||
return value
|
||||
}
|
||||
|
||||
export function getNodeOrThrow(graph: SceneGraph, id: string): SceneNode {
|
||||
return expectDefined(graph.getNode(id), `node ${id}`)
|
||||
}
|
||||
|
||||
export function childIdAt(node: SceneNode, index: number): string {
|
||||
return expectDefined(node.childIds[index], `child ${index} of ${node.id}`)
|
||||
}
|
||||
Loading…
Reference in a new issue