openpencil/tests/engine/editor/clipboard/paste-to-replace.test.ts
2026-05-17 16:08:03 +03:00

187 lines
6 KiB
TypeScript

import { describe, expect, test } from 'bun:test'
import { buildOpenPencilClipboardHTML } from '@open-pencil/core/clipboard'
import { createEditor } from '@open-pencil/core/editor'
import type { Editor } from '@open-pencil/core/editor'
function copiedRectangleHtml(name = 'Pasted') {
const source = createEditor()
const sourcePageId = source.state.currentPageId
const pasted = source.graph.createNode('RECTANGLE', sourcePageId, {
name,
x: 0,
y: 0,
width: 20,
height: 20
})
return buildOpenPencilClipboardHTML([pasted], source.graph)
}
function copiedFrameHtml() {
const source = createEditor()
const sourcePageId = source.state.currentPageId
const frame = source.graph.createNode('FRAME', sourcePageId, {
name: 'Pasted frame',
x: 0,
y: 0,
width: 40,
height: 40
})
source.graph.createNode('RECTANGLE', frame.id, {
name: 'Pasted child',
x: 5,
y: 6,
width: 10,
height: 12
})
return buildOpenPencilClipboardHTML([frame], source.graph)
}
function createTarget(editor: Editor, parentId = editor.state.currentPageId) {
return editor.graph.createNode('ELLIPSE', parentId, {
name: 'Target',
x: 100,
y: 100,
width: 40,
height: 40
})
}
describe('paste to replace', () => {
test('replaces selected nodes with pasted OpenPencil nodes', async () => {
const html = copiedRectangleHtml()
const editor = createEditor()
const pageId = editor.state.currentPageId
const target = createTarget(editor)
editor.select([target.id])
await editor.pasteFromHTML(html, undefined, { replaceSelection: true })
expect(editor.graph.getNode(target.id)).toBeUndefined()
const [createdId] = [...editor.state.selectedIds]
const created = editor.graph.getNode(createdId)
expect(created?.name).toBe('Pasted')
expect(created?.parentId).toBe(pageId)
expect(created?.x).toBe(110)
expect(created?.y).toBe(110)
})
test('undo and redo preserve replaced and pasted subtrees', async () => {
const html = copiedRectangleHtml()
const editor = createEditor()
const pageId = editor.state.currentPageId
const target = createTarget(editor)
editor.select([target.id])
await editor.pasteFromHTML(html, undefined, { replaceSelection: true })
const [createdId] = [...editor.state.selectedIds]
editor.undo.undo()
expect(editor.graph.getNode(createdId)).toBeUndefined()
expect(editor.graph.getNode(target.id)?.parentId).toBe(pageId)
expect(editor.state.selectedIds).toEqual(new Set([target.id]))
editor.undo.redo()
expect(editor.graph.getNode(target.id)).toBeUndefined()
expect(editor.graph.getNode(createdId)?.parentId).toBe(pageId)
expect(editor.state.selectedIds).toEqual(new Set([createdId]))
})
test('falls back to regular paste when all selected targets are locked', async () => {
const html = copiedRectangleHtml()
const editor = createEditor()
const pageId = editor.state.currentPageId
const target = editor.graph.createNode('ELLIPSE', pageId, {
name: 'Locked target',
x: 100,
y: 100,
width: 40,
height: 40,
locked: true
})
editor.select([target.id])
await editor.pasteFromHTML(html, undefined, { replaceSelection: true })
expect(editor.graph.getNode(target.id)?.parentId).toBe(pageId)
const [createdId] = [...editor.state.selectedIds]
expect(createdId).not.toBe(target.id)
expect(editor.graph.getNode(createdId)?.parentId).toBe(pageId)
})
test('replaces only top-level selected targets', async () => {
const html = copiedRectangleHtml()
const editor = createEditor()
const pageId = editor.state.currentPageId
const parent = editor.graph.createNode('FRAME', pageId, {
name: 'Parent target',
x: 100,
y: 100,
width: 80,
height: 80
})
const child = editor.graph.createNode('RECTANGLE', parent.id, {
name: 'Child target',
x: 10,
y: 10,
width: 20,
height: 20
})
editor.select([parent.id, child.id])
await editor.pasteFromHTML(html, undefined, { replaceSelection: true })
expect(editor.graph.getNode(parent.id)).toBeUndefined()
expect(editor.graph.getNode(child.id)).toBeUndefined()
const [createdId] = [...editor.state.selectedIds]
expect(editor.graph.getNode(createdId)?.parentId).toBe(pageId)
})
test('replaces targets inside a frame and preserves parent order through undo and redo', async () => {
const html = copiedRectangleHtml()
const editor = createEditor()
const pageId = editor.state.currentPageId
const frame = editor.graph.createNode('FRAME', pageId, {
name: 'Container',
x: 50,
y: 50,
width: 300,
height: 300
})
const before = editor.graph.createNode('RECTANGLE', frame.id, { name: 'Before', x: 0, y: 0 })
const target = createTarget(editor, frame.id)
const after = editor.graph.createNode('RECTANGLE', frame.id, { name: 'After', x: 200, y: 200 })
editor.select([target.id])
await editor.pasteFromHTML(html, undefined, { replaceSelection: true })
const [createdId] = [...editor.state.selectedIds]
expect(editor.graph.getNode(createdId)?.parentId).toBe(frame.id)
expect(editor.graph.getNode(frame.id)?.childIds).toEqual([before.id, createdId, after.id])
editor.undo.undo()
expect(editor.graph.getNode(frame.id)?.childIds).toEqual([before.id, target.id, after.id])
editor.undo.redo()
expect(editor.graph.getNode(frame.id)?.childIds).toEqual([before.id, createdId, after.id])
})
test('preserves pasted child subtrees', async () => {
const html = copiedFrameHtml()
const editor = createEditor()
const target = createTarget(editor)
editor.select([target.id])
await editor.pasteFromHTML(html, undefined, { replaceSelection: true })
const [createdId] = [...editor.state.selectedIds]
const created = editor.graph.getNode(createdId)
expect(created?.type).toBe('FRAME')
expect(created?.childIds).toHaveLength(1)
const childId = created?.childIds[0]
expect(childId ? editor.graph.getNode(childId)?.name : undefined).toBe('Pasted child')
})
})