tldraw
Version:
A tiny little drawing editor.
1,123 lines (1,035 loc) • 19.4 kB
text/typescript
import { TLShapeId, createShapeId } from '@tldraw/editor'
import { TestEditor } from '../TestEditor'
let editor: TestEditor
function expectShapesInOrder(editor: TestEditor, ...ids: TLShapeId[]) {
expect(editor.getCurrentPageShapesSorted().map((shape) => shape.id)).toMatchObject(ids)
}
function getSiblingBelow(editor: TestEditor, id: TLShapeId) {
const shape = editor.getShape(id)!
const siblings = editor.getSortedChildIdsForParent(shape.parentId)
const index = siblings.indexOf(id)
return siblings[index - 1]
}
function getSiblingAbove(editor: TestEditor, id: TLShapeId) {
const shape = editor.getShape(id)!
const siblings = editor.getSortedChildIdsForParent(shape.parentId)
const index = siblings.indexOf(id)
return siblings[index + 1]
}
const ids = {
A: createShapeId('A'),
B: createShapeId('B'),
C: createShapeId('C'),
D: createShapeId('D'),
E: createShapeId('E'),
F: createShapeId('F'),
G: createShapeId('G'),
}
beforeEach(() => {
editor?.dispose()
editor = new TestEditor()
editor.createShapes([
{
id: ids['A'],
type: 'geo',
},
{
id: ids['B'],
type: 'geo',
},
{
id: ids['C'],
type: 'geo',
},
{
id: ids['D'],
type: 'geo',
},
{
id: ids['E'],
type: 'geo',
},
{
id: ids['F'],
type: 'geo',
},
{
id: ids['G'],
type: 'geo',
},
])
})
describe('When running zindex tests', () => {
it('Correctly initializes indices', () => {
expect(editor.getCurrentPageShapesSorted().map((shape) => shape.index)).toMatchObject([
'a1',
'a2',
'a3',
'a4',
'a5',
'a6',
'a7',
])
})
it('Correctly identifies shape orders', () => {
expectShapesInOrder(
editor,
ids['A'],
ids['B'],
ids['C'],
ids['D'],
ids['E'],
ids['F'],
ids['G']
)
})
})
describe('editor.getSiblingAbove', () => {
it('Gets the correct shape above', () => {
expect(getSiblingAbove(editor, ids['B'])).toBe(ids['C'])
expect(getSiblingAbove(editor, ids['C'])).toBe(ids['D'])
expect(getSiblingAbove(editor, ids['G'])).toBeUndefined()
})
})
describe('editor.getSiblingAbove', () => {
it('Gets the correct shape above', () => {
expect(getSiblingBelow(editor, ids['A'])).toBeUndefined()
expect(getSiblingBelow(editor, ids['B'])).toBe(ids['A'])
expect(getSiblingBelow(editor, ids['C'])).toBe(ids['B'])
})
})
describe('When sending to back', () => {
it('Moves one shape to back', () => {
editor.sendToBack([ids['D']])
expectShapesInOrder(
editor,
ids['D'],
ids['A'],
ids['B'],
ids['C'],
ids['E'],
ids['F'],
ids['G']
)
editor.sendToBack([ids['D']]) // noop
expectShapesInOrder(
editor,
ids['D'],
ids['A'],
ids['B'],
ids['C'],
ids['E'],
ids['F'],
ids['G']
)
})
it('Moves no shapes when selecting shapes at the back', () => {
editor.sendToBack([ids['A'], ids['B'], ids['C']])
expectShapesInOrder(
editor,
ids['A'],
ids['B'],
ids['C'],
ids['D'],
ids['E'],
ids['F'],
ids['G']
)
editor.sendToBack([ids['A'], ids['B'], ids['C']])
expectShapesInOrder(
editor,
ids['A'],
ids['B'],
ids['C'],
ids['D'],
ids['E'],
ids['F'],
ids['G']
)
})
it('Moves two adjacent shapes to back', () => {
editor.sendToBack([ids['D'], ids['E']])
expectShapesInOrder(
editor,
ids['D'],
ids['E'],
ids['A'],
ids['B'],
ids['C'],
ids['F'],
ids['G']
)
editor.sendToBack([ids['D'], ids['E']])
expectShapesInOrder(
editor,
ids['D'],
ids['E'],
ids['A'],
ids['B'],
ids['C'],
ids['F'],
ids['G']
)
})
it('Moves non-adjacent shapes to back', () => {
editor.sendToBack([ids['E'], ids['G']])
expectShapesInOrder(
editor,
ids['E'],
ids['G'],
ids['A'],
ids['B'],
ids['C'],
ids['D'],
ids['F']
)
editor.sendToBack([ids['E'], ids['G']])
expectShapesInOrder(
editor,
ids['E'],
ids['G'],
ids['A'],
ids['B'],
ids['C'],
ids['D'],
ids['F']
)
})
it('Moves non-adjacent shapes to back when one is at the back', () => {
editor.sendToBack([ids['A'], ids['G']])
expectShapesInOrder(
editor,
ids['A'],
ids['G'],
ids['B'],
ids['C'],
ids['D'],
ids['E'],
ids['F']
)
editor.sendToBack([ids['A'], ids['G']])
expectShapesInOrder(
editor,
ids['A'],
ids['G'],
ids['B'],
ids['C'],
ids['D'],
ids['E'],
ids['F']
)
})
})
describe('When sending to front', () => {
it('Moves one shape to front', () => {
editor.bringToFront([ids['A']])
expectShapesInOrder(
editor,
ids['B'],
ids['C'],
ids['D'],
ids['E'],
ids['F'],
ids['G'],
ids['A']
)
editor.bringToFront([ids['A']]) // noop
expectShapesInOrder(
editor,
ids['B'],
ids['C'],
ids['D'],
ids['E'],
ids['F'],
ids['G'],
ids['A']
)
})
it('Moves no shapes when selecting shapes at the front', () => {
editor.bringToFront([ids['G']])
expectShapesInOrder(
editor,
ids['A'],
ids['B'],
ids['C'],
ids['D'],
ids['E'],
ids['F'],
ids['G']
)
editor.bringToFront([ids['G']]) // noop
expectShapesInOrder(
editor,
ids['A'],
ids['B'],
ids['C'],
ids['D'],
ids['E'],
ids['F'],
ids['G']
)
})
it('Moves two adjacent shapes to front', () => {
editor.bringToFront([ids['D'], ids['E']])
expectShapesInOrder(
editor,
ids['A'],
ids['B'],
ids['C'],
ids['F'],
ids['G'],
ids['D'],
ids['E']
)
editor.bringToFront([ids['D'], ids['E']]) // noop
expectShapesInOrder(
editor,
ids['A'],
ids['B'],
ids['C'],
ids['F'],
ids['G'],
ids['D'],
ids['E']
)
})
it('Moves non-adjacent shapes to front', () => {
editor.bringToFront([ids['A'], ids['C']])
expectShapesInOrder(
editor,
ids['B'],
ids['D'],
ids['E'],
ids['F'],
ids['G'],
ids['A'],
ids['C']
)
editor.bringToFront([ids['A'], ids['C']]) // noop
expectShapesInOrder(
editor,
ids['B'],
ids['D'],
ids['E'],
ids['F'],
ids['G'],
ids['A'],
ids['C']
)
})
it('Moves non-adjacent shapes to front when one is at the front', () => {
editor.bringToFront([ids['E'], ids['G']])
expectShapesInOrder(
editor,
ids['A'],
ids['B'],
ids['C'],
ids['D'],
ids['F'],
ids['E'],
ids['G']
)
editor.bringToFront([ids['E'], ids['G']])
expectShapesInOrder(
editor,
ids['A'],
ids['B'],
ids['C'],
ids['D'],
ids['F'],
ids['E'],
ids['G']
)
})
})
describe('When sending backward', () => {
it('Moves one shape backward', () => {
editor.sendBackward([ids['C']])
expectShapesInOrder(
editor,
ids['A'],
ids['C'],
ids['B'],
ids['D'],
ids['E'],
ids['F'],
ids['G']
)
editor.sendBackward([ids['C']])
expectShapesInOrder(
editor,
ids['C'],
ids['A'],
ids['B'],
ids['D'],
ids['E'],
ids['F'],
ids['G']
)
})
it('Moves shapes to the first position', () => {
editor.sendBackward([ids['B']])
expectShapesInOrder(
editor,
ids['B'],
ids['A'],
ids['C'],
ids['D'],
ids['E'],
ids['F'],
ids['G']
)
editor.sendBackward([ids['A']])
expectShapesInOrder(
editor,
ids['A'],
ids['B'],
ids['C'],
ids['D'],
ids['E'],
ids['F'],
ids['G']
)
editor.sendBackward([ids['B']])
expectShapesInOrder(
editor,
ids['B'],
ids['A'],
ids['C'],
ids['D'],
ids['E'],
ids['F'],
ids['G']
)
})
it('Moves two shapes to the first position', () => {
editor.sendBackward([ids['B'], ids['C']])
expectShapesInOrder(
editor,
ids['B'],
ids['C'],
ids['A'],
ids['D'],
ids['E'],
ids['F'],
ids['G']
)
editor.sendBackward([ids['C'], ids['A']])
expectShapesInOrder(
editor,
ids['C'],
ids['A'],
ids['B'],
ids['D'],
ids['E'],
ids['F'],
ids['G']
)
editor.sendBackward([ids['A'], ids['B']])
expectShapesInOrder(
editor,
ids['A'],
ids['B'],
ids['C'],
ids['D'],
ids['E'],
ids['F'],
ids['G']
)
})
it('Moves no shapes when sending shapes at the back', () => {
editor.sendBackward([ids['A'], ids['B'], ids['C']])
expectShapesInOrder(
editor,
ids['A'],
ids['B'],
ids['C'],
ids['D'],
ids['E'],
ids['F'],
ids['G']
)
editor.sendBackward([ids['A'], ids['B'], ids['C']])
expectShapesInOrder(
editor,
ids['A'],
ids['B'],
ids['C'],
ids['D'],
ids['E'],
ids['F'],
ids['G']
)
})
it('Moves two adjacent shapes backward', () => {
editor.sendBackward([ids['D'], ids['E']])
expectShapesInOrder(
editor,
ids['A'],
ids['B'],
ids['D'],
ids['E'],
ids['C'],
ids['F'],
ids['G']
)
})
it('Moves two adjacent shapes backward when one is at the back', () => {
editor.sendBackward([ids['A'], ids['E']])
expectShapesInOrder(
editor,
ids['A'],
ids['B'],
ids['C'],
ids['E'],
ids['D'],
ids['F'],
ids['G']
)
editor.sendBackward([ids['A'], ids['E']])
expectShapesInOrder(
editor,
ids['A'],
ids['B'],
ids['E'],
ids['C'],
ids['D'],
ids['F'],
ids['G']
)
})
it('Moves non-adjacent shapes backward', () => {
editor.sendBackward([ids['E'], ids['G']])
expectShapesInOrder(
editor,
ids['A'],
ids['B'],
ids['C'],
ids['E'],
ids['D'],
ids['G'],
ids['F']
)
editor.sendBackward([ids['E'], ids['G']])
expectShapesInOrder(
editor,
ids['A'],
ids['B'],
ids['E'],
ids['C'],
ids['G'],
ids['D'],
ids['F']
)
})
it('Moves non-adjacent shapes backward when one is at the back', () => {
editor.sendBackward([ids['A'], ids['G']])
expectShapesInOrder(
editor,
ids['A'],
ids['B'],
ids['C'],
ids['D'],
ids['E'],
ids['G'],
ids['F']
)
editor.sendBackward([ids['A'], ids['G']])
expectShapesInOrder(
editor,
ids['A'],
ids['B'],
ids['C'],
ids['D'],
ids['G'],
ids['E'],
ids['F']
)
})
it('Moves non-adjacent shapes to backward when both are at the back', () => {
editor.sendBackward([ids['A'], ids['B']])
expectShapesInOrder(
editor,
ids['A'],
ids['B'],
ids['C'],
ids['D'],
ids['E'],
ids['F'],
ids['G']
)
editor.sendBackward([ids['A'], ids['B']])
expectShapesInOrder(
editor,
ids['A'],
ids['B'],
ids['C'],
ids['D'],
ids['E'],
ids['F'],
ids['G']
)
})
})
describe('When moving forward', () => {
it('Moves one shape forward', () => {
editor.bringForward([ids['A']])
expectShapesInOrder(
editor,
ids['B'],
ids['A'],
ids['C'],
ids['D'],
ids['E'],
ids['F'],
ids['G']
)
editor.bringForward([ids['A']])
expectShapesInOrder(
editor,
ids['B'],
ids['C'],
ids['A'],
ids['D'],
ids['E'],
ids['F'],
ids['G']
)
})
it('Moves no shapes when sending shapes at the front', () => {
editor.bringForward([ids['E'], ids['F'], ids['G']])
expectShapesInOrder(
editor,
ids['A'],
ids['B'],
ids['C'],
ids['D'],
ids['E'],
ids['F'],
ids['G']
)
editor.bringForward([ids['E'], ids['F'], ids['G']]) // noop
expectShapesInOrder(
editor,
ids['A'],
ids['B'],
ids['C'],
ids['D'],
ids['E'],
ids['F'],
ids['G']
)
})
it('Moves two adjacent shapes forward', () => {
editor.bringForward([ids['C'], ids['D']])
expectShapesInOrder(
editor,
ids['A'],
ids['B'],
ids['E'],
ids['C'],
ids['D'],
ids['F'],
ids['G']
)
editor.bringForward([ids['C'], ids['D']])
expectShapesInOrder(
editor,
ids['A'],
ids['B'],
ids['E'],
ids['F'],
ids['C'],
ids['D'],
ids['G']
)
})
it('Moves non-adjacent shapes forward', () => {
editor.bringForward([ids['A'], ids['C']])
expectShapesInOrder(
editor,
ids['B'],
ids['A'],
ids['D'],
ids['C'],
ids['E'],
ids['F'],
ids['G']
)
editor.bringForward([ids['A'], ids['C']])
expectShapesInOrder(
editor,
ids['B'],
ids['D'],
ids['A'],
ids['E'],
ids['C'],
ids['F'],
ids['G']
)
})
it('Moves non-adjacent shapes to forward when one is at the front', () => {
editor.bringForward([ids['C'], ids['G']])
expectShapesInOrder(
editor,
ids['A'],
ids['B'],
ids['D'],
ids['C'],
ids['E'],
ids['F'],
ids['G']
)
editor.bringForward([ids['C'], ids['G']])
expectShapesInOrder(
editor,
ids['A'],
ids['B'],
ids['D'],
ids['E'],
ids['C'],
ids['F'],
ids['G']
)
})
it('Moves adjacent shapes to forward when both are at the front', () => {
editor.bringForward([ids['F'], ids['G']])
expectShapesInOrder(
editor,
ids['A'],
ids['B'],
ids['C'],
ids['D'],
ids['E'],
ids['F'],
ids['G']
)
editor.bringForward([ids['F'], ids['G']])
expectShapesInOrder(
editor,
ids['A'],
ids['B'],
ids['C'],
ids['D'],
ids['E'],
ids['F'],
ids['G']
)
})
})
// Edges
describe('Edge cases...', () => {
it('When bringing forward, does not increment order if shapes at at the top', () => {
editor.bringForward([ids['F'], ids['G']])
})
it('When bringing forward, does not increment order with non-adjacent shapes if shapes at at the top', () => {
editor.bringForward([ids['E'], ids['G']])
})
it('When bringing to front, does not change order of shapes already at top', () => {
editor.bringToFront([ids['E'], ids['G']])
})
it('When sending to back, does not change order of shapes already at bottom', () => {
editor.sendToBack([ids['A'], ids['C']])
})
it('When moving back to front...', () => {
editor.sendBackward([ids['F'], ids['G']])
expectShapesInOrder(
editor,
ids['A'],
ids['B'],
ids['C'],
ids['D'],
ids['F'],
ids['G'],
ids['E']
)
editor.sendBackward([ids['F'], ids['G']])
expectShapesInOrder(
editor,
ids['A'],
ids['B'],
ids['C'],
ids['F'],
ids['G'],
ids['D'],
ids['E']
)
editor.sendBackward([ids['F'], ids['G']])
expectShapesInOrder(
editor,
ids['A'],
ids['B'],
ids['F'],
ids['G'],
ids['C'],
ids['D'],
ids['E']
)
editor.sendBackward([ids['F'], ids['G']])
expectShapesInOrder(
editor,
ids['A'],
ids['F'],
ids['G'],
ids['B'],
ids['C'],
ids['D'],
ids['E']
)
editor.sendBackward([ids['F'], ids['G']])
expectShapesInOrder(
editor,
ids['F'],
ids['G'],
ids['A'],
ids['B'],
ids['C'],
ids['D'],
ids['E']
)
editor
.bringForward([ids['F'], ids['G']])
.bringForward([ids['F'], ids['G']])
.bringForward([ids['F'], ids['G']])
.bringForward([ids['F'], ids['G']])
.bringForward([ids['F'], ids['G']])
expectShapesInOrder(
editor,
ids['A'],
ids['B'],
ids['C'],
ids['D'],
ids['E'],
ids['F'],
ids['G']
)
})
})
describe('When undoing and redoing...', () => {
it('Undoes and redoes', () => {
expectShapesInOrder(
editor,
ids['A'],
ids['B'],
ids['C'],
ids['D'],
ids['E'],
ids['F'],
ids['G']
)
editor.markHistoryStoppingPoint('before sending to back')
editor.sendBackward([ids['F'], ids['G']])
expectShapesInOrder(
editor,
ids['A'],
ids['B'],
ids['C'],
ids['D'],
ids['F'],
ids['G'],
ids['E']
)
editor.undo()
expectShapesInOrder(
editor,
ids['A'],
ids['B'],
ids['C'],
ids['D'],
ids['E'],
ids['F'],
ids['G']
)
// .redo()
// .expectShapesInOrder(ids['A'], ids['B'], ids['C'], ids['D'], ids['F'], ids['G'], ids['E'])
})
})
describe('When shapes are parented...', () => {
it('Sorted correctly by pageIndex', () => {
editor.reparentShapes([ids['C']], ids['A']).reparentShapes([ids['B']], ids['D'])
expectShapesInOrder(
editor,
ids['A'],
ids['C'],
ids['D'],
ids['B'],
ids['E'],
ids['F'],
ids['G']
)
})
})
test('When only two shapes exist', () => {
editor = new TestEditor()
editor.createShapes([
{
id: ids['A'],
type: 'geo',
},
{
id: ids['B'],
type: 'geo',
},
])
expectShapesInOrder(editor, ids['A'], ids['B'])
editor.sendToBack([ids['B']])
expectShapesInOrder(editor, ids['B'], ids['A'])
editor.bringToFront([ids['B']])
expectShapesInOrder(editor, ids['A'], ids['B'])
editor.sendBackward([ids['B']])
expectShapesInOrder(editor, ids['B'], ids['A'])
editor.bringForward([ids['B']])
expectShapesInOrder(editor, ids['A'], ids['B'])
})
test("bringForward ignores shapes that don't overlap by default", () => {
editor = new TestEditor()
// a and c overlap but a and b do not and neither do a and d
editor.createShapes([
{
id: ids['A'],
type: 'geo',
x: 0,
y: 0,
props: { w: 100, h: 100 },
},
{
id: ids['B'],
type: 'geo',
x: 200,
y: 200,
props: { w: 100, h: 100 },
},
{
id: ids['C'],
type: 'geo',
x: 50,
y: 50,
props: { w: 100, h: 100 },
},
{
id: ids['D'],
type: 'geo',
x: 110,
y: 110,
props: { w: 100, h: 100 },
},
])
expectShapesInOrder(editor, ids['A'], ids['B'], ids['C'], ids['D'])
editor.bringForward([ids['A']])
// a should now be in front of c but behind d
expectShapesInOrder(editor, ids['B'], ids['C'], ids['A'], ids['D'])
})
test("bringForward does not ignore shapes that don't overlap with considerAllShapes", () => {
editor = new TestEditor()
// a and c overlap but a and b do not and neither do a and d
editor.createShapes([
{
id: ids['A'],
type: 'geo',
x: 0,
y: 0,
props: { w: 100, h: 100 },
},
{
id: ids['B'],
type: 'geo',
x: 200,
y: 200,
props: { w: 100, h: 100 },
},
{
id: ids['C'],
type: 'geo',
x: 50,
y: 50,
props: { w: 100, h: 100 },
},
{
id: ids['D'],
type: 'geo',
x: 110,
y: 110,
props: { w: 100, h: 100 },
},
])
expectShapesInOrder(editor, ids['A'], ids['B'], ids['C'], ids['D'])
editor.bringForward([ids['A']], { considerAllShapes: true })
// a should now be in front of c but behind d
expectShapesInOrder(editor, ids['B'], ids['A'], ids['C'], ids['D'])
})
test("sendBackwards ignores shapes that don't overlap by default", () => {
editor = new TestEditor()
// d overlaps with b but not a or c
editor.createShapes([
{
id: ids['A'],
type: 'geo',
x: -110,
y: -110,
props: { w: 100, h: 100 },
},
{
id: ids['B'],
type: 'geo',
x: 0,
y: 0,
props: { w: 100, h: 100 },
},
{
id: ids['C'],
type: 'geo',
x: 200,
y: 200,
props: { w: 100, h: 100 },
},
{
id: ids['D'],
type: 'geo',
x: 50,
y: 50,
props: { w: 100, h: 100 },
},
])
expectShapesInOrder(editor, ids['A'], ids['B'], ids['C'], ids['D'])
editor.sendBackward([ids['D']])
// d should now be behind b
expectShapesInOrder(editor, ids['A'], ids['D'], ids['B'], ids['C'])
})
test("sendBackwards does not ignore shapes that don't overlap with considerAllShapes", () => {
editor = new TestEditor()
// d overlaps with b but not a or c
editor.createShapes([
{
id: ids['A'],
type: 'geo',
x: -110,
y: -110,
props: { w: 100, h: 100 },
},
{
id: ids['B'],
type: 'geo',
x: 0,
y: 0,
props: { w: 100, h: 100 },
},
{
id: ids['C'],
type: 'geo',
x: 200,
y: 200,
props: { w: 100, h: 100 },
},
{
id: ids['D'],
type: 'geo',
x: 50,
y: 50,
props: { w: 100, h: 100 },
},
])
expectShapesInOrder(editor, ids['A'], ids['B'], ids['C'], ids['D'])
editor.sendBackward([ids['D']], { considerAllShapes: true })
// d should now be behind b
expectShapesInOrder(editor, ids['A'], ids['B'], ids['D'], ids['C'])
})