Tldraw/packages/tldraw/src/test/TldrawEditor.test.tsx

349 wiersze
8.1 KiB
TypeScript

import { act, render, screen } from '@testing-library/react'
import {
BaseBoxShapeTool,
BaseBoxShapeUtil,
Editor,
HTMLContainer,
TLBaseShape,
TldrawEditor,
createShapeId,
createTLStore,
noop,
} from '@tldraw/editor'
import { defaultTools } from '../lib/defaultTools'
import { GeoShapeUtil } from '../lib/shapes/geo/GeoShapeUtil'
import { renderTldrawComponent } from './testutils/renderTldrawComponent'
function checkAllShapes(editor: Editor, shapes: string[]) {
expect(Object.keys(editor!.shapeUtils)).toStrictEqual(shapes)
}
describe('<TldrawEditor />', () => {
it('Renders without crashing', async () => {
await renderTldrawComponent(
<TldrawEditor tools={defaultTools} autoFocus initialState="select" />,
{ waitForPatterns: false }
)
await screen.findByTestId('canvas')
})
it('Creates its own store with core shapes', async () => {
let editor: Editor
await renderTldrawComponent(
<TldrawEditor
onMount={(e) => {
editor = e
}}
initialState="select"
tools={defaultTools}
autoFocus
/>,
{ waitForPatterns: false }
)
checkAllShapes(editor!, ['group'])
})
it('Can be created with default shapes', async () => {
let editor: Editor
await renderTldrawComponent(
<TldrawEditor
shapeUtils={[]}
tools={defaultTools}
initialState="select"
onMount={(e) => {
editor = e
}}
autoFocus
/>,
{ waitForPatterns: false }
)
expect(editor!).toBeTruthy()
checkAllShapes(editor!, ['group'])
})
it('Renders with an external store', async () => {
const store = createTLStore({ shapeUtils: [] })
await renderTldrawComponent(
<TldrawEditor
store={store}
tools={defaultTools}
initialState="select"
onMount={(editor) => {
expect(editor.store).toBe(store)
}}
autoFocus
/>,
{ waitForPatterns: false }
)
})
it('throws if the store has different shapes to the ones passed in', async () => {
const spy = jest.spyOn(console, 'error').mockImplementation(noop)
// expect(() =>
// render(
// <TldrawEditor
// shapeUtils={[GroupShapeUtil]}
// store={createTLStore({ shapeUtils: [] })}
// autoFocus
// components={{
// ErrorFallback: ({ error }) => {
// throw error
// },
// }}
// >
// <div data-testid="canvas-1" />
// </TldrawEditor>
// )
// ).toThrowErrorMatchingInlineSnapshot(
// `"Editor and store have different shapes: \\"draw\\" was passed into the editor but not the schema"`
// )
// expect(() =>
// render(
// <TldrawEditor
// store={createTLStore({ shapeUtils: [GroupShapeUtil] })}
// autoFocus
// components={{
// ErrorFallback: ({ error }) => {
// throw error
// },
// }}
// >
// <div data-testid="canvas-1" />
// </TldrawEditor>
// )
// ).toThrowErrorMatchingInlineSnapshot(
// `"Editor and store have different shapes: \\"draw\\" is present in the store schema but not provided to the editor"`
// )
spy.mockRestore()
})
it('Accepts fresh versions of store and calls `onMount` for each one', async () => {
const initialStore = createTLStore({ shapeUtils: [] })
const onMount = jest.fn()
const rendered = render(
<TldrawEditor
initialState="select"
tools={defaultTools}
store={initialStore}
onMount={onMount}
autoFocus
/>
)
const initialEditor = onMount.mock.lastCall[0]
jest.spyOn(initialEditor, 'dispose')
expect(initialEditor.store).toBe(initialStore)
// re-render with the same store:
rendered.rerender(
<TldrawEditor
tools={defaultTools}
initialState="select"
store={initialStore}
onMount={onMount}
autoFocus
/>
)
// not called again:
expect(onMount).toHaveBeenCalledTimes(1)
// re-render with a new store:
const newStore = createTLStore({ shapeUtils: [] })
rendered.rerender(
<TldrawEditor
tools={defaultTools}
initialState="select"
store={newStore}
onMount={onMount}
autoFocus
/>
)
expect(initialEditor.dispose).toHaveBeenCalledTimes(1)
expect(onMount).toHaveBeenCalledTimes(2)
expect(onMount.mock.lastCall[0].store).toBe(newStore)
})
it('Renders the canvas and shapes', async () => {
let editor = {} as Editor
await renderTldrawComponent(
<TldrawEditor
shapeUtils={[GeoShapeUtil]}
initialState="select"
tools={defaultTools}
autoFocus
onMount={(editorApp) => {
editor = editorApp
}}
/>,
{ waitForPatterns: false }
)
expect(editor).toBeTruthy()
await act(async () => {
editor.updateInstanceState(
{ screenBounds: { x: 0, y: 0, w: 1080, h: 720 } },
{ ephemeral: true, squashing: true }
)
})
const id = createShapeId()
await act(async () => {
editor.createShapes([
{
id,
type: 'geo',
props: { w: 100, h: 100 },
},
])
})
// Does the shape exist?
expect(editor.getShape(id)).toMatchObject({
id,
type: 'geo',
x: 0,
y: 0,
opacity: 1,
props: { geo: 'rectangle', w: 100, h: 100 },
})
// Is the shape's component rendering?
expect(document.querySelectorAll('.tl-shape')).toHaveLength(1)
// though indicator should be display none
expect(document.querySelectorAll('.tl-shape-indicator')).toHaveLength(1)
// Select the shape
await act(async () => editor.select(id))
expect(editor.getSelectedShapeIds().length).toBe(1)
// though indicator it should be visible
expect(document.querySelectorAll('.tl-shape-indicator')).toHaveLength(1)
// Select the eraser tool...
await act(async () => editor.setCurrentTool('eraser'))
// Is the editor's current tool correct?
expect(editor.getCurrentToolId()).toBe('eraser')
})
})
describe('Custom shapes', () => {
type CardShape = TLBaseShape<
'card',
{
w: number
h: number
}
>
class CardUtil extends BaseBoxShapeUtil<CardShape> {
static override type = 'card' as const
override isAspectRatioLocked = (_shape: CardShape) => false
override canResize = (_shape: CardShape) => true
override canBind = (_shape: CardShape) => true
override getDefaultProps(): CardShape['props'] {
return {
w: 300,
h: 300,
}
}
component(shape: CardShape) {
return (
<HTMLContainer
id={shape.id}
data-testid="card-shape"
style={{
border: '1px solid black',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
pointerEvents: 'all',
}}
>
{shape.props.w.toFixed()}x{shape.props.h.toFixed()}
</HTMLContainer>
)
}
indicator(shape: CardShape) {
return <rect data-testid="card-indicator" width={shape.props.w} height={shape.props.h} />
}
}
class CardTool extends BaseBoxShapeTool {
static override id = 'card'
static override initial = 'idle'
override shapeType = 'card'
}
const tools = [CardTool]
const shapeUtils = [CardUtil]
it('Uses custom shapes', async () => {
let editor = {} as Editor
await renderTldrawComponent(
<TldrawEditor
shapeUtils={shapeUtils}
tools={[...defaultTools, ...tools]}
autoFocus
initialState="select"
onMount={(editorApp) => {
editor = editorApp
}}
/>,
{ waitForPatterns: false }
)
expect(editor).toBeTruthy()
await act(async () => {
editor.updateInstanceState(
{ screenBounds: { x: 0, y: 0, w: 1080, h: 720 } },
{ ephemeral: true, squashing: true }
)
})
expect(editor.shapeUtils.card).toBeTruthy()
checkAllShapes(editor, ['group', 'card'])
const id = createShapeId()
await act(async () => {
editor.createShapes([
{
id,
type: 'card',
props: { w: 100, h: 100 },
},
])
})
// Does the shape exist?
expect(editor.getShape(id)).toMatchObject({
id,
type: 'card',
x: 0,
y: 0,
opacity: 1,
props: { w: 100, h: 100 },
})
// Is the shape's component rendering?
expect(await screen.findByTestId('card-shape')).toBeTruthy()
// Select the shape
await act(async () => editor.select(id))
// Is the shape's component rendering?
expect(await screen.findByTestId('card-indicator')).toBeTruthy()
// Select the tool...
await act(async () => editor.setCurrentTool('card'))
// Is the editor's current tool correct?
expect(editor.getCurrentToolId()).toBe('card')
})
})