openpencil/tests/engine/vue/controls/property-preview.test.ts
Danila Poyarkov d16400b3e6 feat(editor): checkpoint live interaction improvements
Unify preview-aware transforms across scene drawing, labels, selection and input. Give live property and creation edits explicit preview ownership, preserve cancellation and one-step undo, and reuse bounded text preparation resources.

Include retained device-grid handling, worktree HMR coverage, and nested/reflected interaction regressions in this cohesive progress checkpoint.

Validation: full check passes; 734 scoped unit tests and 53 targeted browser tests pass. This is NOT merge-ready: the unchanged exact nested filled-section held/released regression still fails with 63 differing pixels (maximum channel delta 5/255). The browser pass count excludes that separately run failure and the previously classified paint-field-width baseline. Raster-origin investigation and broader acceptance remain outstanding.
2026-09-15 11:36:17 +03:00

140 lines
5 KiB
TypeScript

import { describe, expect, test } from 'bun:test'
import { computed, effectScope } from 'vue'
import { createEditor } from '@open-pencil/core/editor'
import { createLayoutActions } from '#vue/controls/layout/helpers'
import { usePropScrub } from '#vue/controls/prop-scrub/use'
describe('property previews', () => {
test('pins targets through selection changes and releases previews on scope disposal', () => {
const editor = createEditor()
const scope = effectScope()
try {
const first = editor.graph.createNode('RECTANGLE', editor.state.currentPageId, { x: 10 })
const second = editor.graph.createNode('RECTANGLE', editor.state.currentPageId, { x: 30 })
editor.select([first.id])
const scrub = scope.run(() => usePropScrub(editor))
if (!scrub) throw new Error('Scrub scope unavailable')
scrub.updateProp([first], 'x', 50)
editor.select([second.id])
scrub.updateProp([second], 'x', 80)
scrub.commitProp([second], 'x', 80, 30)
expect([first.x, second.x]).toEqual([10, 30])
expect(editor.undo.canUndo).toBe(false)
scrub.updateProp([second], 'x', 90)
scope.stop()
expect(second.x).toBe(30)
expect(editor.isInteractiveEditing()).toBe(false)
} finally {
scope.stop()
editor.dispose()
}
})
test('size preview owns the Hug-to-Fixed change and cancels or undoes it with the value', () => {
const editor = createEditor()
try {
const frame = editor.graph.createNode('FRAME', editor.state.currentPageId, {
width: 100,
height: 40,
layoutMode: 'HORIZONTAL',
primaryAxisSizing: 'HUG',
counterAxisSizing: 'FIXED'
})
editor.graph.createNode('RECTANGLE', frame.id, { width: 100, height: 20 })
editor.runLayoutForNode(frame.id)
const original = frame.width
const actions = createLayoutActions({
editor,
node: computed(() => frame),
isInAutoLayout: computed(() => false)
})
actions.updateAxisSize('width', 200)
expect(frame.primaryAxisSizing).toBe('FIXED')
expect(editor.undo.canUndo).toBe(false)
actions.cancelPreview()
expect(frame.primaryAxisSizing).toBe('HUG')
expect(frame.width).toBe(original)
actions.updateAxisSize('width', 200)
actions.commitAxisSize('width', 200, original)
editor.undoAction()
expect(frame.primaryAxisSizing).toBe('HUG')
expect(frame.width).toBe(original)
expect(editor.undo.canUndo).toBe(false)
} finally {
editor.dispose()
}
})
test('updates layout live without committed events, then commits and undoes the whole result', () => {
const editor = createEditor()
try {
const frame = editor.graph.createNode('FRAME', editor.state.currentPageId, {
width: 200,
height: 100,
layoutMode: 'HORIZONTAL',
primaryAxisSizing: 'FIXED',
counterAxisSizing: 'FIXED'
})
const child = editor.graph.createNode('RECTANGLE', frame.id, {
width: 100,
height: 20,
layoutGrow: 1
})
editor.updateNode(frame.id, { width: 200 })
expect(child.width).toBe(200)
const sceneVersion = editor.state.sceneVersion
let committed = 0
editor.onEditorEvent('node:updated', () => {
committed++
})
const preview = editor.beginNodePreview('Resize')
preview.update(frame.id, { width: 320 })
expect(child.width).toBe(320)
expect(editor.state.sceneVersion).toBe(sceneVersion)
expect(committed).toBe(0)
expect(editor.undo.canUndo).toBe(false)
preview.commit()
expect(committed).toBeGreaterThan(0)
editor.undoAction()
expect(frame.width).toBe(200)
expect(child.width).toBe(200)
expect(editor.undo.canUndo).toBe(false)
editor.redoAction()
expect(frame.width).toBe(320)
expect(child.width).toBe(320)
} finally {
editor.dispose()
}
})
test('restores different multi-selection values on cancel and groups a completed scrub in one undo', () => {
const editor = createEditor()
try {
const first = editor.graph.createNode('RECTANGLE', editor.state.currentPageId, { x: 10 })
const second = editor.graph.createNode('RECTANGLE', editor.state.currentPageId, { x: 30 })
const nodes = [first, second]
const scrub = usePropScrub(editor)
const version = editor.state.sceneVersion
scrub.updateProp(nodes, 'x', 50)
expect(nodes.map((n) => n.x)).toEqual([50, 50])
scrub.cancelProp(nodes, 'x')
expect(nodes.map((n) => n.x)).toEqual([10, 30])
// Cancel restores through preview events, then reconciles the scene once.
expect(editor.state.sceneVersion).toBe(version + 1)
expect(editor.undo.canUndo).toBe(false)
scrub.updateProp(nodes, 'x', 60)
scrub.commitProp(nodes, 'x', 60, 0)
editor.undoAction()
expect(nodes.map((n) => n.x)).toEqual([10, 30])
expect(editor.undo.canUndo).toBe(false)
editor.redoAction()
expect(nodes.map((n) => n.x)).toEqual([60, 60])
} finally {
editor.dispose()
}
})
})