Tldraw/packages/tldraw/src/test/selection-omnibus.test.ts

1925 wiersze
63 KiB
TypeScript

import { TLFrameShape, TLGeoShape, createShapeId } from '@tldraw/editor'
import { TestEditor } from './TestEditor'
let editor: TestEditor
const ids = {
box1: createShapeId('box1'),
box2: createShapeId('box2'),
box3: createShapeId('box3'),
box4: createShapeId('box4'),
box5: createShapeId('box5'),
frame1: createShapeId('frame1'),
group1: createShapeId('group1'),
group2: createShapeId('group2'),
group3: createShapeId('group3'),
}
beforeEach(() => {
editor = new TestEditor()
editor.setScreenBounds({ w: 3000, h: 3000, x: 0, y: 0 })
})
it('lists a sorted shapes array correctly', () => {
editor.createShapes([
{ id: ids.box1, type: 'geo' },
{ id: ids.box2, type: 'geo' },
{ id: ids.box3, type: 'geo' },
{ id: ids.frame1, type: 'frame' },
{ id: ids.box4, type: 'geo', parentId: ids.frame1 },
{ id: ids.box5, type: 'geo', parentId: ids.frame1 },
])
editor.sendBackward([ids.frame1])
editor.sendBackward([ids.frame1])
expect(editor.getCurrentPageShapesSorted().map((s) => s.id)).toEqual([
ids.box1,
ids.frame1,
ids.box4,
ids.box5,
ids.box2,
ids.box3,
])
})
describe('Hovering shapes', () => {
beforeEach(() => {
editor.createShapes([{ id: ids.box1, type: 'geo', x: 0, y: 0, props: { w: 100, h: 100 } }])
})
it('hovers the margins of hollow shapes but not their insides', () => {
expect(editor.getHoveredShapeId()).toBe(null)
editor.pointerMove(-4, 50)
expect(editor.getHoveredShapeId()).toBe(ids.box1)
editor.pointerMove(-50, 50)
expect(editor.getHoveredShapeId()).toBe(null)
editor.pointerMove(4, 50)
expect(editor.getHoveredShapeId()).toBe(ids.box1)
editor.pointerMove(75, 75)
expect(editor.getHoveredShapeId()).toBe(null)
// does not hover the label of a geo shape when the label is empty
editor.pointerMove(50, 50)
expect(editor.getHoveredShapeId()).toBe(null)
editor.updateShape({ id: ids.box1, type: 'geo', props: { text: 'hello' } })
// oh there's text now? hover it
editor.pointerMove(50, 50)
expect(editor.getHoveredShapeId()).toBe(ids.box1)
})
it('selects a shape with a full label on pointer down', () => {
editor.updateShape({ id: ids.box1, type: 'geo', props: { text: 'hello' } })
editor.pointerMove(50, 50)
editor.pointerDown()
expect(editor.isIn('select.pointing_shape')).toBe(true)
expect(editor.getSelectedShapes().length).toBe(1)
editor.pointerUp()
expect(editor.getSelectedShapes().length).toBe(1)
expect(editor.isIn('select.idle')).toBe(true)
})
it('selects a shape with an empty label on pointer up', () => {
editor.pointerMove(50, 50)
editor.pointerDown()
expect(editor.isIn('select.pointing_canvas')).toBe(true)
expect(editor.getSelectedShapes().length).toBe(0)
editor.pointerUp()
expect(editor.isIn('select.idle')).toBe(true)
expect(editor.getSelectedShapes().length).toBe(1)
})
it('hovers the margins or inside of filled shapes', () => {
editor.updateShape({ id: ids.box1, type: 'geo', props: { fill: 'solid' } })
expect(editor.getHoveredShapeId()).toBe(null)
editor.pointerMove(-4, 50)
expect(editor.getHoveredShapeId()).toBe(ids.box1)
editor.pointerMove(-50, 50)
expect(editor.getHoveredShapeId()).toBe(null)
editor.pointerMove(4, 50)
expect(editor.getHoveredShapeId()).toBe(ids.box1)
editor.pointerMove(50, 50)
expect(editor.getHoveredShapeId()).toBe(ids.box1)
})
it('hovers the closest edge or else the highest shape', () => {
// box2 is above box1
editor.createShapes([{ id: ids.box2, type: 'geo', x: 6, y: 0, props: { w: 100, h: 100 } }])
editor.pointerMove(2, 50)
expect(editor.getHoveredShapeId()).toBe(ids.box1)
editor.pointerMove(4, 50)
expect(editor.getHoveredShapeId()).toBe(ids.box2)
editor.pointerMove(3, 50)
expect(editor.getHoveredShapeId()).toBe(ids.box2)
editor.sendToBack([ids.box2])
editor.pointerMove(3, 50) // ! does not update automatically, only on move
expect(editor.getHoveredShapeId()).toBe(ids.box1)
})
})
describe('brushing', () => {
beforeEach(() => {
editor.createShapes([{ id: ids.box1, type: 'geo', props: { fill: 'solid', w: 50, h: 50 } }])
editor.user.updateUserPreferences({ isWrapMode: false })
})
afterAll(() => {
editor.user.updateUserPreferences({ isWrapMode: false })
})
it('brushes on wrap', () => {
editor.pointerMove(-50, -50)
editor.pointerDown()
editor.pointerMove(100, 100)
expect(editor.getSelectedShapeIds().length).toBe(1)
})
it('brushes on intersection', () => {
editor.pointerMove(-50, -50)
editor.pointerDown()
editor.pointerMove(10, 10)
expect(editor.getSelectedShapeIds().length).toBe(1)
})
it('brushes only on wrap when ctrl key is down', () => {
editor.pointerMove(-50, -50)
editor.pointerDown()
editor.pointerMove(10, 10)
editor.keyDown('Control')
expect(editor.getSelectedShapeIds().length).toBe(0)
editor.pointerMove(100, 100)
expect(editor.getSelectedShapeIds().length).toBe(1)
})
})
describe('brushing with wrap mode on', () => {
beforeEach(() => {
editor.createShapes([{ id: ids.box1, type: 'geo', props: { fill: 'solid', w: 50, h: 50 } }])
editor.user.updateUserPreferences({ isWrapMode: true })
})
afterAll(() => {
editor.user.updateUserPreferences({ isWrapMode: false })
})
it('brushes on wrap', () => {
editor.pointerMove(-50, -50)
editor.pointerDown()
editor.pointerMove(100, 100)
expect(editor.getSelectedShapeIds().length).toBe(1)
})
it('does not brush on intersection', () => {
editor.pointerMove(-50, -50)
editor.pointerDown()
editor.pointerMove(10, 10)
expect(editor.getSelectedShapeIds().length).toBe(0)
})
it('brushes on intersection when ctrl key is down', () => {
editor.pointerMove(-50, -50)
editor.pointerDown()
editor.pointerMove(10, 10)
expect(editor.getSelectedShapeIds().length).toBe(0)
editor.keyDown('Control')
expect(editor.getSelectedShapeIds().length).toBe(1)
editor.pointerMove(100, 100)
expect(editor.getSelectedShapeIds().length).toBe(1)
})
})
describe('when shape is filled', () => {
let box1: TLGeoShape
beforeEach(() => {
editor.createShapes([{ id: ids.box1, type: 'geo', props: { fill: 'solid' } }])
box1 = editor.getShape<TLGeoShape>(ids.box1)!
})
it('hits on pointer down over shape', () => {
editor.pointerMove(50, 50)
expect(editor.getHoveredShapeId()).toBe(box1.id)
editor.pointerDown()
expect(editor.getSelectedShapeIds()).toEqual([box1.id])
editor.pointerUp()
expect(editor.getSelectedShapeIds()).toEqual([box1.id])
})
it('hits on pointer down over shape margin (inside', () => {
editor.pointerMove(95, 50)
expect(editor.getHoveredShapeId()).toBe(box1.id)
editor.pointerDown()
expect(editor.getSelectedShapeIds()).toEqual([box1.id])
editor.pointerUp()
expect(editor.getSelectedShapeIds()).toEqual([box1.id])
})
it('hits on pointer down over shape margin (outside)', () => {
editor.pointerMove(104, 50)
expect(editor.getHoveredShapeId()).toBe(box1.id)
editor.pointerDown()
expect(editor.getSelectedShapeIds()).toEqual([box1.id])
editor.pointerUp()
expect(editor.getSelectedShapeIds()).toEqual([box1.id])
})
it('misses on pointer down outside of shape', () => {
editor.pointerMove(250, 50)
expect(editor.getHoveredShapeId()).toBe(null)
editor.pointerDown()
expect(editor.getSelectedShapeIds()).toEqual([])
editor.pointerUp()
expect(editor.getSelectedShapeIds()).toEqual([])
})
it('selects and drags on point inside and drag', () => {
editor.pointerMove(50, 50)
expect(editor.getHoveredShapeId()).toBe(box1.id)
editor.pointerDown()
expect(editor.getSelectedShapeIds()).toEqual([box1.id])
editor.pointerMove(55, 55)
editor.expectToBeIn('select.translating')
editor.pointerUp()
expect(editor.getSelectedShapeIds()).toEqual([box1.id])
})
})
describe('when shape is hollow', () => {
let box1: TLGeoShape
beforeEach(() => {
editor.createShapes([{ id: ids.box1, type: 'geo', props: { fill: 'none' } }])
box1 = editor.getShape<TLGeoShape>(ids.box1)!
})
it('misses on pointer down over shape, misses on pointer up', () => {
editor.pointerMove(10, 10)
expect(editor.getHoveredShapeId()).toBe(null)
editor.pointerDown()
expect(editor.getSelectedShapeIds()).toEqual([])
editor.pointerUp()
expect(editor.getSelectedShapeIds()).toEqual([])
})
it('hits on the label', () => {
editor.pointerMove(-100, -100)
expect(editor.getHoveredShapeId()).toBe(null)
expect(editor.getSelectedShapeIds()).toEqual([])
editor.pointerMove(50, 50)
// no hover over label...
expect(editor.getHoveredShapeId()).toBe(null)
expect(editor.getSelectedShapeIds()).toEqual([])
editor.pointerDown()
// will select on pointer up
expect(editor.getHoveredShapeId()).toBe(null)
expect(editor.getSelectedShapeIds()).toEqual([])
// selects on pointer up
editor.pointerUp()
expect(editor.getHoveredShapeId()).toBe(null)
expect(editor.getSelectedShapeIds()).toEqual([ids.box1])
})
it('missed on the label when the shape is locked', () => {
editor.updateShape({ id: ids.box1, type: 'geo', isLocked: true })
editor.pointerMove(-100, -100)
expect(editor.getHoveredShapeId()).toBe(null)
expect(editor.getSelectedShapeIds()).toEqual([])
editor.pointerMove(50, 50)
// no hover over label...
expect(editor.getHoveredShapeId()).toBe(null)
expect(editor.getSelectedShapeIds()).toEqual([])
editor.pointerDown()
// will select on pointer up
expect(editor.getHoveredShapeId()).toBe(null)
expect(editor.getSelectedShapeIds()).toEqual([])
// selects on pointer up
editor.pointerUp()
expect(editor.getHoveredShapeId()).toBe(null)
expect(editor.getSelectedShapeIds()).toEqual([])
})
it('hits on pointer down over shape margin (inside)', () => {
editor.pointerMove(96, 50)
expect(editor.getHoveredShapeId()).toBe(box1.id)
editor.pointerDown()
expect(editor.getSelectedShapeIds()).toEqual([box1.id])
editor.pointerUp()
expect(editor.getSelectedShapeIds()).toEqual([box1.id])
})
it('hits on pointer down over shape margin (outside)', () => {
editor.pointerMove(104, 50)
expect(editor.getHoveredShapeId()).toBe(box1.id)
editor.pointerDown()
expect(editor.getSelectedShapeIds()).toEqual([box1.id])
editor.pointerUp()
expect(editor.getSelectedShapeIds()).toEqual([box1.id])
})
it('misses on pointer down outside of shape', () => {
editor.pointerMove(250, 50)
expect(editor.getHoveredShapeId()).toBe(null)
editor.pointerDown()
expect(editor.getSelectedShapeIds()).toEqual([])
editor.pointerUp()
expect(editor.getSelectedShapeIds()).toEqual([])
})
it('brushes on point inside and drag', () => {
editor.pointerMove(75, 75)
expect(editor.getHoveredShapeId()).toBe(null)
editor.pointerDown()
expect(editor.getSelectedShapeIds()).toEqual([])
editor.pointerMove(80, 80)
editor.expectToBeIn('select.brushing')
editor.pointerUp()
expect(editor.getSelectedShapeIds()).toEqual([])
})
it('drags draw shape child', () => {
editor
.selectAll()
.deleteShapes(editor.getSelectedShapeIds())
.setCurrentTool('draw')
.pointerMove(500, 500)
.pointerDown()
.pointerMove(501, 501)
.pointerMove(550, 550)
.pointerMove(599, 599)
.pointerMove(600, 600)
.pointerUp()
.selectAll()
.setCurrentTool('select')
expect(editor.getSelectedShapeIds().length).toBe(1)
// Not inside of the shape but inside of the selection bounds
editor.pointerMove(510, 590)
expect(editor.getHoveredShapeId()).toBe(null)
// Draw shapes have `hideSelectionBoundsBg` set to false
editor.pointerDown()
editor.expectToBeIn('select.pointing_selection')
editor.pointerUp()
editor.selectAll()
editor.rotateSelection(Math.PI)
editor.setCurrentTool('select')
editor.pointerMove(590, 510)
editor.pointerDown()
editor.expectToBeIn('select.pointing_selection')
editor.pointerUp()
})
it('does not drag arrow shape', () => {
editor
.selectAll()
.deleteShapes(editor.getSelectedShapeIds())
.setCurrentTool('arrow')
.pointerMove(500, 500)
.pointerDown()
.pointerMove(600, 600)
.pointerUp()
.selectAll()
.setCurrentTool('select')
expect(editor.getSelectedShapeIds().length).toBe(1)
// Not inside of the shape but inside of the selection bounds
editor.pointerMove(510, 590)
expect(editor.getHoveredShapeId()).toBe(null)
// Arrow shapes have `hideSelectionBoundsBg` set to true
editor.pointerDown()
editor.expectToBeIn('select.pointing_canvas')
editor.selectAll()
editor.rotateSelection(Math.PI)
editor.setCurrentTool('select')
editor.pointerMove(590, 510)
editor.pointerDown()
editor.expectToBeIn('select.pointing_canvas')
editor.pointerUp()
})
it('does not drag line shape', () => {
editor
.selectAll()
.deleteShapes(editor.getSelectedShapeIds())
.setCurrentTool('line')
.pointerMove(500, 500)
.pointerDown()
.pointerMove(600, 600)
.pointerUp()
.selectAll()
.setCurrentTool('select')
expect(editor.getSelectedShapeIds().length).toBe(1)
// Not inside of the shape but inside of the selection bounds
editor.pointerMove(510, 590)
expect(editor.getHoveredShapeId()).toBe(null)
// Line shapes have `hideSelectionBoundsBg` set to true
editor.pointerDown()
editor.expectToBeIn('select.pointing_canvas')
editor.selectAll()
editor.rotateSelection(Math.PI)
editor.setCurrentTool('select')
editor.pointerMove(590, 510)
editor.pointerDown()
editor.expectToBeIn('select.pointing_canvas')
editor.pointerUp()
})
})
describe('when shape is a frame', () => {
let frame1: TLFrameShape
beforeEach(() => {
editor.createShape<TLFrameShape>({ id: ids.frame1, type: 'frame', props: { w: 100, h: 100 } })
frame1 = editor.getShape<TLFrameShape>(ids.frame1)!
})
it('misses on pointer down over shape, hits on pointer up', () => {
editor.pointerMove(50, 50)
expect(editor.getHoveredShapeId()).toBe(null)
editor.pointerDown()
expect(editor.getSelectedShapeIds()).toEqual([])
editor.pointerUp()
expect(editor.getSelectedShapeIds()).toEqual([])
})
it('hits on pointer down over shape margin (inside)', () => {
editor.pointerMove(96, 50)
expect(editor.getHoveredShapeId()).toBe(frame1.id)
editor.pointerDown()
expect(editor.getSelectedShapeIds()).toEqual([frame1.id])
editor.pointerUp()
expect(editor.getSelectedShapeIds()).toEqual([frame1.id])
})
it('hits on pointer down over shape margin (outside)', () => {
editor.pointerMove(104, 50)
expect(editor.getHoveredShapeId()).toBe(frame1.id)
editor.pointerDown()
expect(editor.getSelectedShapeIds()).toEqual([frame1.id])
editor.pointerUp()
expect(editor.getSelectedShapeIds()).toEqual([frame1.id])
})
it('misses on pointer down outside of shape', () => {
editor.pointerMove(250, 50)
expect(editor.getHoveredShapeId()).toBe(null)
editor.pointerDown()
expect(editor.getSelectedShapeIds()).toEqual([])
editor.pointerUp()
expect(editor.getSelectedShapeIds()).toEqual([])
})
it('brushes on point inside and drag', () => {
editor.pointerMove(50, 50)
expect(editor.getHoveredShapeId()).toBe(null)
editor.pointerDown()
expect(editor.getSelectedShapeIds()).toEqual([])
editor.pointerMove(55, 55)
editor.expectToBeIn('select.brushing')
editor.pointerUp()
expect(editor.getSelectedShapeIds()).toEqual([])
})
})
describe('When a shape is behind a frame', () => {
beforeEach(() => {
editor.selectAll().deleteShapes(editor.getSelectedShapeIds())
editor.createShape<TLGeoShape>({ id: ids.box1, type: 'geo', x: 25, y: 25 })
editor.createShape<TLFrameShape>({ id: ids.frame1, type: 'frame', props: { w: 100, h: 100 } })
})
it('does not select the shape when clicked inside', () => {
editor.sendToBack([ids.box1]) // send it to back!
expect(editor.getCurrentPageShapesSorted().map((s) => s.index)).toEqual(['a1', 'a2'])
expect(editor.getCurrentPageShapesSorted().map((s) => s.id)).toEqual([ids.box1, ids.frame1])
editor.pointerMove(50, 50)
expect(editor.getHoveredShapeId()).toBe(null)
editor.pointerDown()
expect(editor.getSelectedShapeIds()).toEqual([])
editor.pointerUp()
expect(editor.getSelectedShapeIds()).toEqual([])
})
it('does not select the shape when clicked on its margin', () => {
editor.pointerMove(25, 25)
expect(editor.getHoveredShapeId()).toBe(null)
editor.pointerDown()
expect(editor.getSelectedShapeIds()).toEqual([])
editor.pointerUp()
expect(editor.getSelectedShapeIds()).toEqual([])
})
})
describe('when shape is inside of a frame', () => {
let frame1: TLFrameShape
let box1: TLGeoShape
beforeEach(() => {
editor.createShape<TLFrameShape>({ id: ids.frame1, type: 'frame', props: { w: 100, h: 100 } })
editor.createShape<TLGeoShape>({
id: ids.box1,
parentId: ids.frame1,
type: 'geo',
x: 25,
y: 25,
})
frame1 = editor.getShape<TLFrameShape>(ids.frame1)!
box1 = editor.getShape<TLGeoShape>(ids.box1)!
})
it('misses on pointer down over frame, misses on pointer up', () => {
editor.pointerMove(10, 10)
expect(editor.getHoveredShapeId()).toBe(null)
editor.pointerDown() // inside of frame1, outside of box1, outside of all margins
expect(editor.getSelectedShapeIds()).toEqual([])
editor.pointerUp()
expect(editor.getSelectedShapeIds()).toEqual([])
})
it('misses on pointer down over shape, misses on pointer up', () => {
editor.pointerMove(35, 35)
expect(editor.getHoveredShapeId()).toBe(null)
editor.pointerDown() // inside of box1 (which is empty)
expect(editor.getSelectedShapeIds()).toEqual([])
editor.pointerUp() // does not select because inside of hollow shape
expect(editor.getSelectedShapeIds()).toEqual([])
})
it('misses on pointer down over shape, hit on pointer up on the edge', () => {
editor.pointerMove(25, 25)
editor.pointerDown() // on the edge of box1 (which is empty)
expect(editor.getSelectedShapeIds()).toEqual([ids.box1])
editor.pointerUp() // does not select because inside of hollow shape
expect(editor.getSelectedShapeIds()).toEqual([ids.box1])
})
it('misses on pointer down over shape, misses on pointer up on the edge when locked', () => {
editor.updateShape({ id: ids.box1, type: 'geo', isLocked: true })
editor.pointerMove(25, 25)
editor.pointerDown() // on the edge of box1 (which is empty)
expect(editor.getSelectedShapeIds()).toEqual([])
editor.pointerUp() // does not select because inside of hollow shape
expect(editor.getSelectedShapeIds()).toEqual([])
})
it('misses on pointer down over shape, misses on pointer up when locked', () => {
editor.updateShape({ id: ids.box1, type: 'geo', isLocked: true })
editor.pointerMove(50, 50)
editor.pointerDown() // on the edge of box1 (which is empty)
expect(editor.getSelectedShapeIds()).toEqual([])
editor.pointerUp() // does not select because inside of hollow shape
expect(editor.getSelectedShapeIds()).toEqual([])
})
it('misses on pointer down over shape label, misses on pointer up when locked', () => {
editor.updateShape({ id: ids.box1, type: 'geo', isLocked: true })
editor.pointerMove(75, 75)
editor.pointerDown() // on the edge of box1 (which is empty)
expect(editor.getSelectedShapeIds()).toEqual([])
editor.pointerUp() // does not select because inside of hollow shape
expect(editor.getSelectedShapeIds()).toEqual([])
})
it('misses when shape is masked by frame on pointer down over shape, misses on pointer up', () => {
editor.pointerMove(110, 50)
expect(editor.getHoveredShapeId()).toBe(null)
editor.pointerDown() // inside of box1 but outside of frame1
expect(editor.getSelectedShapeIds()).toEqual([])
editor.pointerUp()
expect(editor.getSelectedShapeIds()).toEqual([])
})
it('hits frame on pointer down over shape margin (inside)', () => {
editor.pointerMove(96, 50)
expect(editor.getHoveredShapeId()).toBe(frame1.id)
editor.pointerDown() // inside of box1, in margin of frame1
expect(editor.getSelectedShapeIds()).toEqual([frame1.id])
editor.pointerUp()
expect(editor.getSelectedShapeIds()).toEqual([frame1.id])
})
it('hits frame on pointer down over shape margin where intersecting child shape margin (inside)', () => {
editor.pointerMove(96, 25)
expect(editor.getHoveredShapeId()).toBe(box1.id)
editor.pointerDown() // in margin of box1 AND frame1
expect(editor.getSelectedShapeIds()).toEqual([box1.id])
editor.pointerUp()
expect(editor.getSelectedShapeIds()).toEqual([box1.id])
})
it('hits frame on pointer down over shape margin (outside)', () => {
editor.pointerMove(104, 25)
expect(editor.getHoveredShapeId()).toBe(frame1.id)
editor.pointerDown()
expect(editor.getSelectedShapeIds()).toEqual([frame1.id])
editor.pointerUp()
expect(editor.getSelectedShapeIds()).toEqual([frame1.id])
})
it('misses on pointer down outside of shape', () => {
editor.pointerMove(250, 50)
expect(editor.getHoveredShapeId()).toBe(null)
editor.pointerDown()
expect(editor.getSelectedShapeIds()).toEqual([])
editor.pointerUp()
expect(editor.getSelectedShapeIds()).toEqual([])
})
it('brushes on point inside and drag', () => {
editor.pointerMove(50, 50)
expect(editor.getHoveredShapeId()).toBe(null)
editor.pointerDown()
expect(editor.getSelectedShapeIds()).toEqual([])
editor.pointerMove(55, 55)
editor.expectToBeIn('select.brushing')
editor.pointerUp()
expect(editor.getSelectedShapeIds()).toEqual([])
})
it('misses when shape is behind frame', () => {
editor.deleteShape(ids.box1)
editor.createShape({
id: ids.box5,
parentId: editor.getCurrentPageId(),
type: 'geo',
props: {
w: 75,
h: 75,
},
})
editor.sendToBack([ids.box5])
editor.pointerMove(50, 50)
expect(editor.getHoveredShapeId()).toBe(null)
editor.pointerDown()
expect(editor.getSelectedShapeIds()).toEqual([])
editor.pointerUp()
expect(editor.getSelectedShapeIds()).toEqual([])
editor.pointerMove(75, 75)
expect(editor.getHoveredShapeId()).toBe(null)
editor.pointerDown()
expect(editor.getSelectedShapeIds()).toEqual([])
editor.pointerUp()
expect(editor.getSelectedShapeIds()).toEqual([])
})
})
describe('when a frame has multiple children', () => {
let box1: TLGeoShape
let box2: TLGeoShape
beforeEach(() => {
editor
.createShape<TLFrameShape>({ id: ids.frame1, type: 'frame', props: { w: 100, h: 100 } })
.createShape<TLGeoShape>({
id: ids.box1,
parentId: ids.frame1,
type: 'geo',
x: 25,
y: 25,
})
.createShape<TLGeoShape>({
id: ids.box2,
parentId: ids.frame1,
type: 'geo',
x: 50,
y: 50,
props: {
w: 80,
h: 80,
},
})
box1 = editor.getShape<TLGeoShape>(ids.box1)!
box2 = editor.getShape<TLGeoShape>(ids.box2)!
})
// This is no longer the case; it will be true for arrows though
// it('selects the smaller of two overlapping hollow shapes on pointer up when both are the child of a frame', () => {
// // make box2 smaller
// editor.updateShape({ ...box2, props: { w: 99, h: 99 } })
// editor.pointerMove(64, 64)
// expect(editor.hoveredShapeId).toBe(null)
// editor.pointerDown()
// expect(editor.selectedShapeIds).toEqual([])
// editor.pointerUp()
// expect(editor.selectedShapeIds).toEqual([ids.box2])
// // make box2 bigger...
// editor.selectNone()
// editor.updateShape({ ...box2, props: { w: 101, h: 101 } })
// editor.pointerMove(64, 64)
// expect(editor.hoveredShapeId).toBe(null)
// editor.pointerDown()
// expect(editor.selectedShapeIds).toEqual([])
// editor.pointerUp()
// expect(editor.selectedShapeIds).toEqual([ids.box1])
// })
it('brush does not select a shape when brushing its masked parts', () => {
editor.pointerMove(110, 0)
expect(editor.getHoveredShapeId()).toBe(null)
editor.pointerDown()
editor.pointerMove(160, 160)
editor.expectToBeIn('select.brushing')
editor.pointerUp()
expect(editor.getSelectedShapeIds()).toEqual([])
})
it('brush selects a shape inside of the frame', () => {
editor.pointerMove(10, 10)
expect(editor.getHoveredShapeId()).toBe(null)
editor.pointerDown()
editor.pointerMove(30, 30)
editor.expectToBeIn('select.brushing')
editor.pointerUp()
expect(editor.getSelectedShapeIds()).toEqual([ids.box1])
})
it('brush selects a shape when dragging from outside of the frame', () => {
editor.pointerMove(-50, -50)
expect(editor.getHoveredShapeId()).toBe(null)
editor.pointerDown()
editor.pointerMove(30, 30)
editor.expectToBeIn('select.brushing')
expect(editor.getSelectedShapeIds()).toEqual([ids.box1])
editor.pointerUp()
})
it('brush selects shapes when containing them in a drag from outside of the frame', () => {
editor.updateShape({ ...box1, x: 10, y: 10, props: { w: 10, h: 10 } })
editor.updateShape({ ...box2, x: 20, y: 20, props: { w: 10, h: 10 } })
editor.pointerMove(-50, -50)
expect(editor.getHoveredShapeId()).toBe(null)
editor.pointerDown()
editor.pointerMove(99, 99)
editor.expectToBeIn('select.brushing')
editor.pointerUp()
expect(editor.getSelectedShapeIds()).toEqual([ids.box1, ids.box2])
})
it('brush selects shapes when containing them in a drag from outside of the frame and also having the current page point outside of the frame without containing the frame', () => {
editor.updateShape({ ...box1, x: 10, y: 10, props: { w: 10, h: 10 } })
editor.updateShape({ ...box2, x: 20, y: 20, props: { w: 10, h: 10 } })
editor.pointerMove(5, -50)
expect(editor.getHoveredShapeId()).toBe(null)
editor.pointerDown()
editor.pointerMove(150, 150)
editor.expectToBeIn('select.brushing')
editor.pointerUp()
expect(editor.getSelectedShapeIds()).toEqual([ids.box1, ids.box2])
})
it('selects only the frame when brush wraps the entire frame', () => {
editor.updateShape({ ...box1, x: 10, y: 10, props: { w: 10, h: 10 } })
editor.updateShape({ ...box2, x: 20, y: 20, props: { w: 10, h: 10 } })
editor.pointerMove(-50, -50)
expect(editor.getHoveredShapeId()).toBe(null)
editor.pointerDown()
editor.pointerMove(150, 150)
editor.expectToBeIn('select.brushing')
editor.pointerUp()
expect(editor.getSelectedShapeIds()).toEqual([ids.frame1])
})
it('selects only the frame when brush wraps the entire frame (with overlapping / masked shapes)', () => {
editor.pointerMove(-50, -50)
expect(editor.getHoveredShapeId()).toBe(null)
editor.pointerDown()
editor.pointerMove(150, 150)
editor.expectToBeIn('select.brushing')
editor.pointerUp()
expect(editor.getSelectedShapeIds()).toEqual([ids.frame1])
})
})
describe('when shape is selected', () => {
it('hits on pointer down over shape, misses on pointer up', () => {
editor.createShapes([{ id: ids.box1, type: 'geo', props: { fill: 'none' } }])
editor.select(ids.box1)
editor.pointerMove(75, 75)
expect(editor.getHoveredShapeId()).toBe(null)
editor.pointerDown()
expect(editor.getSelectedShapeIds()).toEqual([ids.box1])
editor.pointerUp()
expect(editor.getSelectedShapeIds()).toEqual([ids.box1])
})
})
describe('When shapes are overlapping', () => {
let box2: TLGeoShape
let box4: TLGeoShape
let box5: TLGeoShape
beforeEach(() => {
editor.createShapes<TLGeoShape>([
{
id: ids.box1,
type: 'geo',
x: 0,
y: 0,
props: {
w: 300,
h: 300,
},
},
{
id: ids.box2,
type: 'geo',
x: 50,
y: 50,
props: {
w: 100,
h: 150,
},
},
{
id: ids.box3,
type: 'geo',
x: 75,
y: 75,
props: {
w: 100,
h: 100,
},
},
{
id: ids.box4,
type: 'geo',
x: 100,
y: 25,
props: {
w: 100,
h: 100,
fill: 'solid',
},
},
{
id: ids.box5,
type: 'geo',
x: 125,
y: 0,
props: {
w: 100,
h: 100,
fill: 'solid',
},
},
])
box2 = editor.getShape<TLGeoShape>(ids.box2)!
box4 = editor.getShape<TLGeoShape>(ids.box4)!
box5 = editor.getShape<TLGeoShape>(ids.box5)!
editor.sendToBack([ids.box4])
editor.bringToFront([ids.box5])
editor.bringToFront([ids.box2])
expect(editor.getCurrentPageShapesSorted().map((s) => s.id)).toEqual([
ids.box4, // filled
ids.box1, // hollow
ids.box3, // hollow
ids.box5, // filled
ids.box2, // hollow
])
})
it('selects the filled shape behind the hollow shapes', () => {
editor.pointerMove(110, 90)
expect(editor.getHoveredShapeId()).toBe(box4.id)
editor.pointerDown()
expect(editor.getSelectedShapeIds()).toEqual([box4.id])
editor.pointerUp()
expect(editor.getSelectedShapeIds()).toEqual([box4.id])
})
it('selects the hollow above the filled shapes when in margin', () => {
expect(editor.getCurrentPageShapesSorted().map((s) => s.id)).toEqual([
ids.box4,
ids.box1,
ids.box3,
ids.box5,
ids.box2,
])
editor.pointerMove(125, 50)
expect(editor.getHoveredShapeId()).toBe(box2.id)
editor.pointerDown()
expect(editor.getSelectedShapeIds()).toEqual([box2.id])
editor.pointerUp()
expect(editor.getSelectedShapeIds()).toEqual([box2.id])
})
it('selects the front-most filled shape', () => {
editor.pointerMove(175, 50)
expect(editor.getHoveredShapeId()).toBe(box5.id)
editor.pointerDown()
expect(editor.getSelectedShapeIds()).toEqual([box5.id])
editor.pointerUp()
expect(editor.getSelectedShapeIds()).toEqual([box5.id])
})
// it('selects the smallest overlapping hollow shape', () => {
// editor.pointerMove(125, 175)
// expect(editor.hoveredShapeId).toBe(null)
// editor.pointerDown()
// expect(editor.selectedShapeIds).toEqual([])
// editor.pointerUp()
// expect(editor.selectedShapeIds).toEqual([box3.id])
// editor.selectNone()
// expect(editor.hoveredShapeId).toBe(null)
// editor.pointerMove(64, 64)
// expect(editor.hoveredShapeId).toBe(null)
// editor.pointerDown()
// expect(editor.selectedShapeIds).toEqual([])
// editor.pointerUp()
// expect(editor.selectedShapeIds).toEqual([box2.id])
// editor.selectNone()
// editor.pointerMove(35, 35)
// expect(editor.hoveredShapeId).toBe(null)
// editor.pointerDown()
// expect(editor.selectedShapeIds).toEqual([])
// editor.pointerUp()
// expect(editor.selectedShapeIds).toEqual([box1.id])
// })
})
describe('Selects inside of groups', () => {
beforeEach(() => {
editor.createShapes([
{ id: ids.box1, type: 'geo', x: 0, y: 0, props: { w: 100, h: 100 } },
{ id: ids.box2, type: 'geo', x: 200, y: 0, props: { w: 100, h: 100, fill: 'solid' } },
])
editor.groupShapes([ids.box1, ids.box2], ids.group1)
editor.selectNone()
})
it('cretes the group with the correct bounds', () => {
expect(editor.getShapeGeometry(ids.group1).bounds).toMatchObject({
x: 0,
y: 0,
w: 300,
h: 100,
})
})
it('does not selects the group when clicking over the group but between grouped shapes bounds', () => {
editor.pointerMove(150, 50)
expect(editor.getHoveredShapeId()).toBe(null)
editor.pointerDown()
expect(editor.getSelectedShapeIds()).toEqual([])
editor.pointerUp()
expect(editor.getSelectedShapeIds()).toEqual([])
})
it('selects on page down when over an edge of shape in th group children', () => {
editor.pointerMove(0, 50)
expect(editor.getHoveredShapeId()).toBe(ids.group1)
editor.pointerDown()
expect(editor.getSelectedShapeIds()).toEqual([ids.group1])
editor.pointerUp()
expect(editor.getSelectedShapeIds()).toEqual([ids.group1])
})
it('selects on page down when over a filled shape in group children', () => {
editor.pointerMove(250, 50)
expect(editor.getHoveredShapeId()).toBe(ids.group1)
editor.pointerDown()
expect(editor.getSelectedShapeIds()).toEqual([ids.group1])
editor.pointerUp()
expect(editor.getSelectedShapeIds()).toEqual([ids.group1])
})
it('drops selection when pointing up on the space between shapes in a group', () => {
editor.pointerMove(0, 0)
expect(editor.getHoveredShapeId()).toBe(ids.group1)
editor.pointerDown()
expect(editor.getSelectedShapeIds()).toEqual([ids.group1])
editor.pointerUp()
expect(editor.getSelectedShapeIds()).toEqual([ids.group1])
editor.pointerMove(150, 50)
expect(editor.getHoveredShapeId()).toBe(null) // the hovered shape (group1) is already selected
editor.pointerDown()
editor.expectToBeIn('select.pointing_selection')
expect(editor.getSelectedShapeIds()).toEqual([ids.group1])
editor.pointerUp()
expect(editor.getSelectedShapeIds()).toEqual([])
})
it('selects child when pointing on a filled child shape', () => {
editor.pointerMove(250, 0)
expect(editor.getHoveredShapeId()).toBe(ids.group1)
editor.pointerDown()
editor.pointerUp()
expect(editor.getSelectedShapeIds()).toEqual([ids.group1])
editor.pointerDown()
editor.expectToBeIn('select.pointing_shape')
expect(editor.getSelectedShapeIds()).toEqual([ids.group1])
editor.pointerUp()
expect(editor.getSelectedShapeIds()).toEqual([ids.box2])
})
// it('selects child when pointing inside of a hollow child shape', () => {
// editor.pointerMove(75, 75)
// expect(editor.hoveredShapeId).toBe(null)
// editor.pointerDown()
// expect(editor.selectedShapeIds).toEqual([])
// editor.pointerUp()
// expect(editor.selectedShapeIds).toEqual([ids.group1])
// editor.pointerDown()
// editor.expectToBeIn('select.pointing_selection')
// expect(editor.selectedShapeIds).toEqual([ids.group1])
// editor.pointerUp()
// expect(editor.selectedShapeIds).toEqual([ids.box1])
// })
it('selects a solid shape in a group when double clicking it', () => {
editor.pointerMove(250, 50)
expect(editor.getHoveredShapeId()).toBe(ids.group1)
editor.doubleClick()
expect(editor.getSelectedShapeIds()).toEqual([ids.box2])
expect(editor.getFocusedGroupId()).toBe(ids.group1)
})
it('selects a solid shape in a group when double clicking its margin', () => {
editor.pointerMove(198, 50)
expect(editor.getHoveredShapeId()).toBe(ids.group1)
editor.doubleClick()
expect(editor.getSelectedShapeIds()).toEqual([ids.box2])
expect(editor.getFocusedGroupId()).toBe(ids.group1)
})
// it('selects a hollow shape in a group when double clicking it', () => {
// editor.pointerMove(50, 50)
// expect(editor.hoveredShapeId).toBe(null)
// editor.doubleClick()
// expect(editor.selectedShapeIds).toEqual([ids.box1])
// expect(editor.focusedGroupId).toBe(ids.group1)
// })
it('selects a hollow shape in a group when double clicking its edge', () => {
editor.pointerMove(102, 50)
expect(editor.getHoveredShapeId()).toBe(ids.group1)
editor.doubleClick()
expect(editor.getSelectedShapeIds()).toEqual([ids.box1])
expect(editor.getFocusedGroupId()).toBe(ids.group1)
})
// it('double clicks a hollow shape when the focus layer is the shapes parent', () => {
// editor.pointerMove(50, 50)
// expect(editor.hoveredShapeId).toBe(null)
// editor.doubleClick()
// editor.doubleClick()
// expect(editor.editingShapeId).toBe(ids.box1)
// editor.expectToBeIn('select.editing_shape')
// })
it('double clicks a solid shape to edit it when the focus layer is the shapes parent', () => {
editor.pointerMove(250, 50)
expect(editor.getHoveredShapeId()).toBe(ids.group1)
editor.doubleClick()
editor.doubleClick()
expect(editor.getEditingShapeId()).toBe(ids.box2)
editor.expectToBeIn('select.editing_shape')
})
// it('double clicks a sibling shape to edit it when the focus layer is the shapes parent', () => {
// editor.pointerMove(50, 50)
// expect(editor.hoveredShapeId).toBe(null)
// editor.doubleClick()
// editor.pointerMove(250, 50)
// expect(editor.hoveredShapeId).toBe(ids.box2)
// editor.doubleClick()
// expect(editor.editingShapeId).toBe(ids.box2)
// editor.expectToBeIn('select.editing_shape')
// })
// it('selects a different sibling shape when editing a layer', () => {
// editor.pointerMove(50, 50)
// expect(editor.hoveredShapeId).toBe(null)
// editor.doubleClick()
// editor.doubleClick()
// expect(editor.editingShapeId).toBe(ids.box1)
// editor.expectToBeIn('select.editing_shape')
// editor.pointerMove(250, 50)
// expect(editor.hoveredShapeId).toBe(ids.box2)
// editor.pointerDown()
// editor.expectToBeIn('select.pointing_shape')
// expect(editor.editingShapeId).toBe(null)
// expect(editor.selectedShapeIds).toEqual([ids.box2])
// })
})
describe('when selecting behind selection', () => {
beforeEach(() => {
editor
.createShapes([
{ id: ids.box1, type: 'geo', x: 100, y: 0, props: { fill: 'solid' } },
{ id: ids.box2, type: 'geo', x: 0, y: 0 },
{ id: ids.box3, type: 'geo', x: 200, y: 0 },
])
.select(ids.box2, ids.box3)
})
it('does not select on pointer down, only on pointer up', () => {
editor.pointerMove(175, 75)
expect(editor.getHoveredShapeId()).toBe(ids.box1)
editor.pointerDown() // inside of box 1
expect(editor.getSelectedShapeIds()).toEqual([ids.box2, ids.box3])
editor.pointerUp()
expect(editor.getSelectedShapeIds()).toEqual([ids.box1])
})
it('can drag the selection', () => {
editor.pointerMove(175, 75)
expect(editor.getHoveredShapeId()).toBe(ids.box1)
editor.pointerDown() // inside of box 1
expect(editor.getSelectedShapeIds()).toEqual([ids.box2, ids.box3])
editor.pointerMove(250, 50)
editor.expectToBeIn('select.translating')
editor.pointerMove(150, 50)
editor.pointerUp()
expect(editor.getSelectedShapeIds()).toEqual([ids.box2, ids.box3])
})
})
describe('when shift+selecting', () => {
beforeEach(() => {
editor
.createShapes([
{ id: ids.box1, type: 'geo', x: 0, y: 0 },
{ id: ids.box2, type: 'geo', x: 200, y: 0 },
{ id: ids.box3, type: 'geo', x: 400, y: 0, props: { fill: 'solid' } },
])
.select(ids.box1)
})
it('adds solid shape to selection on pointer down', () => {
editor.keyDown('Shift')
editor.pointerMove(450, 50) // inside of box 3
expect(editor.getHoveredShapeId()).toBe(ids.box3)
editor.pointerDown()
expect(editor.getSelectedShapeIds()).toEqual([ids.box1, ids.box3])
editor.pointerUp()
expect(editor.getSelectedShapeIds()).toEqual([ids.box1, ids.box3])
})
it('adds and removes solid shape from selection on pointer up (without causing a double click)', () => {
editor.keyDown('Shift')
editor.pointerMove(450, 50) // above box 3
expect(editor.getHoveredShapeId()).toBe(ids.box3)
editor.pointerDown()
expect(editor.getSelectedShapeIds()).toEqual([ids.box1, ids.box3])
editor.pointerUp()
expect(editor.getSelectedShapeIds()).toEqual([ids.box1, ids.box3])
editor.pointerDown()
expect(editor.getSelectedShapeIds()).toEqual([ids.box1, ids.box3])
editor.pointerUp()
expect(editor.getSelectedShapeIds()).toEqual([ids.box1])
})
it('adds and removes solid shape from selection on double clicks (without causing an edit by double clicks)', () => {
editor.keyDown('Shift')
editor.pointerMove(450, 50) // above box 3
expect(editor.getHoveredShapeId()).toBe(ids.box3)
editor.doubleClick()
expect(editor.getSelectedShapeIds()).toEqual([ids.box1, ids.box3])
editor.doubleClick()
expect(editor.getSelectedShapeIds()).toEqual([ids.box1])
})
it('adds how shape to selection on pointer down when pointing margin', () => {
editor.keyDown('Shift')
editor.pointerMove(204, 50) // inside of box 2 margin
expect(editor.getHoveredShapeId()).toBe(ids.box2)
editor.pointerDown()
expect(editor.getSelectedShapeIds()).toEqual([ids.box1, ids.box2])
editor.pointerUp()
expect(editor.getSelectedShapeIds()).toEqual([ids.box1, ids.box2])
})
it('adds and removes hollow shape from selection on pointer up (without causing a double click) when pointing margin', () => {
editor.keyDown('Shift')
editor.pointerMove(204, 50) // inside of box 2 margin
expect(editor.getHoveredShapeId()).toBe(ids.box2)
editor.pointerDown()
expect(editor.getSelectedShapeIds()).toEqual([ids.box1, ids.box2])
editor.pointerUp()
expect(editor.getSelectedShapeIds()).toEqual([ids.box1, ids.box2])
editor.pointerDown()
expect(editor.getSelectedShapeIds()).toEqual([ids.box1, ids.box2])
editor.pointerUp()
expect(editor.getSelectedShapeIds()).toEqual([ids.box1])
})
it('does not add hollow shape to selection on pointer up when in empty space', () => {
expect(editor.getSelectedShapeIds()).toEqual([ids.box1])
editor.keyDown('Shift')
editor.pointerMove(215, 75) // above box 2
expect(editor.getHoveredShapeId()).toBe(null)
editor.pointerDown()
expect(editor.getSelectedShapeIds()).toEqual([ids.box1])
editor.pointerUp()
expect(editor.getSelectedShapeIds()).toEqual([ids.box1])
})
it('does not add hollow shape to selection on pointer up when over the edge/label, but select on pointer up', () => {
expect(editor.getSelectedShapeIds()).toEqual([ids.box1])
editor.keyDown('Shift')
editor.pointerMove(250, 50) // above box 2's label
expect(editor.getHoveredShapeId()).toBe(null)
editor.pointerDown()
expect(editor.getSelectedShapeIds()).toEqual([ids.box1])
editor.pointerUp()
expect(editor.getSelectedShapeIds()).toEqual([ids.box1, ids.box2])
})
it('does not add and remove hollow shape from selection on pointer up (without causing an edit by double clicks)', () => {
editor.keyDown('Shift')
editor.pointerMove(215, 75) // above box 2, empty space
expect(editor.getHoveredShapeId()).toBe(null)
editor.pointerDown()
expect(editor.getSelectedShapeIds()).toEqual([ids.box1])
editor.pointerUp()
expect(editor.getSelectedShapeIds()).toEqual([ids.box1])
editor.pointerDown()
expect(editor.getSelectedShapeIds()).toEqual([ids.box1])
editor.pointerUp()
expect(editor.getSelectedShapeIds()).toEqual([ids.box1])
})
it('does not add and remove hollow shape from selection on double clicks (without causing an edit by double clicks)', () => {
editor.keyDown('Shift')
editor.pointerMove(215, 75) // above box 2, empty space
expect(editor.getHoveredShapeId()).toBe(null)
editor.doubleClick()
expect(editor.getSelectedShapeIds()).toEqual([ids.box1])
editor.doubleClick()
expect(editor.getSelectedShapeIds()).toEqual([ids.box1])
})
})
describe('when shift+selecting a group', () => {
beforeEach(() => {
editor
.createShapes([
{ id: ids.box1, type: 'geo', x: 0, y: 0 },
{ id: ids.box2, type: 'geo', x: 200, y: 0 },
{ id: ids.box3, type: 'geo', x: 400, y: 0, props: { fill: 'solid' } },
{ id: ids.box4, type: 'geo', x: 600, y: 0 },
])
.groupShapes([ids.box2, ids.box3], ids.group1)
.select(ids.box1)
})
it('does not add group to selection when pointing empty space in the group', () => {
editor.keyDown('Shift')
editor.pointerMove(350, 50)
expect(editor.getHoveredShapeId()).toBe(null)
editor.pointerDown() // inside of box 2, inside of group 1
expect(editor.getSelectedShapeIds()).toEqual([ids.box1])
editor.pointerUp()
expect(editor.getSelectedShapeIds()).toEqual([ids.box1])
})
it('does not add to selection on shift + on pointer up when clicking in hollow shape', () => {
editor.keyDown('Shift')
editor.pointerMove(215, 75)
expect(editor.getHoveredShapeId()).toBe(null)
editor.pointerDown() // inside of box 2, inside of group 1
expect(editor.getSelectedShapeIds()).toEqual([ids.box1])
editor.pointerUp()
expect(editor.getSelectedShapeIds()).toEqual([ids.box1])
})
it('adds to selection on pointer down when clicking in margin', () => {
editor.keyDown('Shift')
editor.pointerMove(304, 50)
expect(editor.getHoveredShapeId()).toBe(ids.group1)
editor.pointerDown() // inside of box 2, inside of group 1
expect(editor.getSelectedShapeIds()).toEqual([ids.box1, ids.group1])
editor.pointerUp()
expect(editor.getSelectedShapeIds()).toEqual([ids.box1, ids.group1])
})
it('adds to selection on pointer down when clicking in filled', () => {
editor.keyDown('Shift')
editor.pointerMove(450, 50)
expect(editor.getHoveredShapeId()).toBe(ids.group1)
editor.pointerDown() // inside of box 2, inside of group 1
expect(editor.getSelectedShapeIds()).toEqual([ids.box1, ids.group1])
editor.pointerUp()
expect(editor.getSelectedShapeIds()).toEqual([ids.box1, ids.group1])
})
it('does not select when shift+clicking into hollow shape inside of a group', () => {
editor.pointerMove(215, 75)
editor.keyDown('Shift')
expect(editor.getHoveredShapeId()).toBe(null)
editor.pointerDown() // inside of box 2, empty space, inside of group 1
expect(editor.getSelectedShapeIds()).toEqual([ids.box1])
editor.pointerUp()
expect(editor.getSelectedShapeIds()).toEqual([ids.box1])
})
it('does not deselect on pointer up when clicking into empty space in hollow shape', () => {
editor.keyDown('Shift')
editor.pointerMove(215, 75)
expect(editor.getHoveredShapeId()).toBe(null)
editor.pointerDown() // inside of box 2, empty space, inside of group 1
expect(editor.getSelectedShapeIds()).toEqual([ids.box1])
editor.pointerUp()
expect(editor.getSelectedShapeIds()).toEqual([ids.box1])
editor.pointerDown()
expect(editor.getSelectedShapeIds()).toEqual([ids.box1])
editor.pointerUp()
expect(editor.getSelectedShapeIds()).toEqual([ids.box1])
})
})
// some of these tests are adapted from the "select hollow shape on pointer up" logic, which was removed.
// the tests may seem arbitrary but their mostly negating the logic that was introduced in that feature.
describe('When children / descendants of a group are selected', () => {
beforeEach(() => {
editor
.createShapes([
{ id: ids.box1, type: 'geo', x: 0, y: 0 },
{ id: ids.box2, type: 'geo', x: 200, y: 0 },
{ id: ids.box3, type: 'geo', x: 400, y: 0, props: { fill: 'solid' } },
{ id: ids.box4, type: 'geo', x: 600, y: 0 },
{ id: ids.box5, type: 'geo', x: 800, y: 0 },
])
.groupShapes([ids.box1, ids.box2], ids.group1)
.groupShapes([ids.box3, ids.box4], ids.group2)
.groupShapes([ids.group1, ids.group2], ids.group3)
.selectNone()
})
it('selects the child', () => {
editor.select(ids.box1)
expect(editor.getSelectedShapeIds()).toEqual([ids.box1])
expect(editor.getFocusedGroupId()).toBe(ids.group1)
})
it('selects the children', () => {
editor.select(ids.box1, ids.box2)
expect(editor.getSelectedShapeIds()).toEqual([ids.box1, ids.box2])
expect(editor.getFocusedGroupId()).toBe(ids.group1)
})
it('does not allow parents and children to be selected, picking the parent', () => {
editor.select(ids.group1, ids.box1)
expect(editor.getSelectedShapeIds()).toEqual([ids.group1])
expect(editor.getFocusedGroupId()).toBe(ids.group3)
editor.select(ids.group1, ids.box1, ids.box2)
expect(editor.getSelectedShapeIds()).toEqual([ids.group1])
expect(editor.getFocusedGroupId()).toBe(ids.group3)
})
it('does not allow ancestors and children to be selected, picking the ancestor', () => {
editor.select(ids.group3, ids.box1)
expect(editor.getSelectedShapeIds()).toEqual([ids.group3])
expect(editor.getFocusedGroupId()).toBe(editor.getCurrentPageId())
editor.select(ids.group3, ids.box1, ids.box2)
expect(editor.getSelectedShapeIds()).toEqual([ids.group3])
expect(editor.getFocusedGroupId()).toBe(editor.getCurrentPageId())
editor.select(ids.group3, ids.group2, ids.box1)
expect(editor.getSelectedShapeIds()).toEqual([ids.group3])
expect(editor.getFocusedGroupId()).toBe(editor.getCurrentPageId())
})
it('picks the highest common focus layer id', () => {
editor.select(ids.box1, ids.box4) // child of group1, child of group 2
expect(editor.getSelectedShapeIds()).toEqual([ids.box1, ids.box4])
expect(editor.getFocusedGroupId()).toBe(ids.group3)
})
it('picks the highest common focus layer id', () => {
editor.select(ids.box1, ids.box5) // child of group1 and child of the page
expect(editor.getSelectedShapeIds()).toEqual([ids.box1, ids.box5])
expect(editor.getFocusedGroupId()).toBe(editor.getCurrentPageId())
})
it('sets the parent to the highest common ancestor', () => {
editor.selectNone()
expect(editor.getFocusedGroupId()).toBe(editor.getCurrentPageId())
editor.select(ids.group3)
expect(editor.getFocusedGroupId()).toBe(editor.getCurrentPageId())
editor.select(ids.group3, ids.box1)
expect(editor.getFocusedGroupId()).toBe(editor.getCurrentPageId())
expect(editor.getSelectedShapeIds()).toEqual([ids.group3])
})
})
describe('When pressing the enter key with groups selected', () => {
beforeEach(() => {
editor
.createShapes([
{ id: ids.box1, type: 'geo', x: 0, y: 0 },
{ id: ids.box2, type: 'geo', x: 200, y: 0 },
{ id: ids.box3, type: 'geo', x: 400, y: 0, props: { fill: 'solid' } },
{ id: ids.box4, type: 'geo', x: 600, y: 0 },
{ id: ids.box5, type: 'geo', x: 800, y: 0 },
])
.groupShapes([ids.box1, ids.box2], ids.group1)
.groupShapes([ids.box3, ids.box4], ids.group2)
})
it('selects the children of the groups on enter up', () => {
editor.select(ids.group1, ids.group2)
editor.keyDown('Enter')
expect(editor.getSelectedShapeIds()).toEqual([ids.group1, ids.group2])
expect(editor.getFocusedGroupId()).toBe(editor.getCurrentPageId())
editor.keyUp('Enter')
expect(editor.getSelectedShapeIds()).toEqual([ids.box1, ids.box2, ids.box3, ids.box4])
expect(editor.getFocusedGroupId()).toBe(editor.getCurrentPageId())
})
it('repeats children of the groups on enter up', () => {
editor.groupShapes([ids.group1, ids.group2], ids.group3)
editor.select(ids.group3)
expect(editor.getSelectedShapeIds()).toEqual([ids.group3])
editor.keyDown('Enter').keyUp('Enter')
expect(editor.getSelectedShapeIds()).toEqual([ids.group1, ids.group2])
expect(editor.getFocusedGroupId()).toBe(ids.group3)
editor.keyDown('Enter').keyUp('Enter')
expect(editor.getSelectedShapeIds()).toEqual([ids.box1, ids.box2, ids.box3, ids.box4])
expect(editor.getFocusedGroupId()).toBe(ids.group3)
})
it('does not select the children of the group if a non-group is also selected', () => {
editor.select(ids.group1, ids.group2, ids.box5)
editor.keyDown('Enter')
expect(editor.getSelectedShapeIds()).toEqual([ids.group1, ids.group2, ids.box5])
editor.keyUp('Enter')
expect(editor.getSelectedShapeIds()).toEqual([ids.group1, ids.group2, ids.box5])
})
})
describe('When double clicking an editable shape', () => {
beforeEach(() => {
editor.createShapes([
{ id: ids.box1, type: 'geo', x: 0, y: 0 },
{
id: ids.box2,
type: 'arrow',
x: 200,
y: 50,
props: {
start: { type: 'point', x: 0, y: 0 },
end: { type: 'point', x: 100, y: 0 },
},
},
])
})
it('starts editing on double click', () => {
editor.pointerMove(50, 50).doubleClick()
expect(editor.getSelectedShapeIds()).toEqual([ids.box1])
expect(editor.getEditingShapeId()).toBe(ids.box1)
editor.expectToBeIn('select.editing_shape')
})
it('does not start editing on double click if shift is down', () => {
editor.pointerMove(50, 50).keyDown('Shift').doubleClick()
expect(editor.getSelectedShapeIds()).toEqual([ids.box1])
expect(editor.getEditingShapeId()).toBe(null)
editor.expectToBeIn('select.idle')
})
it('starts editing arrow on double click', () => {
editor.pointerMove(250, 50)
editor.doubleClick()
expect(editor.getSelectedShapeIds()).toEqual([ids.box2])
expect(editor.getEditingShapeId()).toBe(ids.box2)
editor.expectToBeIn('select.editing_shape')
editor.doubleClick()
expect(editor.getSelectedShapeIds()).toEqual([ids.box2])
expect(editor.getEditingShapeId()).toBe(ids.box2)
editor.expectToBeIn('select.editing_shape')
})
it('starts editing a child of a group on triple (not double!) click', () => {
editor.createShape({ id: ids.box2, type: 'geo', x: 300, y: 0 })
editor.groupShapes([ids.box1, ids.box2], ids.group1)
editor.selectNone()
editor.pointerMove(50, 50).click() // clicks on the shape label
expect(editor.getSelectedShapeIds()).toEqual([ids.group1])
expect(editor.getEditingShapeId()).toBe(null)
editor.pointerMove(50, 50).click() // clicks on the shape label
expect(editor.getSelectedShapeIds()).toEqual([ids.box1])
expect(editor.getEditingShapeId()).toBe(null)
editor.pointerMove(50, 50).click() // clicks on the shape label
expect(editor.getSelectedShapeIds()).toEqual([ids.box1])
expect(editor.getEditingShapeId()).toBe(ids.box1)
editor.expectToBeIn('select.editing_shape')
})
})
describe('shift brushes to add to the selection', () => {
beforeEach(() => {
editor.user.updateUserPreferences({ isWrapMode: false })
editor
.createShapes([
{ id: ids.box1, type: 'geo', x: 0, y: 0 },
{ id: ids.box2, type: 'geo', x: 200, y: 0 },
{ id: ids.box3, type: 'geo', x: 400, y: 0 },
{ id: ids.box4, type: 'geo', x: 600, y: 200 },
])
.groupShapes([ids.box3, ids.box4], ids.group1)
})
it('does not select when brushing into margin', () => {
editor.pointerMove(-50, -50)
editor.pointerDown()
editor.pointerMove(-1, -1)
expect(editor.getSelectedShapeIds()).toEqual([])
})
it('selects when brushing into shape edge', () => {
editor.pointerMove(-50, -50)
editor.pointerDown()
editor.pointerMove(1, 1)
expect(editor.getSelectedShapeIds()).toEqual([ids.box1])
})
it('selects when wrapping shape', () => {
editor.pointerMove(-50, -50)
editor.pointerDown()
editor.pointerMove(101, 101)
expect(editor.getSelectedShapeIds()).toEqual([ids.box1])
})
it('does not select when brushing into shape edge when holding control', () => {
editor.pointerMove(-50, -50)
editor.keyDown('Control')
editor.pointerDown()
editor.pointerMove(1, 1)
expect(editor.getSelectedShapeIds()).toEqual([])
})
it('selects when wrapping shape when holding control', () => {
editor.pointerMove(-50, -50)
editor.keyDown('Control')
editor.pointerDown()
editor.pointerMove(101, 101)
expect(editor.getSelectedShapeIds()).toEqual([ids.box1])
})
it('does not select a group when colliding only with the groups bounds', () => {
editor.pointerMove(650, -50)
editor.pointerDown()
editor.pointerMove(600, 50)
expect(editor.getSelectedShapeIds()).toEqual([])
})
it('selects a group when colliding with the groups child shape', () => {
editor.pointerMove(650, -50)
editor.pointerDown()
editor.pointerMove(600, 250)
expect(editor.getSelectedShapeIds()).toEqual([ids.group1])
})
it('adds to selection when shift + brushing into shape', () => {
editor.select(ids.box2)
editor.pointerMove(-50, -50)
editor.keyDown('Shift')
editor.pointerDown()
editor.pointerMove(1, 1)
expect(editor.getSelectedShapeIds()).toEqual([ids.box2, ids.box1])
editor.keyUp('Shift')
// there's a timer here—we should keep the shift mode until the timer expires
expect(editor.getSelectedShapeIds()).toEqual([ids.box2, ids.box1])
jest.advanceTimersByTime(500)
// once the timer expires, we should be back in regular mode
expect(editor.getSelectedShapeIds()).toEqual([ids.box1])
editor.keyDown('Shift')
// there's no timer on key down, so go right into shift mode again
expect(editor.getSelectedShapeIds()).toEqual([ids.box2, ids.box1])
})
})
describe('scribble brushes to add to the selection', () => {
beforeEach(() => {
editor.createShapes([
{ id: ids.box1, type: 'geo', x: 0, y: 0 },
{ id: ids.box2, type: 'geo', x: 200, y: 0 },
{ id: ids.box3, type: 'geo', x: 400, y: 0 },
{ id: ids.box4, type: 'geo', x: 600, y: 200 },
])
})
it('does not select when scribbling into margin', () => {
editor.pointerMove(-50, -50)
editor.keyDown('Alt')
editor.pointerDown()
editor.pointerMove(-1, -1)
editor.expectToBeIn('select.scribble_brushing')
expect(editor.getSelectedShapeIds()).toEqual([])
})
it('selects when scribbling into shape edge', () => {
editor.pointerMove(-50, -50)
editor.keyDown('Alt')
editor.pointerDown()
editor.pointerMove(1, 1)
expect(editor.getSelectedShapeIds()).toEqual([ids.box1])
})
it('selects when scribbling through shape', () => {
editor.pointerMove(-50, -50)
editor.keyDown('Alt')
editor.pointerDown()
editor.pointerMove(101, 101)
expect(editor.getSelectedShapeIds()).toEqual([ids.box1])
})
it('does not select a group when scribble is colliding only with the groups bounds', () => {
editor.pointerMove(650, -50)
editor.keyDown('Alt')
editor.pointerDown()
editor.pointerMove(600, 50)
expect(editor.getSelectedShapeIds()).toEqual([])
})
it('selects a group when scribble is colliding with the groups child shape', () => {
editor.groupShapes([ids.box3, ids.box4], ids.group1)
editor.pointerMove(650, -50)
editor.keyDown('Alt')
editor.pointerDown()
editor.pointerMove(600, 250)
expect(editor.getSelectedShapeIds()).toEqual([ids.group1])
})
it('adds to selection when shift + scribbling into shape', () => {
editor.select(ids.box2)
editor.pointerMove(-50, -50)
editor.keyDown('Alt')
editor.keyDown('Shift')
editor.pointerDown()
editor.pointerMove(50, 50)
expect(editor.getSelectedShapeIds()).toEqual([ids.box1, ids.box2])
editor.keyUp('Shift')
jest.advanceTimersByTime(500)
expect(editor.getSelectedShapeIds()).toEqual([ids.box1])
editor.keyDown('Shift')
expect(editor.getSelectedShapeIds()).toEqual([ids.box1, ids.box2])
})
it('selects when switching between moves', () => {
editor.ungroupShapes([ids.group1]) // ungroup boxes 3 and 4
editor.pointerMove(650, 0)
editor.keyDown('Alt') // scribble
editor.pointerDown()
editor.pointerMove(650, 250) // into box 4
expect(editor.getSelectedShapeIds()).toEqual([ids.box4])
editor.pointerMove(450, 250) // below box 3
expect(editor.getSelectedShapeIds()).toEqual([ids.box4])
editor.keyUp('Alt') // scribble
expect(editor.getSelectedShapeIds()).toEqual([ids.box4]) // still in timer
jest.advanceTimersByTime(1000) // let timer expire
expect(editor.getSelectedShapeIds()).toEqual([ids.box3, ids.box4]) // brushed!
editor.keyDown('Alt') // scribble
expect(editor.getSelectedShapeIds()).toEqual([ids.box4]) // back to brushed only
editor.pointerMove(450, 240) // below box 3
expect(editor.getSelectedShapeIds()).toEqual([ids.box4]) // back to brushed only
})
})
describe('creating text on double click', () => {
it('creates text on double click', () => {
editor.doubleClick()
expect(editor.getCurrentPageShapes().length).toBe(1)
editor.pointerMove(0, 100)
editor.click()
})
})
it.todo('maybe? does not select a hollow closed shape that contains the viewport?')
it.todo('maybe? does not select a hollow closed shape if the negative distance is more than X?')
it.todo(
'maybe? does not edit a hollow geo shape when double clicking inside of it unless it already has a label OR the double click is in the middle of the shape'
)
it('selects one of the selected shapes on pointer up', () => {
editor.createShapes([
{ id: ids.box1, type: 'geo' },
{ id: ids.box2, type: 'geo', x: 300 },
])
editor.selectAll()
editor.pointerMove(96, 50)
editor.pointerDown()
editor.pointerUp()
expect(editor.getSelectedShapeIds()).toEqual([ids.box1])
})
describe('right clicking', () => {
it('selects on right click', () => {
editor.createShapes([{ id: ids.box1, type: 'geo' }])
expect(editor.getSelectedShapeIds()).toEqual([])
editor.pointerMove(4, 4)
editor.pointerDown(4, 4, { target: 'canvas', button: 2 })
editor.pointerUp()
expect(editor.getSelectedShapeIds()).toEqual([ids.box1])
})
it('keeps selection when right-clicking a selection background', () => {
editor.createShapes([{ id: ids.box1, type: 'geo' }])
editor.selectAll()
editor.pointerMove(30, 30)
editor.pointerDown(30, 30, { target: 'canvas', button: 2 })
editor.pointerUp()
expect(editor.getSelectedShapeIds()).toEqual([ids.box1])
})
it('keeps selection when right-clicking a selection background', () => {
editor
.selectAll()
.deleteShapes(editor.getSelectedShapeIds())
.setCurrentTool('arrow')
.pointerMove(500, 500)
.pointerDown()
.pointerMove(600, 600)
.pointerUp()
.selectAll()
.setCurrentTool('select')
expect(editor.getSelectedShapeIds().length).toBe(1)
// Not inside of the shape but inside of the selection bounds
editor.pointerMove(510, 590)
expect(editor.getHoveredShapeId()).toBe(null)
editor.pointerDown(30, 30, { target: 'canvas', button: 2 })
editor.pointerUp()
expect(editor.getSelectedShapeIds()).toEqual([])
})
})
describe('When brushing close to the edges of the screen', () => {
it('moves the camera', () => {
editor.user.updateUserPreferences({ edgeScrollSpeed: 1 })
const camera1 = editor.getCamera()
editor.pointerMove(300, 300)
editor.pointerDown()
editor.pointerMove(0, 0)
jest.advanceTimersByTime(100)
editor.pointerUp()
const camera2 = editor.getCamera()
expect(camera2.x).toBeGreaterThan(camera1.x) // for some reason > is left
expect(camera2.y).toBeGreaterThan(camera1.y) // for some reason > is up
})
it('moves the camera correctly when the viewport is nonzero', () => {
editor.user.updateUserPreferences({ edgeScrollSpeed: 1 })
const camera1 = editor.getCamera()
editor.pointerMove(300, 300)
editor.pointerDown()
editor.pointerMove(100, 100)
jest.advanceTimersByTime(100)
editor.pointerUp()
const camera2 = editor.getCamera()
// should NOT have moved the camera by edge scrolling
expect(camera2.x).toEqual(camera1.x)
expect(camera2.y).toEqual(camera1.y)
// Now change the bounds so that the corner is at 100,100 on the screen
editor.setScreenBounds({ ...editor.getViewportScreenBounds(), x: 100, y: 100 })
editor.user.updateUserPreferences({ edgeScrollSpeed: 1 })
const camera3 = editor.getCamera()
editor.pointerMove(300, 300)
editor.pointerDown()
editor.pointerMove(100, 100)
jest.advanceTimersByTime(100)
editor.pointerUp()
const camera4 = editor.getCamera()
// should NOT have moved the camera by edge scrolling because the edge is now "inset"
expect(camera4.x).toEqual(camera3.x)
expect(camera4.y).toEqual(camera3.y)
editor.pointerDown()
editor.pointerMove(90, 90) // off the edge of the component
jest.advanceTimersByTime(100)
const camera5 = editor.getCamera()
// should have moved the camera by edge scrolling off the component edge
expect(camera5.x).toBeGreaterThan(camera4.x)
expect(camera5.y).toBeGreaterThan(camera4.y)
})
it('selects shapes that are outside of the viewport', () => {
editor.user.updateUserPreferences({ edgeScrollSpeed: 1 })
editor.createShapes([{ id: ids.box1, type: 'geo', x: 100, y: 100, props: { w: 100, h: 100 } }])
editor.createShapes([
{ id: ids.box2, type: 'geo', x: -150, y: -150, props: { w: 100, h: 100 } },
])
editor.pointerMove(300, 300)
editor.pointerDown()
editor.pointerMove(50, 50)
editor.expectToBeIn('select.brushing')
expect(editor.getSelectedShapeIds()).toEqual([ids.box1])
editor.pointerMove(0, 0)
// still only box 1...
expect(editor.getSelectedShapeIds()).toEqual([ids.box1])
jest.advanceTimersByTime(100)
// ...but now viewport will have moved to select box2 as well
expect(editor.getSelectedShapeIds()).toEqual([ids.box1, ids.box2])
editor.pointerUp()
})
it('doesnt edge scroll to the other shape', () => {
editor.user.updateUserPreferences({ edgeScrollSpeed: 0 }) // <-- no edge scrolling
editor.createShapes([{ id: ids.box1, type: 'geo', x: 100, y: 100, props: { w: 100, h: 100 } }])
editor.createShapes([
{ id: ids.box2, type: 'geo', x: -150, y: -150, props: { w: 100, h: 100 } },
])
editor.pointerMove(300, 300)
editor.pointerDown()
editor.pointerMove(50, 50)
editor.expectToBeIn('select.brushing')
expect(editor.getSelectedShapeIds()).toEqual([ids.box1])
editor.pointerMove(0, 0)
expect(editor.getSelectedShapeIds()).toEqual([ids.box1])
jest.advanceTimersByTime(100)
expect(editor.getSelectedShapeIds()).toEqual([ids.box1])
editor.pointerUp()
})
})
describe('When a shape is locked', () => {
beforeEach(() => {
editor.createShape({
id: ids.box1,
type: 'geo',
x: 0,
y: 0,
isLocked: true,
props: { w: 300, h: 300 },
})
})
it('does not select the shape', () => {
editor.pointerDown(50, 50)
editor.expectToBeIn('select.pointing_canvas')
editor.pointerUp()
editor.expectToBeIn('select.idle')
expect(editor.getSelectedShapeIds()).toEqual([])
})
it('allows translating shapes on top of the locked shape', () => {
editor.createShape({ id: ids.box2, x: 50, y: 50, type: 'geo', props: { w: 50, h: 50 } })
editor.createShape({ id: ids.box3, x: 200, y: 200, type: 'geo', props: { w: 50, h: 50 } })
// Select the first shape
editor.pointerMove(60, 60)
editor.pointerDown()
editor.pointerUp()
expect(editor.getSelectedShapeIds()).toEqual([ids.box2])
// Shift select the second shape
editor.pointerMove(210, 210)
editor.keyDown('Shift')
editor.pointerDown()
editor.pointerUp()
editor.keyUp('Shift')
editor.expectToBeIn('select.idle')
expect(editor.getSelectedShapeIds()).toEqual([ids.box2, ids.box3])
// Click between them and start dragging
editor.pointerMove(150, 150)
editor.pointerDown()
editor.expectToBeIn('select.pointing_selection')
editor.pointerMove(100, 150)
editor.expectToBeIn('select.translating')
editor.pointerUp()
editor.expectToBeIn('select.idle')
expect(editor.getSelectedShapeIds()).toEqual([ids.box2, ids.box3])
})
})