kopia lustrzana https://github.com/Tldraw/Tldraw
Add DSL to make writing shape-layout test cases much easier (#1413)
As part of my highlighter work, I've been writing a few test cases around rendering with different combinations of nested frames and groups. Writing these test cases using `createShapes` is really hard, and reading them is even harder. I wanted to see if there was an easier way for us to define shapes for test cases, and turns out... there is! This diff introduces a JSX-based DSL for defining test cases. It looks something like this: ```tsx // create some shapes const ids = app.createShapesFromJsx([ <TL.geo ref="A" x={100} y={100} w={100} h={100} />, <TL.frame ref="B" x={200} y={200} w={300} h={300}> <TL.geo ref="C" x={200} y={200} w={50} h={50} /> <TL.text ref="D" x={1000} y={1000} text="Hello, world!" align="end" /> </TL.frame>, ]) // refer to shape IDs according to their `ref` app.select(ids.C) ``` It's probably not worth trying to migrate everything possible to this, but i picked a few random tests to convert over to show how it works (and because i wanted this diff to end up red overall) In the future, I'd like to use this with visual regression testing to test rendering/exports on some complex combinations of shapes too. ### Change Type - [x] `tests` — Changes to any testing-related code only (will not publish a new version) ### Release Notes [internal only change]pull/1484/head^2
rodzic
4048064e78
commit
e3dec58499
|
@ -1,5 +1,6 @@
|
|||
import { createCustomShapeId, TLShapeId } from '@tldraw/tlschema'
|
||||
import { TLShapeId } from '@tldraw/tlschema'
|
||||
import { TestApp } from '../../test/TestApp'
|
||||
import { TL } from '../../test/jsx'
|
||||
|
||||
let app: TestApp
|
||||
|
||||
|
@ -7,41 +8,11 @@ beforeEach(() => {
|
|||
app = new TestApp()
|
||||
})
|
||||
|
||||
const ids = {
|
||||
box1: createCustomShapeId('box1'),
|
||||
box2: createCustomShapeId('box2'),
|
||||
box3: createCustomShapeId('box3'),
|
||||
|
||||
box4: createCustomShapeId('box4'),
|
||||
box5: createCustomShapeId('box5'),
|
||||
box6: createCustomShapeId('box6'),
|
||||
}
|
||||
|
||||
describe('arrowBindingsIndex', () => {
|
||||
it('keeps a mapping from bound shapes to the arrows that bind to them', () => {
|
||||
app.createShapes([
|
||||
{
|
||||
type: 'geo',
|
||||
id: ids.box1,
|
||||
x: 0,
|
||||
y: 0,
|
||||
props: {
|
||||
w: 100,
|
||||
h: 100,
|
||||
fill: 'solid',
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'geo',
|
||||
id: ids.box2,
|
||||
x: 200,
|
||||
y: 0,
|
||||
props: {
|
||||
w: 100,
|
||||
h: 100,
|
||||
fill: 'solid',
|
||||
},
|
||||
},
|
||||
const ids = app.createShapesFromJsx([
|
||||
<TL.geo ref="box1" x={0} y={0} w={100} h={100} fill="solid" />,
|
||||
<TL.geo ref="box2" x={200} y={0} w={100} h={100} fill="solid" />,
|
||||
])
|
||||
|
||||
app.setSelectedTool('arrow')
|
||||
|
@ -54,27 +25,9 @@ describe('arrowBindingsIndex', () => {
|
|||
})
|
||||
|
||||
it('works if there are many arrows', () => {
|
||||
app.createShapes([
|
||||
{
|
||||
type: 'geo',
|
||||
id: ids.box1,
|
||||
x: 0,
|
||||
y: 0,
|
||||
props: {
|
||||
w: 100,
|
||||
h: 100,
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'geo',
|
||||
id: ids.box2,
|
||||
x: 200,
|
||||
y: 0,
|
||||
props: {
|
||||
w: 100,
|
||||
h: 100,
|
||||
},
|
||||
},
|
||||
const ids = app.createShapesFromJsx([
|
||||
<TL.geo ref="box1" x={0} y={0} w={100} h={100} />,
|
||||
<TL.geo ref="box2" x={200} y={0} w={100} h={100} />,
|
||||
])
|
||||
|
||||
app.setSelectedTool('arrow')
|
||||
|
@ -134,28 +87,11 @@ describe('arrowBindingsIndex', () => {
|
|||
let arrowCId: TLShapeId
|
||||
let arrowDId: TLShapeId
|
||||
let arrowEId: TLShapeId
|
||||
let ids: Record<string, TLShapeId>
|
||||
beforeEach(() => {
|
||||
app.createShapes([
|
||||
{
|
||||
type: 'geo',
|
||||
id: ids.box1,
|
||||
x: 0,
|
||||
y: 0,
|
||||
props: {
|
||||
w: 100,
|
||||
h: 100,
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'geo',
|
||||
id: ids.box2,
|
||||
x: 200,
|
||||
y: 0,
|
||||
props: {
|
||||
w: 100,
|
||||
h: 100,
|
||||
},
|
||||
},
|
||||
ids = app.createShapesFromJsx([
|
||||
<TL.geo ref="box1" x={0} y={0} w={100} h={100} />,
|
||||
<TL.geo ref="box2" x={200} y={0} w={100} h={100} />,
|
||||
])
|
||||
|
||||
// span both boxes
|
||||
|
@ -227,18 +163,7 @@ describe('arrowBindingsIndex', () => {
|
|||
|
||||
// create a new box
|
||||
|
||||
app.createShapes([
|
||||
{
|
||||
type: 'geo',
|
||||
id: ids.box3,
|
||||
x: 400,
|
||||
y: 0,
|
||||
props: {
|
||||
w: 100,
|
||||
h: 100,
|
||||
},
|
||||
},
|
||||
])
|
||||
const { box3 } = app.createShapesFromJsx(<TL.geo ref="box3" x={400} y={0} w={100} h={100} />)
|
||||
|
||||
// draw from box 2 to box 3
|
||||
|
||||
|
@ -246,7 +171,7 @@ describe('arrowBindingsIndex', () => {
|
|||
app.pointerDown(250, 50).pointerMove(450, 50).pointerUp(450, 50)
|
||||
expect(app.getArrowsBoundTo(ids.box2)).toHaveLength(5)
|
||||
expect(app.getArrowsBoundTo(ids.box1)).toHaveLength(4)
|
||||
expect(app.getArrowsBoundTo(ids.box3)).toHaveLength(1)
|
||||
expect(app.getArrowsBoundTo(box3)).toHaveLength(1)
|
||||
})
|
||||
|
||||
it('works when copy pasting', () => {
|
||||
|
@ -280,18 +205,7 @@ describe('arrowBindingsIndex', () => {
|
|||
|
||||
// create another box
|
||||
|
||||
app.createShapes([
|
||||
{
|
||||
type: 'geo',
|
||||
id: ids.box3,
|
||||
x: 400,
|
||||
y: 0,
|
||||
props: {
|
||||
w: 100,
|
||||
h: 100,
|
||||
},
|
||||
},
|
||||
])
|
||||
const { box3 } = app.createShapesFromJsx(<TL.geo ref="box3" x={400} y={0} w={100} h={100} />)
|
||||
|
||||
// move arrowA from box2 to box3
|
||||
app.updateShapes([
|
||||
|
@ -302,7 +216,7 @@ describe('arrowBindingsIndex', () => {
|
|||
end: {
|
||||
type: 'binding',
|
||||
isExact: false,
|
||||
boundShapeId: ids.box3,
|
||||
boundShapeId: box3,
|
||||
normalizedAnchor: { x: 0.5, y: 0.5 },
|
||||
},
|
||||
},
|
||||
|
@ -311,7 +225,7 @@ describe('arrowBindingsIndex', () => {
|
|||
|
||||
expect(app.getArrowsBoundTo(ids.box2)).toHaveLength(2)
|
||||
expect(app.getArrowsBoundTo(ids.box1)).toHaveLength(3)
|
||||
expect(app.getArrowsBoundTo(ids.box3)).toHaveLength(1)
|
||||
expect(app.getArrowsBoundTo(box3)).toHaveLength(1)
|
||||
})
|
||||
})
|
||||
})
|
|
@ -27,6 +27,7 @@ import {
|
|||
} from '../app/types/event-types'
|
||||
import { RequiredKeys } from '../app/types/misc-types'
|
||||
import { TldrawEditorConfig } from '../config/TldrawEditorConfig'
|
||||
import { shapesFromJsx } from './jsx'
|
||||
|
||||
jest.useFakeTimers()
|
||||
|
||||
|
@ -576,6 +577,12 @@ export class TestApp extends App {
|
|||
return this
|
||||
}
|
||||
|
||||
createShapesFromJsx(shapesJsx: JSX.Element | JSX.Element[]): Record<string, TLShapeId> {
|
||||
const { shapes, ids } = shapesFromJsx(shapesJsx)
|
||||
this.createShapes(shapes)
|
||||
return ids
|
||||
}
|
||||
|
||||
static CreateShapeId(id?: string) {
|
||||
return id ? createCustomShapeId(id) : createShapeId()
|
||||
}
|
||||
|
|
|
@ -1,51 +1,21 @@
|
|||
import { Box2d, PI } from '@tldraw/primitives'
|
||||
import { createCustomShapeId } from '@tldraw/tlschema'
|
||||
import { TLShapeId } from '@tldraw/tlschema'
|
||||
import { TestApp } from '../TestApp'
|
||||
import { TL } from '../jsx'
|
||||
|
||||
let app: TestApp
|
||||
let ids: Record<string, TLShapeId>
|
||||
|
||||
jest.useFakeTimers()
|
||||
|
||||
const ids = {
|
||||
boxA: createCustomShapeId('boxA'),
|
||||
boxB: createCustomShapeId('boxB'),
|
||||
boxC: createCustomShapeId('boxC'),
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
app = new TestApp()
|
||||
app.createShapes([
|
||||
{
|
||||
id: ids.boxA,
|
||||
type: 'geo',
|
||||
x: 0,
|
||||
y: 0,
|
||||
props: {
|
||||
w: 100,
|
||||
h: 100,
|
||||
},
|
||||
},
|
||||
{
|
||||
id: ids.boxB,
|
||||
type: 'geo',
|
||||
x: 100,
|
||||
y: 100,
|
||||
props: {
|
||||
w: 50,
|
||||
h: 50,
|
||||
},
|
||||
},
|
||||
{
|
||||
id: ids.boxC,
|
||||
type: 'geo',
|
||||
x: 400,
|
||||
y: 400,
|
||||
props: {
|
||||
w: 100,
|
||||
h: 100,
|
||||
},
|
||||
},
|
||||
ids = app.createShapesFromJsx([
|
||||
<TL.geo ref="boxA" x={0} y={0} w={100} h={100} />,
|
||||
<TL.geo ref="boxB" x={100} y={100} w={50} h={50} />,
|
||||
<TL.geo ref="boxC" x={400} y={400} w={100} h={100} />,
|
||||
])
|
||||
|
||||
app.selectAll()
|
||||
})
|
||||
|
||||
|
@ -255,40 +225,13 @@ describe('When shapes are parented to other shapes...', () => {
|
|||
app = new TestApp()
|
||||
app.selectAll()
|
||||
app.deleteShapes()
|
||||
app.createShapes([
|
||||
{
|
||||
id: ids.boxA,
|
||||
type: 'geo',
|
||||
x: 100,
|
||||
y: 100,
|
||||
props: {
|
||||
w: 100,
|
||||
h: 100,
|
||||
},
|
||||
},
|
||||
{
|
||||
id: ids.boxB,
|
||||
type: 'geo',
|
||||
parentId: ids.boxA,
|
||||
x: 100,
|
||||
y: 100,
|
||||
props: {
|
||||
geo: 'ellipse',
|
||||
w: 50,
|
||||
h: 50,
|
||||
},
|
||||
},
|
||||
{
|
||||
id: ids.boxC,
|
||||
type: 'geo',
|
||||
x: 400,
|
||||
y: 400,
|
||||
props: {
|
||||
w: 100,
|
||||
h: 100,
|
||||
},
|
||||
},
|
||||
ids = app.createShapesFromJsx([
|
||||
<TL.geo ref="boxA" x={0} y={0} w={100} h={100}>
|
||||
<TL.geo ref="boxB" x={100} y={100} w={50} h={50} />
|
||||
</TL.geo>,
|
||||
<TL.geo ref="boxC" x={400} y={400} w={100} h={100} />,
|
||||
])
|
||||
|
||||
app.selectAll()
|
||||
})
|
||||
|
|
@ -1,67 +1,21 @@
|
|||
import { createCustomShapeId } from '@tldraw/tlschema'
|
||||
import { TLShapeId } from '@tldraw/tlschema'
|
||||
import { TestApp } from '../TestApp'
|
||||
import { TL } from '../jsx'
|
||||
|
||||
let app: TestApp
|
||||
|
||||
export const ids = {
|
||||
A: createCustomShapeId('A'),
|
||||
B: createCustomShapeId('B'),
|
||||
C: createCustomShapeId('C'),
|
||||
D: createCustomShapeId('D'),
|
||||
}
|
||||
let ids: Record<string, TLShapeId>
|
||||
|
||||
beforeEach(() => {
|
||||
app = new TestApp()
|
||||
|
||||
app.createShapes([
|
||||
{
|
||||
id: ids.A,
|
||||
type: 'geo',
|
||||
x: 100,
|
||||
y: 100,
|
||||
props: {
|
||||
w: 100,
|
||||
h: 100,
|
||||
},
|
||||
},
|
||||
{
|
||||
id: ids.B,
|
||||
type: 'frame',
|
||||
x: 200,
|
||||
y: 200,
|
||||
props: {
|
||||
w: 300,
|
||||
h: 300,
|
||||
},
|
||||
},
|
||||
// This shape is a child of the frame.
|
||||
{
|
||||
id: ids.C,
|
||||
type: 'geo',
|
||||
parentId: ids.B,
|
||||
x: 200,
|
||||
y: 200,
|
||||
props: {
|
||||
w: 50,
|
||||
h: 50,
|
||||
},
|
||||
},
|
||||
// this shape is also a child of the frame,
|
||||
// but is outside of its clipping bounds; so it
|
||||
// should never be rendered unless its canUnmount
|
||||
// flag is set to false
|
||||
{
|
||||
id: ids.D,
|
||||
type: 'geo',
|
||||
parentId: ids.B,
|
||||
x: 1000,
|
||||
y: 1000,
|
||||
props: {
|
||||
w: 50,
|
||||
h: 50,
|
||||
},
|
||||
},
|
||||
ids = app.createShapesFromJsx([
|
||||
<TL.geo ref="A" x={100} y={100} w={100} h={100} />,
|
||||
<TL.frame ref="B" x={200} y={200} w={300} h={300}>
|
||||
<TL.geo ref="C" x={200} y={200} w={50} h={50} />
|
||||
{/* this is outside of the frames clipping bounds, so it should never be rendered */}
|
||||
<TL.geo ref="D" x={1000} y={1000} w={50} h={50} />
|
||||
</TL.frame>,
|
||||
])
|
||||
|
||||
app.setScreenBounds({ x: 0, y: 0, w: 1800, h: 900 })
|
||||
})
|
||||
|
|
@ -1,68 +1,25 @@
|
|||
import { PI } from '@tldraw/primitives'
|
||||
import { createCustomShapeId, TLShapeId } from '@tldraw/tlschema'
|
||||
import { TLShapeId } from '@tldraw/tlschema'
|
||||
import { TestApp } from '../TestApp'
|
||||
import { TL } from '../jsx'
|
||||
|
||||
let app: TestApp
|
||||
let ids: Record<string, TLShapeId>
|
||||
|
||||
jest.useFakeTimers()
|
||||
|
||||
const ids = {
|
||||
boxA: createCustomShapeId('boxA'),
|
||||
boxB: createCustomShapeId('boxB'),
|
||||
boxC: createCustomShapeId('boxC'),
|
||||
videoA: createCustomShapeId('videoA'),
|
||||
}
|
||||
|
||||
function createVideoShape(id: TLShapeId) {
|
||||
app.createShapes([
|
||||
{
|
||||
id: id,
|
||||
type: 'video',
|
||||
x: 0,
|
||||
y: 0,
|
||||
props: {
|
||||
w: 160,
|
||||
h: 90,
|
||||
},
|
||||
},
|
||||
])
|
||||
function createVideoShape() {
|
||||
return app.createShapesFromJsx(<TL.video ref="video1" x={0} y={0} w={160} h={90} />).video1
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
app = new TestApp()
|
||||
app.selectAll()
|
||||
app.deleteShapes()
|
||||
app.createShapes([
|
||||
{
|
||||
id: ids.boxA,
|
||||
type: 'geo',
|
||||
x: 0,
|
||||
y: 0,
|
||||
props: {
|
||||
w: 100,
|
||||
h: 100,
|
||||
},
|
||||
},
|
||||
{
|
||||
id: ids.boxB,
|
||||
type: 'geo',
|
||||
x: 100,
|
||||
y: 100,
|
||||
props: {
|
||||
w: 50,
|
||||
h: 50,
|
||||
},
|
||||
},
|
||||
{
|
||||
id: ids.boxC,
|
||||
type: 'geo',
|
||||
x: 400,
|
||||
y: 400,
|
||||
props: {
|
||||
w: 100,
|
||||
h: 100,
|
||||
},
|
||||
},
|
||||
ids = app.createShapesFromJsx([
|
||||
<TL.geo ref="boxA" x={0} y={0} w={100} h={100} />,
|
||||
<TL.geo ref="boxB" x={100} y={100} w={50} h={50} />,
|
||||
<TL.geo ref="boxC" x={400} y={400} w={100} h={100} />,
|
||||
])
|
||||
app.selectAll()
|
||||
})
|
||||
|
@ -92,7 +49,7 @@ describe('when multiple shapes are selected', () => {
|
|||
})
|
||||
|
||||
it('stretches horizontally and preserves aspect ratio', () => {
|
||||
createVideoShape(ids.videoA)
|
||||
const videoA = createVideoShape()
|
||||
app.selectAll()
|
||||
expect(app.selectedShapes.length).toBe(4)
|
||||
app.stretchShapes('horizontal')
|
||||
|
@ -102,7 +59,7 @@ describe('when multiple shapes are selected', () => {
|
|||
{ id: ids.boxA, x: 0, y: 0, props: { w: 500 } },
|
||||
{ id: ids.boxB, x: 0, y: 100, props: { w: 500 } },
|
||||
{ id: ids.boxC, x: 0, y: 400, props: { w: 500 } },
|
||||
{ id: ids.videoA, x: 0, y: -95.625, props: { w: 500, h: newHeight } }
|
||||
{ id: videoA, x: 0, y: -95.625, props: { w: 500, h: newHeight } }
|
||||
)
|
||||
})
|
||||
|
||||
|
@ -118,7 +75,7 @@ describe('when multiple shapes are selected', () => {
|
|||
})
|
||||
|
||||
it('stretches vertically and preserves aspect ratio', () => {
|
||||
createVideoShape(ids.videoA)
|
||||
const videoA = createVideoShape()
|
||||
app.selectAll()
|
||||
expect(app.selectedShapes.length).toBe(4)
|
||||
app.stretchShapes('vertical')
|
||||
|
@ -128,7 +85,7 @@ describe('when multiple shapes are selected', () => {
|
|||
{ id: ids.boxA, x: 0, y: 0, props: { h: 500 } },
|
||||
{ id: ids.boxB, x: 100, y: 0, props: { h: 500 } },
|
||||
{ id: ids.boxC, x: 400, y: 0, props: { h: 500 } },
|
||||
{ id: ids.videoA, x: -364.44444444444446, y: 0, props: { w: newWidth, h: 500 } }
|
||||
{ id: videoA, x: -364.44444444444446, y: 0, props: { w: newWidth, h: 500 } }
|
||||
)
|
||||
})
|
||||
|
||||
|
@ -174,39 +131,11 @@ describe('When shapes are the child of a rotated shape.', () => {
|
|||
app = new TestApp()
|
||||
app.selectAll()
|
||||
app.deleteShapes()
|
||||
app.createShapes([
|
||||
{
|
||||
id: ids.boxA,
|
||||
type: 'geo',
|
||||
x: 0,
|
||||
y: 0,
|
||||
props: {
|
||||
w: 100,
|
||||
h: 100,
|
||||
},
|
||||
rotation: PI,
|
||||
},
|
||||
{
|
||||
id: ids.boxB,
|
||||
type: 'geo',
|
||||
parentId: ids.boxA,
|
||||
x: 100,
|
||||
y: 100,
|
||||
props: {
|
||||
w: 50,
|
||||
h: 50,
|
||||
},
|
||||
},
|
||||
{
|
||||
id: ids.boxC,
|
||||
type: 'geo',
|
||||
x: 200,
|
||||
y: 200,
|
||||
props: {
|
||||
w: 100,
|
||||
h: 100,
|
||||
},
|
||||
},
|
||||
ids = app.createShapesFromJsx([
|
||||
<TL.geo ref="boxA" x={0} y={0} w={100} h={100} rotation={PI}>
|
||||
<TL.geo ref="boxB" x={100} y={100} w={50} h={50} />
|
||||
</TL.geo>,
|
||||
<TL.geo ref="boxC" x={200} y={200} w={100} h={100} />,
|
||||
])
|
||||
app.selectAll()
|
||||
})
|
|
@ -0,0 +1,132 @@
|
|||
import { getIndexAbove } from '@tldraw/indices'
|
||||
import {
|
||||
TLDefaultShape,
|
||||
TLShapeId,
|
||||
TLShapePartial,
|
||||
createCustomShapeId,
|
||||
createShapeId,
|
||||
} from '@tldraw/tlschema'
|
||||
import { assert, assertExists, omitFromStackTrace } from '@tldraw/utils'
|
||||
|
||||
const shapeTypeSymbol = Symbol('shapeJsx')
|
||||
|
||||
const createElement = (tag: string) => {
|
||||
const component = () => {
|
||||
throw new Error(`Cannot render test tag ${tag}`)
|
||||
}
|
||||
;(component as any)[shapeTypeSymbol] = tag
|
||||
return component
|
||||
}
|
||||
|
||||
type CommonProps = {
|
||||
x: number
|
||||
y: number
|
||||
id?: TLShapeId
|
||||
rotation?: number
|
||||
isLocked?: number
|
||||
ref?: string
|
||||
children?: JSX.Element | JSX.Element[]
|
||||
}
|
||||
|
||||
type ShapeByType<Type extends TLDefaultShape['type']> = Extract<TLDefaultShape, { type: Type }>
|
||||
type PropsForShape<Type extends string> = Type extends TLDefaultShape['type']
|
||||
? CommonProps & Partial<ShapeByType<Type>['props']>
|
||||
: CommonProps & Record<string, unknown>
|
||||
|
||||
/**
|
||||
* TL - jsx helpers for creating tldraw shapes in test cases
|
||||
*/
|
||||
export const TL = new Proxy(
|
||||
{},
|
||||
{
|
||||
get(target, key) {
|
||||
return createElement(key as string)
|
||||
},
|
||||
}
|
||||
) as { [K in TLDefaultShape['type']]: (props: PropsForShape<K>) => null }
|
||||
|
||||
export function shapesFromJsx(shapes: JSX.Element | Array<JSX.Element>) {
|
||||
const ids = {} as Record<string, TLShapeId>
|
||||
const shapesArray: Array<TLShapePartial> = []
|
||||
|
||||
function addChildren(children: JSX.Element | Array<JSX.Element>, parentId?: TLShapeId) {
|
||||
let nextIndex = 'a0'
|
||||
|
||||
for (const el of Array.isArray(children) ? children : [children]) {
|
||||
const shapeType = (el.type as any)[shapeTypeSymbol] as string
|
||||
if (!shapeType) {
|
||||
throw new Error(
|
||||
`Cannot use ${el.type} as a shape. Only TL.* tags are allowed in shape jsx.`
|
||||
)
|
||||
}
|
||||
|
||||
let id
|
||||
const ref = (el as any).ref as string | undefined
|
||||
if (ref) {
|
||||
assert(!ids[ref], `Duplicate shape ref: ${ref}`)
|
||||
assert(!el.props.id, `Cannot use both ref and id on shape: ${ref}`)
|
||||
id = createCustomShapeId(ref)
|
||||
ids[ref] = id
|
||||
} else if (el.props.id) {
|
||||
id = el.props.id
|
||||
} else {
|
||||
id = createShapeId()
|
||||
}
|
||||
|
||||
const x: number = assertExists(el.props.x, `Shape ${id} is missing x prop`)
|
||||
const y: number = assertExists(el.props.y, `Shape ${id} is missing y prop`)
|
||||
|
||||
const shapePartial = {
|
||||
id,
|
||||
type: shapeType,
|
||||
x,
|
||||
y,
|
||||
index: nextIndex,
|
||||
props: {},
|
||||
} as TLShapePartial
|
||||
|
||||
nextIndex = getIndexAbove(nextIndex)
|
||||
|
||||
if (parentId) {
|
||||
shapePartial.parentId = parentId
|
||||
}
|
||||
|
||||
for (const [key, value] of Object.entries(el.props)) {
|
||||
if (key === 'x' || key === 'y' || key === 'ref' || key === 'id' || key === 'children') {
|
||||
continue
|
||||
}
|
||||
if (key === 'rotation' || key === 'isLocked') {
|
||||
shapePartial[key] = value as any
|
||||
continue
|
||||
}
|
||||
;(shapePartial.props as Record<string, unknown>)[key] = value
|
||||
}
|
||||
|
||||
shapesArray.push(shapePartial)
|
||||
|
||||
if (el.props.children) {
|
||||
addChildren(el.props.children, id)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
addChildren(shapes)
|
||||
|
||||
return {
|
||||
ids: new Proxy(ids, {
|
||||
get: omitFromStackTrace((target, key) => {
|
||||
if (!(key in target)) {
|
||||
throw new Error(
|
||||
`Cannot access ID '${String(
|
||||
key
|
||||
)}'. No ref with that name was specified.\nAvailable refs: ${Object.keys(ids).join(
|
||||
', '
|
||||
)}`
|
||||
)
|
||||
}
|
||||
return target[key as string]
|
||||
}),
|
||||
}),
|
||||
shapes: shapesArray,
|
||||
}
|
||||
}
|
|
@ -1,5 +1,6 @@
|
|||
import { TLSelectTool } from '../../app/statechart/TLSelectTool/TLSelectTool'
|
||||
import { TestApp } from '../TestApp'
|
||||
import { TL } from '../jsx'
|
||||
|
||||
let app: TestApp
|
||||
|
||||
|
@ -68,95 +69,74 @@ describe(TLSelectTool, () => {
|
|||
|
||||
describe('When pointing a shape behind the current selection', () => {
|
||||
it('Does not select on pointer down, but does select on pointer up', () => {
|
||||
const idA = app.createShapeId('a')
|
||||
const idB = app.createShapeId('b')
|
||||
const idC = app.createShapeId('c')
|
||||
app.selectNone()
|
||||
app.createShapes([
|
||||
{ id: idA, type: 'geo', x: 0, y: 0, props: { w: 100, h: 100 } },
|
||||
{ id: idB, type: 'geo', x: 50, y: 50, props: { w: 100, h: 100 } },
|
||||
{ id: idC, type: 'geo', x: 100, y: 100, props: { w: 100, h: 100 } },
|
||||
const ids = app.createShapesFromJsx([
|
||||
<TL.geo ref="A" x={0} y={0} w={100} h={100} />,
|
||||
<TL.geo ref="B" x={50} y={50} w={100} h={100} />,
|
||||
<TL.geo ref="C" x={100} y={100} w={100} h={100} />,
|
||||
])
|
||||
app.select(idA, idC)
|
||||
app.select(ids.A, ids.C)
|
||||
// don't select it yet! It's behind the current selection
|
||||
app.pointerDown(100, 100, idB)
|
||||
expect(app.selectedIds).toMatchObject([idA, idC])
|
||||
app.pointerUp(100, 100, idB)
|
||||
expect(app.selectedIds).toMatchObject([idB])
|
||||
app.pointerDown(100, 100, ids.B)
|
||||
expect(app.selectedIds).toMatchObject([ids.A, ids.C])
|
||||
app.pointerUp(100, 100, ids.B)
|
||||
expect(app.selectedIds).toMatchObject([ids.B])
|
||||
})
|
||||
|
||||
it('Selects on shift+pointer up', () => {
|
||||
const idA = app.createShapeId('a')
|
||||
const idB = app.createShapeId('b')
|
||||
const idC = app.createShapeId('c')
|
||||
app.selectNone()
|
||||
app.createShapes([
|
||||
{ id: idA, type: 'geo', x: 0, y: 0, props: { w: 100, h: 100 } },
|
||||
{ id: idB, type: 'geo', x: 50, y: 50, props: { w: 100, h: 100 } },
|
||||
{ id: idC, type: 'geo', x: 100, y: 100, props: { w: 100, h: 100 } },
|
||||
const ids = app.createShapesFromJsx([
|
||||
<TL.geo ref="A" x={0} y={0} w={100} h={100} />,
|
||||
<TL.geo ref="B" x={50} y={50} w={100} h={100} />,
|
||||
<TL.geo ref="C" x={100} y={100} w={100} h={100} />,
|
||||
])
|
||||
app.select(idA, idC)
|
||||
app.select(ids.A, ids.C)
|
||||
// don't select it yet! It's behind the current selection
|
||||
app.pointerDown(100, 100, idB, { shiftKey: true })
|
||||
expect(app.selectedIds).toMatchObject([idA, idC])
|
||||
app.pointerUp(100, 100, idB, { shiftKey: true })
|
||||
expect(app.selectedIds).toMatchObject([idA, idC, idB])
|
||||
app.pointerDown(100, 100, ids.B, { shiftKey: true })
|
||||
expect(app.selectedIds).toMatchObject([ids.A, ids.C])
|
||||
app.pointerUp(100, 100, ids.B, { shiftKey: true })
|
||||
expect(app.selectedIds).toMatchObject([ids.A, ids.C, ids.B])
|
||||
|
||||
// and deselect
|
||||
app.pointerDown(100, 100, idB, { shiftKey: true })
|
||||
expect(app.selectedIds).toMatchObject([idA, idC, idB])
|
||||
app.pointerUp(100, 100, idB, { shiftKey: true })
|
||||
expect(app.selectedIds).toMatchObject([idA, idC])
|
||||
app.pointerDown(100, 100, ids.B, { shiftKey: true })
|
||||
expect(app.selectedIds).toMatchObject([ids.A, ids.C, ids.B])
|
||||
app.pointerUp(100, 100, ids.B, { shiftKey: true })
|
||||
expect(app.selectedIds).toMatchObject([ids.A, ids.C])
|
||||
})
|
||||
|
||||
it('Moves on pointer move, does not select on pointer up', () => {
|
||||
const idA = app.createShapeId('a')
|
||||
const idB = app.createShapeId('b')
|
||||
const idC = app.createShapeId('c')
|
||||
app.selectNone()
|
||||
app.createShapes([
|
||||
{ id: idA, type: 'geo', x: 0, y: 0, props: { w: 100, h: 100 } },
|
||||
{ id: idB, type: 'geo', x: 50, y: 50, props: { w: 100, h: 100 } },
|
||||
{ id: idC, type: 'geo', x: 100, y: 100, props: { w: 100, h: 100 } },
|
||||
const ids = app.createShapesFromJsx([
|
||||
<TL.geo ref="A" x={0} y={0} w={100} h={100} />,
|
||||
<TL.geo ref="B" x={50} y={50} w={100} h={100} />,
|
||||
<TL.geo ref="C" x={100} y={100} w={100} h={100} />,
|
||||
])
|
||||
app.select(idA, idC) // don't select it yet! It's behind the current selection
|
||||
app.pointerDown(100, 100, idB)
|
||||
app.select(ids.A, ids.C) // don't select it yet! It's behind the current selection
|
||||
app.pointerDown(100, 100, ids.B)
|
||||
app.pointerMove(150, 150)
|
||||
app.pointerMove(151, 151)
|
||||
app.pointerMove(100, 100)
|
||||
expect(app.selectedIds).toMatchObject([idA, idC])
|
||||
app.pointerUp(100, 100, idB)
|
||||
expect(app.selectedIds).toMatchObject([idA, idC]) // no change! we've moved
|
||||
expect(app.selectedIds).toMatchObject([ids.A, ids.C])
|
||||
app.pointerUp(100, 100, ids.B)
|
||||
expect(app.selectedIds).toMatchObject([ids.A, ids.C]) // no change! we've moved
|
||||
})
|
||||
})
|
||||
|
||||
describe('When brushing arrows', () => {
|
||||
it('Brushes a straight arrow', () => {
|
||||
const ids = { arrow1: app.createShapeId('arrow1') }
|
||||
app
|
||||
const ids = app
|
||||
.selectAll()
|
||||
.deleteShapes()
|
||||
.setCamera(0, 0, 1)
|
||||
.createShapes([
|
||||
{
|
||||
id: ids.arrow1,
|
||||
type: 'arrow',
|
||||
x: 0,
|
||||
y: 0,
|
||||
props: {
|
||||
start: {
|
||||
type: 'point',
|
||||
x: 0,
|
||||
y: 0,
|
||||
},
|
||||
bend: 0,
|
||||
end: {
|
||||
type: 'point',
|
||||
x: 100,
|
||||
y: 100,
|
||||
},
|
||||
},
|
||||
},
|
||||
.createShapesFromJsx([
|
||||
<TL.arrow
|
||||
ref="arrow1"
|
||||
x={0}
|
||||
y={0}
|
||||
start={{ type: 'point', x: 0, y: 0 }}
|
||||
end={{ type: 'point', x: 100, y: 100 }}
|
||||
bend={0}
|
||||
/>,
|
||||
])
|
||||
app.setSelectedTool('select')
|
||||
app.pointerDown(55, 45)
|
||||
|
@ -166,31 +146,19 @@ describe('When brushing arrows', () => {
|
|||
})
|
||||
|
||||
it('Brushes within the curve of a curved arrow without selecting the arrow', () => {
|
||||
const ids = { arrow1: app.createShapeId('arrow1') }
|
||||
app
|
||||
.selectAll()
|
||||
.deleteShapes()
|
||||
.setCamera(0, 0, 1)
|
||||
.createShapes([
|
||||
{
|
||||
id: ids.arrow1,
|
||||
type: 'arrow',
|
||||
x: 0,
|
||||
y: 0,
|
||||
props: {
|
||||
start: {
|
||||
type: 'point',
|
||||
x: 0,
|
||||
y: 0,
|
||||
},
|
||||
bend: 40,
|
||||
end: {
|
||||
type: 'point',
|
||||
x: 100,
|
||||
y: 100,
|
||||
},
|
||||
},
|
||||
},
|
||||
.createShapesFromJsx([
|
||||
<TL.arrow
|
||||
ref="arrow1"
|
||||
x={0}
|
||||
y={0}
|
||||
start={{ type: 'point', x: 0, y: 0 }}
|
||||
end={{ type: 'point', x: 100, y: 100 }}
|
||||
bend={40}
|
||||
/>,
|
||||
])
|
||||
app.setSelectedTool('select')
|
||||
app.pointerDown(55, 45)
|
Ładowanie…
Reference in New Issue