Camera Constraints Tests (#3844)
This PR adds some unit tests for the camera constraints API I took educated guesses at the intended panning and zoom behaviour for the editor's constraints. But I couldn't work out what the intended behaviour was for a couple of the tests, so I've left these ones for now: ```javascript describe('Allows mixed values for x and y', () => { it.todo('Allows different values to be set for x and y axes') }) ``` ```javascript describe('Contain behavior', () => { it.todo( 'Locks axis until the bounds are bigger than the padded viewport, then allows "inside" panning' ) }) ``` I also edited some earlier tests so they now use chaining, to be consistent with the other tests and hopefully easier to read. ### Change Type <!-- ❗ Please select a 'Scope' label ❗️ --> - [ ] `sdk` — Changes the tldraw SDK - [ ] `dotcom` — Changes the tldraw.com web app - [ ] `docs` — Changes to the documentation, examples, or templates. - [ ] `vs code` — Changes to the vscode plugin - [x] `internal` — Does not affect user-facing stuff <!-- ❗ Please select a 'Type' label ❗️ --> - [ ] `bugfix` — Bug fix - [ ] `feature` — New feature - [ ] `improvement` — Improving existing features - [ ] `chore` — Updating dependencies, other boring stuff - [ ] `galaxy brain` — Architectural changes - [x] `tests` — Changes to any test code - [ ] `tools` — Changes to infrastructure, CI, internal scripts, debugging tools, etc. - [ ] `dunno` — I don't know ### Test Plan 1. Add a step-by-step description of how to test your PR here. 2. - [ ] Unit Tests - [ ] End to end tests ### Release Notes - Adds tests for the camera constraints api
This commit is contained in:
parent
aba77fd089
commit
fc302ec4a5
1 changed files with 236 additions and 64 deletions
|
@ -1,4 +1,4 @@
|
||||||
import { Box, DEFAULT_CAMERA_OPTIONS, Vec } from '@tldraw/editor'
|
import { Box, DEFAULT_CAMERA_OPTIONS, Vec, createShapeId } from '@tldraw/editor'
|
||||||
import { TestEditor } from '../TestEditor'
|
import { TestEditor } from '../TestEditor'
|
||||||
|
|
||||||
let editor: TestEditor
|
let editor: TestEditor
|
||||||
|
@ -28,84 +28,100 @@ const pinchEvent = {
|
||||||
ctrlKey: false,
|
ctrlKey: false,
|
||||||
} as const
|
} as const
|
||||||
|
|
||||||
|
const keyBoardEvent = {
|
||||||
|
type: 'keyboard',
|
||||||
|
name: 'key_down',
|
||||||
|
key: ' ',
|
||||||
|
code: 'Space',
|
||||||
|
shiftKey: false,
|
||||||
|
altKey: false,
|
||||||
|
ctrlKey: false,
|
||||||
|
} as const
|
||||||
|
|
||||||
describe('With default options', () => {
|
describe('With default options', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
editor.setCameraOptions({ ...DEFAULT_CAMERA_OPTIONS })
|
editor.setCameraOptions({ ...DEFAULT_CAMERA_OPTIONS })
|
||||||
})
|
})
|
||||||
it('pans', () => {
|
it('pans', () => {
|
||||||
expect(editor.getCamera()).toMatchObject({ x: 0, y: 0, z: 1 })
|
expect(editor.getCamera()).toMatchObject({ x: 0, y: 0, z: 1 })
|
||||||
editor.dispatch({
|
editor
|
||||||
...pinchEvent,
|
.dispatch({
|
||||||
name: 'pinch_start',
|
...pinchEvent,
|
||||||
})
|
name: 'pinch_start',
|
||||||
editor.forceTick()
|
})
|
||||||
editor.dispatch({
|
.forceTick()
|
||||||
...pinchEvent,
|
editor
|
||||||
name: 'pinch',
|
.dispatch({
|
||||||
delta: new Vec(100, -10),
|
...pinchEvent,
|
||||||
})
|
name: 'pinch',
|
||||||
editor.forceTick()
|
delta: new Vec(100, -10),
|
||||||
editor.dispatch({
|
})
|
||||||
...pinchEvent,
|
.forceTick()
|
||||||
name: 'pinch_end',
|
editor
|
||||||
})
|
.dispatch({
|
||||||
editor.forceTick()
|
...pinchEvent,
|
||||||
|
name: 'pinch_end',
|
||||||
|
})
|
||||||
|
.forceTick()
|
||||||
expect(editor.getCamera()).toMatchObject({ x: 100, y: -10, z: 1 })
|
expect(editor.getCamera()).toMatchObject({ x: 100, y: -10, z: 1 })
|
||||||
})
|
})
|
||||||
it('pans with wheel', () => {
|
it('pans with wheel', () => {
|
||||||
expect(editor.getCamera()).toMatchObject({ x: 0, y: 0, z: 1 })
|
expect(editor.getCamera()).toMatchObject({ x: 0, y: 0, z: 1 })
|
||||||
editor.dispatch({ ...wheelEvent, delta: new Vec(5, 10) })
|
editor.dispatch({ ...wheelEvent, delta: new Vec(5, 10) }).forceTick()
|
||||||
editor.forceTick()
|
|
||||||
expect(editor.getCamera()).toMatchObject({ x: 5, y: 10, z: 1 })
|
expect(editor.getCamera()).toMatchObject({ x: 5, y: 10, z: 1 })
|
||||||
})
|
})
|
||||||
it('zooms with wheel', () => {
|
it('zooms with wheel', () => {
|
||||||
expect(editor.getCamera()).toMatchObject({ x: 0, y: 0, z: 1 })
|
expect(editor.getCamera()).toMatchObject({ x: 0, y: 0, z: 1 })
|
||||||
// zoom in 10%
|
// zoom in 10%
|
||||||
editor.dispatch({ ...wheelEvent, delta: new Vec(0, 0, -0.1), ctrlKey: true })
|
editor.dispatch({ ...wheelEvent, delta: new Vec(0, 0, -0.1), ctrlKey: true }).forceTick()
|
||||||
editor.forceTick()
|
|
||||||
expect(editor.getCamera()).toMatchObject({ x: 0, y: 0, z: 0.9 })
|
expect(editor.getCamera()).toMatchObject({ x: 0, y: 0, z: 0.9 })
|
||||||
// zoom out 10%
|
// zoom out 10%
|
||||||
editor.dispatch({ ...wheelEvent, delta: new Vec(0, 0, 0.1), ctrlKey: true })
|
editor.dispatch({ ...wheelEvent, delta: new Vec(0, 0, 0.1), ctrlKey: true }).forceTick()
|
||||||
editor.forceTick()
|
|
||||||
expect(editor.getCamera()).toMatchObject({ x: 0, y: 0, z: 0.99 })
|
expect(editor.getCamera()).toMatchObject({ x: 0, y: 0, z: 0.99 })
|
||||||
})
|
})
|
||||||
it('pinch zooms', () => {
|
it('pinch zooms', () => {
|
||||||
expect(editor.getCamera()).toMatchObject({ x: 0, y: 0, z: 1 })
|
expect(editor.getCamera()).toMatchObject({ x: 0, y: 0, z: 1 })
|
||||||
// zoom in
|
// zoom in
|
||||||
editor.dispatch({
|
editor
|
||||||
...pinchEvent,
|
.dispatch({
|
||||||
name: 'pinch_start',
|
...pinchEvent,
|
||||||
})
|
name: 'pinch_start',
|
||||||
editor.forceTick()
|
})
|
||||||
editor.dispatch({
|
.forceTick()
|
||||||
...pinchEvent,
|
editor
|
||||||
name: 'pinch',
|
.dispatch({
|
||||||
point: new Vec(0, 0, 0.5),
|
...pinchEvent,
|
||||||
})
|
name: 'pinch',
|
||||||
editor.forceTick()
|
point: new Vec(0, 0, 0.5),
|
||||||
editor.dispatch({
|
})
|
||||||
...pinchEvent,
|
.forceTick()
|
||||||
name: 'pinch_end',
|
editor
|
||||||
})
|
.dispatch({
|
||||||
editor.forceTick()
|
...pinchEvent,
|
||||||
|
name: 'pinch_end',
|
||||||
|
})
|
||||||
|
.forceTick()
|
||||||
expect(editor.getCamera()).toMatchObject({ x: 0, y: 0, z: 0.5 })
|
expect(editor.getCamera()).toMatchObject({ x: 0, y: 0, z: 0.5 })
|
||||||
// zoom out
|
// zoom out
|
||||||
editor.dispatch({
|
editor
|
||||||
...pinchEvent,
|
.dispatch({
|
||||||
name: 'pinch_start',
|
...pinchEvent,
|
||||||
})
|
name: 'pinch_start',
|
||||||
editor.forceTick()
|
})
|
||||||
editor.dispatch({
|
.forceTick()
|
||||||
...pinchEvent,
|
editor
|
||||||
name: 'pinch',
|
.dispatch({
|
||||||
point: new Vec(0, 0, 1),
|
...pinchEvent,
|
||||||
})
|
name: 'pinch',
|
||||||
editor.forceTick()
|
point: new Vec(0, 0, 1),
|
||||||
editor.dispatch({
|
})
|
||||||
...pinchEvent,
|
.forceTick()
|
||||||
name: 'pinch_end',
|
editor
|
||||||
})
|
.dispatch({
|
||||||
editor.forceTick()
|
...pinchEvent,
|
||||||
|
name: 'pinch_end',
|
||||||
|
})
|
||||||
|
.forceTick()
|
||||||
expect(editor.getCamera()).toMatchObject({ x: 0, y: 0, z: 1 })
|
expect(editor.getCamera()).toMatchObject({ x: 0, y: 0, z: 1 })
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
@ -211,10 +227,47 @@ describe('CameraOptions.panSpeed', () => {
|
||||||
.forceTick()
|
.forceTick()
|
||||||
expect(editor.getCamera()).toMatchObject({ x: 0, y: 0, z: 1.01 }) // 1 + 1
|
expect(editor.getCamera()).toMatchObject({ x: 0, y: 0, z: 1.01 }) // 1 + 1
|
||||||
})
|
})
|
||||||
|
it('Does not effect hand tool panning', () => {
|
||||||
it.todo('hand tool panning')
|
editor.setCameraOptions({ ...DEFAULT_CAMERA_OPTIONS, panSpeed: 2 })
|
||||||
it.todo('spacebar panning')
|
editor.setCurrentTool('hand').pointerDown(0, 0).pointerMove(5, 10).forceTick()
|
||||||
it.todo('edge scroll panning')
|
expect(editor.getCamera()).toMatchObject({ x: 5, y: 10, z: 1 })
|
||||||
|
})
|
||||||
|
it('Effects spacebar panning (2x)', () => {
|
||||||
|
editor.setCameraOptions({ ...DEFAULT_CAMERA_OPTIONS, panSpeed: 2 })
|
||||||
|
editor
|
||||||
|
.dispatch({ ...keyBoardEvent, key: ' ', code: 'Space' })
|
||||||
|
.pointerDown(0, 0)
|
||||||
|
.pointerMove(5, 10)
|
||||||
|
.forceTick()
|
||||||
|
expect(editor.getCamera()).toMatchObject({ x: 10, y: 20, z: 1 })
|
||||||
|
})
|
||||||
|
it('Effects spacebar panning (0.5x)', () => {
|
||||||
|
editor.setCameraOptions({ ...DEFAULT_CAMERA_OPTIONS, panSpeed: 0.5 })
|
||||||
|
editor
|
||||||
|
.dispatch({ ...keyBoardEvent, key: ' ', code: 'Space' })
|
||||||
|
.pointerDown(0, 0)
|
||||||
|
.pointerMove(5, 10)
|
||||||
|
.forceTick()
|
||||||
|
expect(editor.getCamera()).toMatchObject({ x: 2.5, y: 5, z: 1 })
|
||||||
|
})
|
||||||
|
it('Does not effect edge scroll panning', () => {
|
||||||
|
const shapeId = createShapeId()
|
||||||
|
const viewportScreenBounds = editor.getViewportScreenBounds()
|
||||||
|
editor.user.updateUserPreferences({ edgeScrollSpeed: 1 })
|
||||||
|
editor
|
||||||
|
.setCameraOptions({ ...DEFAULT_CAMERA_OPTIONS, panSpeed: 2 })
|
||||||
|
.createShape({ id: shapeId, type: 'geo', x: 10, y: 10 })
|
||||||
|
.select(shapeId)
|
||||||
|
const shape = editor.getSelectedShapes()[0]
|
||||||
|
editor.selectNone()
|
||||||
|
// Move shape far beyond bounds to trigger edge scrolling at maximum speed
|
||||||
|
editor.pointerDown(shape.x, shape.y).pointerMove(-5000, -5000).forceTick()
|
||||||
|
// At maximum speed and a zoom level of 1, the camera should move by 40px per tick if the screen
|
||||||
|
// is wider than 1000 pixels, or by 40 * 0.612px if it is smaller.
|
||||||
|
const newX = viewportScreenBounds.w < 1000 ? 40 * 0.612 : 40
|
||||||
|
const newY = viewportScreenBounds.h < 1000 ? 40 * 0.612 : 40
|
||||||
|
expect(editor.getCamera()).toMatchObject({ x: newX, y: newY, z: 1 })
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
describe('CameraOptions.zoomSpeed', () => {
|
describe('CameraOptions.zoomSpeed', () => {
|
||||||
|
@ -251,9 +304,78 @@ describe('CameraOptions.zoomSpeed', () => {
|
||||||
expect(editor.getCamera()).toMatchObject({ x: 5, y: 10, z: 1 })
|
expect(editor.getCamera()).toMatchObject({ x: 5, y: 10, z: 1 })
|
||||||
})
|
})
|
||||||
|
|
||||||
it.todo('zoom method')
|
it('Effects pinch zooming (2x)', () => {
|
||||||
it.todo('zoom tool zooming')
|
editor
|
||||||
it.todo('pinch zooming')
|
.setCameraOptions({ ...DEFAULT_CAMERA_OPTIONS, zoomSpeed: 2 })
|
||||||
|
.dispatch({
|
||||||
|
...pinchEvent,
|
||||||
|
name: 'pinch_start',
|
||||||
|
})
|
||||||
|
.forceTick()
|
||||||
|
editor.dispatch({
|
||||||
|
...pinchEvent,
|
||||||
|
name: 'pinch',
|
||||||
|
delta: new Vec(0, 0, 1),
|
||||||
|
})
|
||||||
|
editor.forceTick()
|
||||||
|
editor.dispatch({
|
||||||
|
...pinchEvent,
|
||||||
|
name: 'pinch_end',
|
||||||
|
})
|
||||||
|
editor.forceTick()
|
||||||
|
expect(editor.getCamera()).toMatchObject({ x: 0, y: 0, z: 2 })
|
||||||
|
})
|
||||||
|
it('Effects pinch zooming (0.5x)', () => {
|
||||||
|
editor
|
||||||
|
.setCameraOptions({ ...DEFAULT_CAMERA_OPTIONS, zoomSpeed: 0.5 })
|
||||||
|
.dispatch({
|
||||||
|
...pinchEvent,
|
||||||
|
name: 'pinch_start',
|
||||||
|
})
|
||||||
|
.forceTick()
|
||||||
|
editor.dispatch({
|
||||||
|
...pinchEvent,
|
||||||
|
name: 'pinch',
|
||||||
|
delta: new Vec(0, 0, 1),
|
||||||
|
})
|
||||||
|
editor.forceTick()
|
||||||
|
editor.dispatch({
|
||||||
|
...pinchEvent,
|
||||||
|
name: 'pinch_end',
|
||||||
|
})
|
||||||
|
editor.forceTick()
|
||||||
|
expect(editor.getCamera()).toMatchObject({ x: 0, y: 0, z: 0.5 })
|
||||||
|
})
|
||||||
|
it('Does not effect zoom tool zooming (2x)', () => {
|
||||||
|
editor.setCameraOptions({ ...DEFAULT_CAMERA_OPTIONS, zoomSpeed: 2 })
|
||||||
|
expect(editor.getCamera()).toMatchObject({ x: 0, y: 0, z: 1 })
|
||||||
|
editor.setCurrentTool('zoom').click()
|
||||||
|
jest.advanceTimersByTime(300)
|
||||||
|
expect(editor.getCamera()).toMatchObject({ x: 0, y: 0, z: 2 })
|
||||||
|
})
|
||||||
|
it('Does not effect zoom tool zooming (0.5x)', () => {
|
||||||
|
editor.setCameraOptions({ ...DEFAULT_CAMERA_OPTIONS, zoomSpeed: 0.5 })
|
||||||
|
expect(editor.getCamera()).toMatchObject({ x: 0, y: 0, z: 1 })
|
||||||
|
editor.setCurrentTool('zoom').click()
|
||||||
|
jest.advanceTimersByTime(300)
|
||||||
|
expect(editor.getCamera()).toMatchObject({ x: 0, y: 0, z: 2 })
|
||||||
|
})
|
||||||
|
it('Does not effect editor zoom method (2x)', () => {
|
||||||
|
editor.setCameraOptions({ ...DEFAULT_CAMERA_OPTIONS, zoomSpeed: 2 })
|
||||||
|
expect(editor.getCamera()).toMatchObject({ x: 0, y: 0, z: 1 })
|
||||||
|
editor.zoomIn(new Vec(0, 0), { immediate: true })
|
||||||
|
expect(editor.getCamera()).toMatchObject({ x: 0, y: 0, z: 2 })
|
||||||
|
editor.zoomOut(new Vec(0, 0), { immediate: true })
|
||||||
|
expect(editor.getCamera()).toMatchObject({ x: 0, y: 0, z: 1 })
|
||||||
|
})
|
||||||
|
it('Does not effect editor zoom method (0.5x)', () => {
|
||||||
|
editor.setCameraOptions({ ...DEFAULT_CAMERA_OPTIONS, zoomSpeed: 0.5 })
|
||||||
|
expect(editor.getCamera()).toMatchObject({ x: 0, y: 0, z: 1 })
|
||||||
|
editor.zoomIn(new Vec(0, 0), { immediate: true })
|
||||||
|
expect(editor.getCamera()).toMatchObject({ x: 0, y: 0, z: 2 })
|
||||||
|
editor.zoomOut(new Vec(0, 0), { immediate: true })
|
||||||
|
expect(editor.getCamera()).toMatchObject({ x: 0, y: 0, z: 1 })
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
describe('CameraOptions.isLocked', () => {
|
describe('CameraOptions.isLocked', () => {
|
||||||
|
@ -747,11 +869,61 @@ describe('Contain behavior', () => {
|
||||||
})
|
})
|
||||||
|
|
||||||
describe('Inside behavior', () => {
|
describe('Inside behavior', () => {
|
||||||
it.todo('Allows panning that keeps the bounds inside of the padded viewport')
|
it('Allows panning that keeps the bounds inside of the padded viewport', () => {
|
||||||
|
const bounds = editor.getViewportScreenBounds()
|
||||||
|
// set the constraints to be inside the viewport + 100px padding
|
||||||
|
editor.setCameraOptions({
|
||||||
|
...DEFAULT_CAMERA_OPTIONS,
|
||||||
|
constraints: {
|
||||||
|
...DEFAULT_CONSTRAINTS,
|
||||||
|
bounds: { x: 0, y: 0, w: bounds.w, h: bounds.h },
|
||||||
|
behavior: 'inside',
|
||||||
|
origin: { x: 0, y: 0 },
|
||||||
|
padding: { x: 100, y: 100 },
|
||||||
|
initialZoom: 'fit-min',
|
||||||
|
},
|
||||||
|
})
|
||||||
|
expect(editor.getCamera()).toMatchObject({ x: 0, y: 0, z: 1 })
|
||||||
|
// panning far outside of the bounds
|
||||||
|
editor.pan(new Vec(-10000, -10000))
|
||||||
|
jest.advanceTimersByTime(300)
|
||||||
|
// should be clamped to the bounds + padding
|
||||||
|
expect(editor.getCamera()).toMatchObject({ x: -100, y: -100, z: 1 })
|
||||||
|
// panning to the opposite direction, far outside of the bounds
|
||||||
|
editor.pan(new Vec(10000, 10000))
|
||||||
|
jest.advanceTimersByTime(300)
|
||||||
|
// should be clamped to the bounds + padding
|
||||||
|
expect(editor.getCamera()).toMatchObject({ x: 100, y: 100, z: 1 })
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
describe('Outside behavior', () => {
|
describe('Outside behavior', () => {
|
||||||
it.todo('Allows panning that keeps the bounds adjacent to the padded viewport')
|
it('Allows panning that keeps the bounds adjacent to the padded viewport', () => {
|
||||||
|
const bounds = editor.getViewportScreenBounds()
|
||||||
|
// set the constraints to be the viewport + 100px padding
|
||||||
|
editor.setCameraOptions({
|
||||||
|
...DEFAULT_CAMERA_OPTIONS,
|
||||||
|
constraints: {
|
||||||
|
...DEFAULT_CONSTRAINTS,
|
||||||
|
bounds: { x: 0, y: 0, w: bounds.w, h: bounds.h },
|
||||||
|
behavior: 'outside',
|
||||||
|
origin: { x: 0, y: 0 },
|
||||||
|
padding: { x: 100, y: 100 },
|
||||||
|
initialZoom: 'fit-min',
|
||||||
|
},
|
||||||
|
})
|
||||||
|
expect(editor.getCamera()).toMatchObject({ x: 0, y: 0, z: 1 })
|
||||||
|
// panning far outside of the bounds
|
||||||
|
editor.pan(new Vec(-10000, -10000))
|
||||||
|
jest.advanceTimersByTime(300)
|
||||||
|
// should be clamped so that the far edge of the bounds is adjacent to the viewport + padding
|
||||||
|
expect(editor.getCamera()).toMatchObject({ x: -bounds.w + 100, y: -bounds.h + 100, z: 1 })
|
||||||
|
// panning to the opposite direction, far outside of the bounds
|
||||||
|
editor.pan(new Vec(10000, 10000))
|
||||||
|
jest.advanceTimersByTime(300)
|
||||||
|
// should be clamped so that the far edge of the bounds is adjacent to the viewport + padding
|
||||||
|
expect(editor.getCamera()).toMatchObject({ x: bounds.w - 100, y: bounds.h - 100, z: 1 })
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
describe('Allows mixed values for x and y', () => {
|
describe('Allows mixed values for x and y', () => {
|
||||||
|
|
Loading…
Reference in a new issue