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
alex 2023-05-30 14:27:26 +01:00 zatwierdzone przez GitHub
rodzic 4048064e78
commit e3dec58499
Nie znaleziono w bazie danych klucza dla tego podpisu
ID klucza GPG: 4AEE18F83AFDEB23
7 zmienionych plików z 250 dodań i 403 usunięć

Wyświetl plik

@ -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)
})
})
})

Wyświetl plik

@ -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()
}

Wyświetl plik

@ -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()
})

Wyświetl plik

@ -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 })
})

Wyświetl plik

@ -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()
})

Wyświetl plik

@ -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,
}
}

Wyświetl plik

@ -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)