kopia lustrzana https://github.com/Tldraw/Tldraw
391 wiersze
11 KiB
TypeScript
391 wiersze
11 KiB
TypeScript
import { createCustomShapeId } from '@tldraw/tlschema'
|
|
import { TestApp } from '../TestApp'
|
|
|
|
let app: TestApp
|
|
|
|
const ids = {
|
|
box1: createCustomShapeId('box1'),
|
|
embed1: createCustomShapeId('embed1'),
|
|
}
|
|
|
|
jest.useFakeTimers()
|
|
|
|
beforeEach(() => {
|
|
app = new TestApp()
|
|
app
|
|
.selectAll()
|
|
.deleteShapes()
|
|
.createShapes([{ id: ids.box1, type: 'geo', x: 100, y: 100, props: { w: 100, h: 100 } }])
|
|
})
|
|
|
|
describe('TLSelectTool.Translating', () => {
|
|
it('Enters from pointing and exits to idle', () => {
|
|
const shape = app.getShapeById(ids.box1)
|
|
app.pointerDown(150, 150, { target: 'shape', shape })
|
|
app.expectToBeIn('select.pointing_shape')
|
|
|
|
app.pointerMove(200, 200)
|
|
app.expectToBeIn('select.translating')
|
|
|
|
app.pointerUp()
|
|
app.expectToBeIn('select.idle')
|
|
})
|
|
|
|
it('Drags a shape', () => {
|
|
const shape = app.getShapeById(ids.box1)
|
|
app.pointerDown(150, 150, { target: 'shape', shape })
|
|
app.pointerMove(200, 200)
|
|
app.pointerUp()
|
|
app.expectShapeToMatch({ id: ids.box1, x: 150, y: 150 })
|
|
})
|
|
|
|
it('Clones a shape, removes the clone, and re-creates the clone', () => {
|
|
const shape = app.getShapeById(ids.box1)
|
|
app.pointerDown(150, 150, { target: 'shape', shape })
|
|
app.pointerMove(200, 200)
|
|
|
|
expect(app.shapesArray.length).toBe(1)
|
|
app.expectShapeToMatch({ id: ids.box1, x: 150, y: 150 })
|
|
const t1 = [...app.shapeIds.values()]
|
|
|
|
app.keyDown('Alt')
|
|
expect(app.shapesArray.length).toBe(2)
|
|
app.expectShapeToMatch({ id: ids.box1, x: 100, y: 100 })
|
|
// const t2 = [...app.shapeIds.values()]
|
|
|
|
app.keyUp('Alt')
|
|
|
|
// There's a timer here! We shouldn't end the clone until the timer is done
|
|
expect(app.shapesArray.length).toBe(2)
|
|
|
|
jest.advanceTimersByTime(250) // tick tock
|
|
|
|
// Timer is done! We should have ended the clone.
|
|
expect(app.shapesArray.length).toBe(1)
|
|
app.expectToBeIn('select.translating')
|
|
|
|
app.expectShapeToMatch({ id: ids.box1, x: 150, y: 150 })
|
|
|
|
expect([...app.shapeIds.values()]).toMatchObject(t1)
|
|
|
|
// todo: Should cloning again duplicate new shapes, or restore the last clone?
|
|
// app.keyDown('Alt')
|
|
// expect(app.shapesArray.length).toBe(2)
|
|
// app.expectShapeToMatch({ id: ids.box1, x: 100, y: 100 })
|
|
// expect([...app.shapeIds.values()]).toMatchObject(t2)
|
|
})
|
|
|
|
it('Constrains when holding shift', () => {
|
|
const shape = app.getShapeById(ids.box1)
|
|
app.pointerDown(150, 150, { target: 'shape', shape })
|
|
app.pointerMove(200, 170)
|
|
app.expectShapeToMatch({ id: ids.box1, x: 150, y: 120 })
|
|
app.keyDown('Shift')
|
|
app.expectShapeToMatch({ id: ids.box1, x: 150, y: 100 })
|
|
})
|
|
|
|
it('Does not expand selection when holding shift and alt', () => {
|
|
const shape = app.getShapeById(ids.box1)
|
|
app.keyDown('Shift')
|
|
|
|
// alt-drag to create a copy:
|
|
app.keyDown('Alt')
|
|
app.pointerDown(150, 150, { target: 'shape', shape })
|
|
app.pointerMove(150, 250)
|
|
app.pointerUp()
|
|
const box2Id = app.onlySelectedShape!.id
|
|
expect(app.shapesArray.length).toStrictEqual(2)
|
|
expect(ids.box1).not.toEqual(box2Id)
|
|
|
|
// shift-alt-drag the original, we shouldn't duplicate the copy too:
|
|
app.pointerDown(150, 150, { target: 'shape', shape })
|
|
expect(app.selectedIds).toStrictEqual([ids.box1])
|
|
app.pointerMove(250, 150)
|
|
app.pointerUp()
|
|
expect(app.shapesArray.length).toStrictEqual(3)
|
|
})
|
|
})
|
|
|
|
describe('PointingHandle', () => {
|
|
it('Enters from idle and exits to idle', () => {
|
|
const shape = app.getShapeById(ids.box1)
|
|
app.pointerDown(150, 150, {
|
|
target: 'handle',
|
|
shape,
|
|
handle: { id: 'start', type: 'vertex', index: 'a1', x: 0, y: 0 },
|
|
})
|
|
app.expectToBeIn('select.pointing_handle')
|
|
|
|
app.pointerUp()
|
|
app.expectToBeIn('select.idle')
|
|
})
|
|
|
|
it('Bails on escape', () => {
|
|
const shape = app.getShapeById(ids.box1)
|
|
app.pointerDown(150, 150, {
|
|
target: 'handle',
|
|
shape,
|
|
handle: { id: 'start', type: 'vertex', index: 'a1', x: 0, y: 0 },
|
|
})
|
|
app.expectToBeIn('select.pointing_handle')
|
|
app.cancel()
|
|
app.expectToBeIn('select.idle')
|
|
})
|
|
})
|
|
|
|
describe('DraggingHandle', () => {
|
|
it('Enters from pointing_handle and exits to idle', () => {
|
|
const shape = app.getShapeById(ids.box1)
|
|
app.pointerDown(150, 150, {
|
|
target: 'handle',
|
|
shape,
|
|
handle: { id: 'start', type: 'vertex', index: 'a1', x: 0, y: 0 },
|
|
})
|
|
app.pointerMove(100, 100)
|
|
app.expectToBeIn('select.dragging_handle')
|
|
|
|
app.pointerUp()
|
|
app.expectToBeIn('select.idle')
|
|
})
|
|
|
|
it('Bails on escape', () => {
|
|
const shape = app.getShapeById(ids.box1)
|
|
|
|
app.pointerDown(150, 150, {
|
|
target: 'handle',
|
|
shape,
|
|
handle: { id: 'start', type: 'vertex', index: 'a1', x: 0, y: 0 },
|
|
})
|
|
app.pointerMove(100, 100)
|
|
app.expectToBeIn('select.dragging_handle')
|
|
app.cancel()
|
|
app.expectToBeIn('select.idle')
|
|
})
|
|
})
|
|
|
|
describe('When double clicking a shape', () => {
|
|
it('begins editing a geo shapes label', () => {
|
|
app
|
|
.selectAll()
|
|
.deleteShapes()
|
|
.selectNone()
|
|
.createShapes([{ id: app.createShapeId(), type: 'geo' }])
|
|
.doubleClick(50, 50, { target: 'shape', shape: app.shapesArray[0] })
|
|
.expectToBeIn('select.editing_shape')
|
|
})
|
|
})
|
|
|
|
describe('When pressing enter on a selected shape', () => {
|
|
it('begins editing a geo shapes label', () => {
|
|
const id = app.createShapeId()
|
|
app
|
|
.selectAll()
|
|
.deleteShapes()
|
|
.selectNone()
|
|
.createShapes([{ id, type: 'geo' }])
|
|
.select(id)
|
|
.keyUp('Enter')
|
|
.expectToBeIn('select.editing_shape')
|
|
})
|
|
})
|
|
|
|
// it('selects the child of a group', () => {
|
|
// const id1 = app.createShapeId()
|
|
// const id2 = app.createShapeId()
|
|
// app
|
|
// .selectAll()
|
|
// .deleteShapes()
|
|
// .selectNone()
|
|
// .createShapes([
|
|
// { id: id1, type: 'geo', x: 100, y: 100 },
|
|
// { id: id2, type: 'geo', x: 200, y: 200 },
|
|
// ])
|
|
// .selectAll()
|
|
// .groupShapes()
|
|
// .doubleClick(50, 50, { target: 'shape', shape: app.getShapeById(id1) })
|
|
// .expectToBeIn('select.editing_shape')
|
|
// })
|
|
|
|
describe('When double clicking the selection edge', () => {
|
|
it('Resets text scale when double clicking the edge of the text', () => {
|
|
const id = app.createShapeId()
|
|
app
|
|
.selectAll()
|
|
.deleteShapes()
|
|
.selectNone()
|
|
.createShapes([{ id, type: 'text', x: 100, y: 100, props: { scale: 2, text: 'hello' } }])
|
|
.select(id)
|
|
.doubleClick(100, 100, { target: 'selection', handle: 'left' })
|
|
|
|
app.expectShapeToMatch({ id, props: { scale: 1 } })
|
|
})
|
|
|
|
it('Resets text autosize first when double clicking the edge of the text', () => {
|
|
const id = app.createShapeId()
|
|
app
|
|
.selectAll()
|
|
.deleteShapes()
|
|
.selectNone()
|
|
.createShapes([
|
|
{
|
|
id,
|
|
type: 'text',
|
|
props: { scale: 2, autoSize: false, w: 200, text: 'hello' },
|
|
},
|
|
])
|
|
.select(id)
|
|
.doubleClick(100, 100, { target: 'selection', handle: 'left' })
|
|
|
|
app.expectShapeToMatch({ id, props: { scale: 2, autoSize: true } })
|
|
|
|
app.doubleClick(100, 100, { target: 'selection', handle: 'left' })
|
|
|
|
app.expectShapeToMatch({ id, props: { scale: 1, autoSize: true } })
|
|
})
|
|
|
|
it('Begins editing the text if handler returns no change', () => {
|
|
const id = app.createShapeId()
|
|
app
|
|
.selectAll()
|
|
.deleteShapes()
|
|
.selectNone()
|
|
.createShapes([
|
|
{
|
|
id,
|
|
type: 'text',
|
|
props: { scale: 2, autoSize: false, w: 200, text: 'hello' },
|
|
},
|
|
])
|
|
.select(id)
|
|
.doubleClick(100, 100, { target: 'selection', handle: 'left' })
|
|
.doubleClick(100, 100, { target: 'selection', handle: 'left' })
|
|
|
|
expect(app.editingId).toBe(null)
|
|
app.expectShapeToMatch({ id, props: { scale: 1, autoSize: true } })
|
|
|
|
app.doubleClick(100, 100, { target: 'selection', handle: 'left' })
|
|
|
|
expect(app.editingId).toBe(id)
|
|
})
|
|
|
|
it('Selects a geo shape when double clicking on its edge', () => {
|
|
const id = app.createShapeId()
|
|
app
|
|
.selectAll()
|
|
.deleteShapes()
|
|
.selectNone()
|
|
.createShapes([
|
|
{
|
|
id,
|
|
type: 'geo',
|
|
},
|
|
])
|
|
.select(id)
|
|
expect(app.editingId).toBe(null)
|
|
|
|
app.doubleClick(100, 100, { target: 'selection', handle: 'left' })
|
|
|
|
expect(app.editingId).toBe(id)
|
|
})
|
|
})
|
|
|
|
describe('When editing shapes', () => {
|
|
let ids: any
|
|
|
|
beforeEach(() => {
|
|
ids = {
|
|
geo1: app.createShapeId(),
|
|
geo2: app.createShapeId(),
|
|
text1: app.createShapeId(),
|
|
text2: app.createShapeId(),
|
|
}
|
|
|
|
app.createShapes([
|
|
{ id: ids.geo1, type: 'geo', props: { text: 'hello world ' } },
|
|
{ id: ids.geo2, type: 'geo', props: { text: 'hello world ' } },
|
|
{ id: ids.text1, type: 'text', props: { text: 'hello world ' } },
|
|
{ id: ids.text2, type: 'text', props: { text: 'hello world ' } },
|
|
])
|
|
})
|
|
|
|
it('Pointing a shape of a different type selects it and leaves editing', () => {
|
|
expect(app.editingId).toBe(null)
|
|
expect(app.selectedIds.length).toBe(0)
|
|
|
|
// start editing the geo shape
|
|
app.doubleClick(50, 50, { target: 'shape', shape: app.getShapeById(ids.geo1) })
|
|
expect(app.editingId).toBe(ids.geo1)
|
|
expect(app.onlySelectedShape?.id).toBe(ids.geo1)
|
|
// point the text shape
|
|
app.pointerDown(50, 50, { target: 'shape', shape: app.getShapeById(ids.text1) })
|
|
expect(app.editingId).toBe(null)
|
|
expect(app.onlySelectedShape?.id).toBe(ids.text1)
|
|
})
|
|
|
|
it('Pointing a shape of a different type selects it and leaves editing', () => {
|
|
expect(app.editingId).toBe(null)
|
|
expect(app.selectedIds.length).toBe(0)
|
|
|
|
// start editing the geo shape
|
|
app.doubleClick(50, 50, { target: 'shape', shape: app.getShapeById(ids.geo1) })
|
|
expect(app.editingId).toBe(ids.geo1)
|
|
expect(app.onlySelectedShape?.id).toBe(ids.geo1)
|
|
// point the other geo shape
|
|
app.pointerDown(50, 50, { target: 'shape', shape: app.getShapeById(ids.geo2) })
|
|
// that other shape should now be editing and selected!
|
|
expect(app.editingId).toBe(ids.geo2)
|
|
expect(app.onlySelectedShape?.id).toBe(ids.geo2)
|
|
})
|
|
|
|
it('Works with text, too', () => {
|
|
expect(app.editingId).toBe(null)
|
|
expect(app.selectedIds.length).toBe(0)
|
|
|
|
// start editing the geo shape
|
|
app.doubleClick(50, 50, { target: 'shape', shape: app.getShapeById(ids.text1) })
|
|
app.pointerDown(50, 50, { target: 'shape', shape: app.getShapeById(ids.text2) })
|
|
// that other shape should now be editing and selected!
|
|
expect(app.editingId).toBe(ids.text2)
|
|
expect(app.onlySelectedShape?.id).toBe(ids.text2)
|
|
})
|
|
|
|
it.todo('restores selection after changing styles')
|
|
})
|
|
|
|
describe('When in readonly mode', () => {
|
|
beforeEach(() => {
|
|
app.createShapes([
|
|
{
|
|
id: ids.embed1,
|
|
type: 'embed',
|
|
x: 100,
|
|
y: 100,
|
|
props: { opacity: '1', w: 100, h: 100, url: '', doesResize: false },
|
|
},
|
|
])
|
|
app.updateUserDocumentSettings({ isReadOnly: true })
|
|
})
|
|
|
|
it('Begins editing embed when double clicked', () => {
|
|
expect(app.editingId).toBe(null)
|
|
expect(app.selectedIds.length).toBe(0)
|
|
expect(app.isReadOnly).toBe(true)
|
|
|
|
const shape = app.getShapeById(ids.embed1)
|
|
app.doubleClick(100, 100, { target: 'shape', shape })
|
|
expect(app.editingId).toBe(ids.embed1)
|
|
})
|
|
|
|
it('Begins editing embed when pressing Enter on a selected embed', () => {
|
|
expect(app.editingId).toBe(null)
|
|
expect(app.selectedIds.length).toBe(0)
|
|
expect(app.isReadOnly).toBe(true)
|
|
|
|
app.setSelectedIds([ids.embed1])
|
|
expect(app.selectedIds.length).toBe(1)
|
|
|
|
app.keyUp('Enter')
|
|
expect(app.editingId).toBe(ids.embed1)
|
|
})
|
|
})
|