kopia lustrzana https://github.com/Tldraw/Tldraw
454 wiersze
14 KiB
TypeScript
454 wiersze
14 KiB
TypeScript
import { TLArrowShape, createShapeId } from '@tldraw/tlschema'
|
|
import { TestEditor } from '../TestEditor'
|
|
|
|
let editor: TestEditor
|
|
|
|
const ids = {
|
|
box1: createShapeId('box1'),
|
|
box2: createShapeId('box2'),
|
|
box3: createShapeId('box3'),
|
|
box4: createShapeId('box4'),
|
|
arrow1: createShapeId('arrow1'),
|
|
}
|
|
|
|
beforeEach(() => {
|
|
editor = new TestEditor()
|
|
|
|
editor.selectAll().deleteShapes()
|
|
})
|
|
|
|
afterEach(() => {
|
|
editor.selectAll().deleteShapes()
|
|
})
|
|
|
|
const doMockClipboard = () => {
|
|
const context: { current: any } = { current: undefined }
|
|
|
|
Object.assign(window.navigator, {
|
|
clipboard: {
|
|
write: jest.fn((content: any) => {
|
|
context.current = content
|
|
}),
|
|
},
|
|
})
|
|
|
|
globalThis.ClipboardItem = jest.fn((payload: any) => payload)
|
|
|
|
return context
|
|
}
|
|
|
|
const assertClipboardOfCorrectShape = async (_clipboardContent: any) => {
|
|
return true
|
|
// expect(clipboardContent.length).toBe(1)
|
|
// expect(clipboardContent[0]['text/html']).toBeDefined()
|
|
// expect(clipboardContent[0]['text/plain']).toBeDefined()
|
|
// expect(clipboardContent.length).toBe(1)
|
|
// expect(await readAsText(clipboardContent[0]['text/html'])).toMatch(/^<tldraw>(.*)<\/tldraw>$/)
|
|
}
|
|
|
|
describe('When copying and pasting', () => {
|
|
it('does nothing when copying with nothing is selected', async () => {
|
|
const mockClipboard = doMockClipboard()
|
|
|
|
editor.createShapes([
|
|
{ id: ids.box1, type: 'geo', x: 100, y: 100, props: { w: 100, h: 100 } },
|
|
{ id: ids.box2, type: 'geo', x: 300, y: 300, props: { w: 100, h: 100 } },
|
|
])
|
|
|
|
editor.copy()
|
|
|
|
expect(mockClipboard.current).toBeUndefined()
|
|
})
|
|
|
|
it('copies the selected shapes and pastes when ALL shapes still in the viewport', async () => {
|
|
const mockClipboard = doMockClipboard()
|
|
|
|
editor.createShapes([
|
|
{ id: ids.box1, type: 'geo', x: 100, y: 100, props: { w: 100, h: 100 } },
|
|
{ id: ids.box2, type: 'geo', x: 300, y: 300, props: { w: 100, h: 100 } },
|
|
])
|
|
|
|
const shapesBefore = editor.shapesArray
|
|
editor.selectAll().copy()
|
|
|
|
await assertClipboardOfCorrectShape(mockClipboard.current)
|
|
|
|
const testOffsetX = 100
|
|
const testOffsetY = 100
|
|
editor.setCamera(editor.camera.x - testOffsetX, editor.camera.y - testOffsetY, editor.zoomLevel)
|
|
|
|
editor.paste()
|
|
const shapesAfter = editor.shapesArray
|
|
|
|
// We should not have changed the original shapes
|
|
expect(shapesBefore[0]).toMatchObject(shapesAfter[0])
|
|
expect(shapesBefore[1]).toMatchObject(shapesAfter[1])
|
|
|
|
const box1a = shapesAfter[0]
|
|
const box2a = shapesAfter[1]
|
|
const box1b = shapesAfter[2]
|
|
const box2b = shapesAfter[3]
|
|
|
|
// The new shapes should match the old shapes, except for their id
|
|
expect(shapesAfter.length).toBe(shapesBefore.length * 2)
|
|
expect(box1b).toMatchObject({ ...box1a, id: box1b.id, index: 'a3' })
|
|
expect(box2b).toMatchObject({ ...box2a, id: box2b.id, index: 'a4' })
|
|
})
|
|
|
|
it.todo('pastes at the correct child index (top of the current focus layer list)')
|
|
|
|
it.todo(
|
|
'does not move shapes that are outside of the viewport when pasting into the children of an existing / non-copied shape'
|
|
)
|
|
|
|
it('copies the selected shapes and pastes when SOME shapes still in the viewport', async () => {
|
|
const mockClipboard = doMockClipboard()
|
|
|
|
editor.createShapes([
|
|
{ id: ids.box1, type: 'geo', x: -2000, y: -100, props: { w: 100, h: 100 } },
|
|
{ id: ids.box2, type: 'geo', x: 1900, y: 0, props: { w: 100, h: 100 } },
|
|
])
|
|
|
|
const shapesBefore = editor.shapesArray
|
|
editor.selectAll().copy()
|
|
|
|
await assertClipboardOfCorrectShape(mockClipboard.current)
|
|
|
|
const testOffsetX = 1800
|
|
const testOffsetY = 0
|
|
editor.setCamera(editor.camera.x - testOffsetX, editor.camera.y - testOffsetY, editor.zoomLevel)
|
|
|
|
editor.paste()
|
|
const shapesAfter = editor.shapesArray
|
|
|
|
// We should not have changed the original shapes
|
|
expect(shapesBefore[0]).toMatchObject(shapesAfter[0])
|
|
expect(shapesBefore[1]).toMatchObject(shapesAfter[1])
|
|
|
|
const box1a = shapesAfter[0]
|
|
const box2a = shapesAfter[1]
|
|
const box1b = shapesAfter[2]
|
|
const box2b = shapesAfter[3]
|
|
|
|
// The new shapes should match the old shapes, except for their id
|
|
expect(shapesAfter.length).toBe(shapesBefore.length * 2)
|
|
expect(box1b).toMatchObject({ ...box1a, id: box1b.id, index: 'a3' })
|
|
expect(box2b).toMatchObject({ ...box2a, id: box2b.id, index: 'a4' })
|
|
})
|
|
|
|
it('copies the selected shapes and pastes when NO shapes still in the viewport', async () => {
|
|
const mockClipboard = doMockClipboard()
|
|
|
|
editor.createShapes([
|
|
// NOTE: These shapes are centered around [0,0] to make calcs in assertions easier.
|
|
{ id: ids.box1, type: 'geo', x: -100, y: -100, props: { w: 100, h: 100 } },
|
|
{ id: ids.box2, type: 'geo', x: 0, y: 0, props: { w: 100, h: 100 } },
|
|
])
|
|
|
|
const shapesBefore = editor.shapesArray
|
|
editor.selectAll().copy()
|
|
|
|
await assertClipboardOfCorrectShape(mockClipboard.current)
|
|
|
|
const testOffsetX = 2000
|
|
const testOffsetY = 3000
|
|
|
|
const { w: screenWidth, h: screenHeight } = editor.viewportScreenBounds
|
|
editor.setCamera(editor.camera.x - testOffsetX, editor.camera.y - testOffsetY, editor.zoomLevel)
|
|
|
|
editor.paste()
|
|
const shapesAfter = editor.shapesArray
|
|
|
|
// We should not have changed the original shapes
|
|
expect(shapesBefore[0]).toMatchObject(shapesAfter[0])
|
|
expect(shapesBefore[1]).toMatchObject(shapesAfter[1])
|
|
|
|
const box1a = shapesAfter[0]
|
|
const box2a = shapesAfter[1]
|
|
const box1b = shapesAfter[2]
|
|
const box2b = shapesAfter[3]
|
|
|
|
// The new shapes should match the old shapes, except for the should be positioned on the new viewport center.
|
|
expect(shapesAfter.length).toBe(shapesBefore.length * 2)
|
|
expect(box1b).toMatchObject({
|
|
...box1a,
|
|
id: box1b.id,
|
|
index: 'a3',
|
|
x: testOffsetX + screenWidth / 2 + box1a.x,
|
|
y: testOffsetY + screenHeight / 2 + box1a.y,
|
|
})
|
|
expect(box2b).toMatchObject({
|
|
...box2a,
|
|
id: box2b.id,
|
|
index: 'a4',
|
|
x: testOffsetX + screenWidth / 2 + box2a.x,
|
|
y: testOffsetY + screenHeight / 2 + box2a.y,
|
|
})
|
|
})
|
|
|
|
it('creates new bindings for arrows when pasting', async () => {
|
|
const mockClipboard = doMockClipboard()
|
|
|
|
editor.createShapes([
|
|
{ id: ids.box1, type: 'geo', x: 100, y: 100, props: { w: 100, h: 100 } },
|
|
{ id: ids.box2, type: 'geo', x: 300, y: 300, props: { w: 100, h: 100 } },
|
|
{
|
|
id: ids.arrow1,
|
|
type: 'arrow',
|
|
x: 150,
|
|
y: 150,
|
|
props: {
|
|
start: {
|
|
type: 'binding',
|
|
boundShapeId: ids.box1,
|
|
isExact: false,
|
|
normalizedAnchor: { x: 0.5, y: 0.5 },
|
|
},
|
|
end: {
|
|
type: 'binding',
|
|
boundShapeId: ids.box2,
|
|
isExact: false,
|
|
normalizedAnchor: { x: 0.5, y: 0.5 },
|
|
},
|
|
},
|
|
},
|
|
])
|
|
|
|
const shapesBefore = editor.shapesArray
|
|
editor.selectAll().copy()
|
|
|
|
// Test the shape of the clipboard data.
|
|
await assertClipboardOfCorrectShape(mockClipboard.current)
|
|
|
|
editor.paste()
|
|
const shapesAfter = editor.shapesArray
|
|
|
|
// We should not have changed the original shapes
|
|
expect(shapesBefore[0]).toMatchObject(shapesAfter[0])
|
|
expect(shapesBefore[1]).toMatchObject(shapesAfter[1])
|
|
expect(shapesBefore[2]).toMatchObject(shapesAfter[2])
|
|
|
|
const box1a = shapesAfter[0]
|
|
const box2a = shapesAfter[1]
|
|
const arrow1a = shapesAfter[2] as TLArrowShape
|
|
|
|
const box1b = shapesAfter[3]
|
|
const box2b = shapesAfter[4]
|
|
const arrow1b = shapesAfter[5]
|
|
|
|
// The new shapes should match the old shapes, except for their id and the arrow's bindings!
|
|
expect(shapesAfter.length).toBe(shapesBefore.length * 2)
|
|
expect(box1b).toMatchObject({ ...box1a, id: box1b.id, index: 'a4' })
|
|
expect(box2b).toMatchObject({ ...box2a, id: box2b.id, index: 'a5' })
|
|
expect(arrow1b).toMatchObject({
|
|
...arrow1a,
|
|
id: arrow1b.id,
|
|
index: 'a6',
|
|
props: {
|
|
...arrow1a.props,
|
|
start: { ...arrow1a.props.start, boundShapeId: box1b.id },
|
|
end: { ...arrow1a.props.end, boundShapeId: box2b.id },
|
|
},
|
|
})
|
|
})
|
|
|
|
it('does nothing when cutting with nothing is selected', async () => {
|
|
const mockClipboard = doMockClipboard()
|
|
|
|
editor.createShapes([
|
|
{ id: ids.box1, type: 'geo', x: 100, y: 100, props: { w: 100, h: 100 } },
|
|
{ id: ids.box2, type: 'geo', x: 300, y: 300, props: { w: 100, h: 100 } },
|
|
])
|
|
|
|
editor.cut()
|
|
|
|
expect(mockClipboard.current).toBeUndefined()
|
|
})
|
|
|
|
it('cuts the selected shapes and pastes when ALL shapes still in the viewport', async () => {
|
|
const mockClipboard = doMockClipboard()
|
|
|
|
editor.createShapes([
|
|
{ id: ids.box1, type: 'geo', x: 100, y: 100, props: { w: 100, h: 100 } },
|
|
{ id: ids.box2, type: 'geo', x: 300, y: 300, props: { w: 100, h: 100 } },
|
|
])
|
|
|
|
const shapesBefore = editor.shapesArray
|
|
editor.selectAll().cut()
|
|
|
|
await assertClipboardOfCorrectShape(mockClipboard.current)
|
|
|
|
const testOffsetX = 100
|
|
const testOffsetY = 100
|
|
editor.setCamera(editor.camera.x - testOffsetX, editor.camera.y - testOffsetY, editor.zoomLevel)
|
|
|
|
editor.paste()
|
|
const shapesAfter = editor.shapesArray
|
|
|
|
// The new shapes should match the old shapes, except for their id
|
|
expect(shapesAfter.length).toBe(shapesBefore.length)
|
|
expect(shapesAfter[0]).toMatchObject({ ...shapesBefore[0], id: shapesAfter[0].id })
|
|
expect(shapesAfter[1]).toMatchObject({ ...shapesBefore[1], id: shapesAfter[1].id })
|
|
})
|
|
|
|
it('cuts the selected shapes and pastes when SOME shapes still in the viewport', async () => {
|
|
const mockClipboard = doMockClipboard()
|
|
|
|
editor.createShapes([
|
|
{ id: ids.box1, type: 'geo', x: -2000, y: -100, props: { w: 100, h: 100 } },
|
|
{ id: ids.box2, type: 'geo', x: 1900, y: 0, props: { w: 100, h: 100 } },
|
|
])
|
|
|
|
const shapesBefore = editor.shapesArray
|
|
editor.selectAll().cut()
|
|
|
|
await assertClipboardOfCorrectShape(mockClipboard.current)
|
|
|
|
const testOffsetX = 1800
|
|
const testOffsetY = 0
|
|
editor.setCamera(editor.camera.x - testOffsetX, editor.camera.y - testOffsetY, editor.zoomLevel)
|
|
|
|
editor.paste()
|
|
const shapesAfter = editor.shapesArray
|
|
|
|
// The new shapes should match the old shapes, except for their id
|
|
expect(shapesAfter.length).toBe(shapesBefore.length)
|
|
expect(shapesAfter[0]).toMatchObject({ ...shapesBefore[0], id: shapesAfter[0].id })
|
|
expect(shapesAfter[1]).toMatchObject({ ...shapesBefore[1], id: shapesAfter[1].id })
|
|
})
|
|
|
|
it('cuts the selected shapes and pastes when NO shapes still in the viewport', async () => {
|
|
const mockClipboard = doMockClipboard()
|
|
|
|
editor.createShapes([
|
|
// NOTE: These shapes are centered around [0,0] to make calcs in assertions easier.
|
|
{ id: ids.box1, type: 'geo', x: -100, y: -100, props: { w: 100, h: 100 } },
|
|
{ id: ids.box2, type: 'geo', x: 0, y: 0, props: { w: 100, h: 100 } },
|
|
])
|
|
|
|
const shapesBefore = editor.shapesArray
|
|
editor.selectAll().cut()
|
|
|
|
await assertClipboardOfCorrectShape(mockClipboard.current)
|
|
|
|
const testOffsetX = 2000
|
|
const testOffsetY = 3000
|
|
|
|
const { w: screenWidth, h: screenHeight } = editor.viewportScreenBounds
|
|
editor.setCamera(editor.camera.x - testOffsetX, editor.camera.y - testOffsetY, editor.zoomLevel)
|
|
|
|
editor.paste()
|
|
const shapesAfter = editor.shapesArray
|
|
|
|
// The new shapes should match the old shapes, except for the should be positioned on the new viewport center.
|
|
expect(shapesAfter.length).toBe(shapesBefore.length)
|
|
expect(shapesAfter[0]).toMatchObject({
|
|
...shapesBefore[0],
|
|
id: shapesAfter[0].id,
|
|
x: testOffsetX + screenWidth / 2 + shapesBefore[0].x,
|
|
y: testOffsetY + screenHeight / 2 + shapesBefore[0].y,
|
|
})
|
|
expect(shapesAfter[1]).toMatchObject({
|
|
...shapesBefore[1],
|
|
id: shapesAfter[1].id,
|
|
x: testOffsetX + screenWidth / 2 + shapesBefore[1].x,
|
|
y: testOffsetY + screenHeight / 2 + shapesBefore[1].y,
|
|
})
|
|
})
|
|
|
|
it('creates new bindings for arrows when pasting', async () => {
|
|
const mockClipboard = doMockClipboard()
|
|
|
|
editor.createShapes([
|
|
{ id: ids.box1, type: 'geo', x: 100, y: 100, props: { w: 100, h: 100 } },
|
|
{ id: ids.box2, type: 'geo', x: 300, y: 300, props: { w: 100, h: 100 } },
|
|
{
|
|
id: ids.arrow1,
|
|
type: 'arrow',
|
|
x: 150,
|
|
y: 150,
|
|
props: {
|
|
start: {
|
|
type: 'binding',
|
|
boundShapeId: ids.box1,
|
|
isExact: false,
|
|
normalizedAnchor: { x: 0.5, y: 0.5 },
|
|
},
|
|
end: {
|
|
type: 'binding',
|
|
boundShapeId: ids.box2,
|
|
isExact: false,
|
|
normalizedAnchor: { x: 0.5, y: 0.5 },
|
|
},
|
|
},
|
|
},
|
|
])
|
|
|
|
const shapesBefore = editor.shapesArray
|
|
|
|
editor.selectAll().cut()
|
|
|
|
// Test the shape of the clipboard data.
|
|
await assertClipboardOfCorrectShape(mockClipboard.current)
|
|
|
|
editor.paste()
|
|
const shapesAfter = editor.shapesArray
|
|
|
|
// The new shapes should match the old shapes, except for their id and the arrow's bindings!
|
|
expect(shapesAfter.length).toBe(shapesBefore.length)
|
|
expect(shapesAfter[0]).toMatchObject({ ...shapesBefore[0], id: shapesAfter[0].id })
|
|
expect(shapesAfter[1]).toMatchObject({ ...shapesBefore[1], id: shapesAfter[1].id })
|
|
expect(shapesAfter[2]).toMatchObject({
|
|
...shapesBefore[2],
|
|
id: shapesAfter[2].id,
|
|
props: {
|
|
...shapesBefore[2].props,
|
|
start: {
|
|
...(shapesBefore[2] as TLArrowShape).props.start,
|
|
boundShapeId: shapesAfter[0].id,
|
|
},
|
|
end: { ...(shapesBefore[2] as TLArrowShape).props.end, boundShapeId: shapesAfter[1].id },
|
|
},
|
|
})
|
|
})
|
|
|
|
it('pastes in the right position after copying from within a group.', async () => {
|
|
const mockClipboard = doMockClipboard()
|
|
|
|
editor.createShapes([
|
|
{ id: ids.box1, type: 'geo', x: 0, y: 0, props: { w: 100, h: 100 } },
|
|
{ id: ids.box2, type: 'geo', x: 300, y: 300, props: { w: 100, h: 100 } },
|
|
])
|
|
editor
|
|
// Create group
|
|
.selectAll()
|
|
.groupShapes()
|
|
// Move the group
|
|
.updateShapes([
|
|
{
|
|
id: editor.shapesArray[2].id,
|
|
type: 'group',
|
|
x: 400,
|
|
y: 400,
|
|
},
|
|
])
|
|
// Copy a shape from within the group
|
|
.selectNone()
|
|
.setSelectedIds([ids.box1])
|
|
.copy()
|
|
|
|
await assertClipboardOfCorrectShape(mockClipboard.current)
|
|
|
|
// Paste the shape
|
|
expect(editor.shapesArray.length).toEqual(3)
|
|
editor.paste()
|
|
expect(editor.shapesArray.length).toEqual(4)
|
|
|
|
// Check if the position is correct
|
|
const pastedShape = editor.shapesArray[editor.shapesArray.length - 1]
|
|
const pastedPoint = { x: pastedShape.x, y: pastedShape.y }
|
|
|
|
expect(pastedPoint).toMatchObject({ x: 150, y: 150 }) // center of group
|
|
})
|
|
})
|