snapshot of https://github.com/tldraw/tldraw.git (main, 2026-09-23; blobs > 131072B omitted)
Some checks failed
Deploy analytics worker / Deploy analytics worker to ${{ (github.ref == 'refs/heads/production' && 'production') || (github.ref == 'refs/heads/main' && 'staging') || 'preview' }} (push) Failing after 1s
i18n upload strings / Upload latest strings (push) Failing after 1s
Deploy bemo / Deploy bemo to ${{ (github.ref == 'refs/heads/bemo-production' && 'production') || (github.ref == 'refs/heads/main' && 'staging') || 'preview' }} (push) Failing after 1s
Deploy grafana resources / ${{ github.event_name == 'push' && 'Deploy grafana resources' || 'Validate grafana resources (dry run)' }} (push) Failing after 0s
End to end tests (dotcom) / Check for relevant changes (push) Failing after 0s
End to end tests (examples) / Check for relevant changes (push) Failing after 0s
End to end tests (mermaid readability) / Check for relevant changes (push) Failing after 1s
End to end tests (perf) / Check for relevant changes (push) Failing after 0s
Publish editor extensions / Publish editor extensions (push) Failing after 1s
Publish SDK packages / Remove the publish-packages label (push) Has been skipped
Checks / Tests & checks (push) Has been cancelled
Checks / Build all projects (push) Has been cancelled
Deploy templates / Deploy templates (push) Has been cancelled
End to end tests (dotcom) / End to end tests (dotcom) (push) Has been cancelled
End to end tests (mermaid readability) / End to end tests (mermaid readability) (push) Has been cancelled
End to end tests (perf) / End to end tests (perf) (push) Has been cancelled
Deploy .com / Deploy dotcom to ${{ (github.ref == 'refs/heads/production' && 'production') || (github.ref == 'refs/heads/main' && 'staging') || 'preview' }} (push) Has been cancelled
End to end tests (examples) / End to end tests (examples) (push) Has been cancelled
Publish SDK packages / Publish SDK packages (push) Has been cancelled
Publish SDK packages / Tell user released version (push) Has been cancelled
Publish SDK packages / Publishes code templates to separate repositories (push) Has been cancelled
Publish SDK packages / Deploys the template demos with the new SDK version (push) Has been cancelled
Some checks failed
Deploy analytics worker / Deploy analytics worker to ${{ (github.ref == 'refs/heads/production' && 'production') || (github.ref == 'refs/heads/main' && 'staging') || 'preview' }} (push) Failing after 1s
i18n upload strings / Upload latest strings (push) Failing after 1s
Deploy bemo / Deploy bemo to ${{ (github.ref == 'refs/heads/bemo-production' && 'production') || (github.ref == 'refs/heads/main' && 'staging') || 'preview' }} (push) Failing after 1s
Deploy grafana resources / ${{ github.event_name == 'push' && 'Deploy grafana resources' || 'Validate grafana resources (dry run)' }} (push) Failing after 0s
End to end tests (dotcom) / Check for relevant changes (push) Failing after 0s
End to end tests (examples) / Check for relevant changes (push) Failing after 0s
End to end tests (mermaid readability) / Check for relevant changes (push) Failing after 1s
End to end tests (perf) / Check for relevant changes (push) Failing after 0s
Publish editor extensions / Publish editor extensions (push) Failing after 1s
Publish SDK packages / Remove the publish-packages label (push) Has been skipped
Checks / Tests & checks (push) Has been cancelled
Checks / Build all projects (push) Has been cancelled
Deploy templates / Deploy templates (push) Has been cancelled
End to end tests (dotcom) / End to end tests (dotcom) (push) Has been cancelled
End to end tests (mermaid readability) / End to end tests (mermaid readability) (push) Has been cancelled
End to end tests (perf) / End to end tests (perf) (push) Has been cancelled
Deploy .com / Deploy dotcom to ${{ (github.ref == 'refs/heads/production' && 'production') || (github.ref == 'refs/heads/main' && 'staging') || 'preview' }} (push) Has been cancelled
End to end tests (examples) / End to end tests (examples) (push) Has been cancelled
Publish SDK packages / Publish SDK packages (push) Has been cancelled
Publish SDK packages / Tell user released version (push) Has been cancelled
Publish SDK packages / Publishes code templates to separate repositories (push) Has been cancelled
Publish SDK packages / Deploys the template demos with the new SDK version (push) Has been cancelled
This commit is contained in:
256
apps/examples/e2e/tests/test-camera.spec.ts
Normal file
256
apps/examples/e2e/tests/test-camera.spec.ts
Normal file
@@ -0,0 +1,256 @@
|
||||
import { CDPSession, Page, expect } from '@playwright/test'
|
||||
import { type Editor } from 'tldraw'
|
||||
import test from '../fixtures/fixtures'
|
||||
import { setupOrReset, sleep } from '../shared-e2e'
|
||||
|
||||
declare const editor: Editor
|
||||
|
||||
let client: CDPSession
|
||||
|
||||
const dispatchTouch = async (
|
||||
client: CDPSession,
|
||||
type: 'touchStart' | 'touchMove' | 'touchEnd' | 'touchCancel',
|
||||
touchPoints: { x: number; y: number }[]
|
||||
) => {
|
||||
await client.send('Input.dispatchTouchEvent', { type, touchPoints })
|
||||
}
|
||||
|
||||
const multiTouchGesture = async ({
|
||||
start,
|
||||
end,
|
||||
steps,
|
||||
}: {
|
||||
start: { x: number; y: number }[]
|
||||
end: { x: number; y: number }[]
|
||||
steps: number
|
||||
}) => {
|
||||
const finger1 = { x: start[0].x, y: start[0].y }
|
||||
const finger2 = { x: start[1].x, y: start[1].y }
|
||||
const finalTouch = [finger1, finger2]
|
||||
|
||||
await dispatchTouch(client, 'touchStart', [finger1, finger2])
|
||||
await sleep(10)
|
||||
|
||||
// how far should the fingers move each step to reach the end point?
|
||||
const finger1StepDistance = {
|
||||
x: (end[0].x - start[0].x) / steps,
|
||||
y: (end[0].y - start[0].y) / steps,
|
||||
}
|
||||
const finger2StepDistance = {
|
||||
x: (end[1].x - start[1].x) / steps,
|
||||
y: (end[1].y - start[1].y) / steps,
|
||||
}
|
||||
|
||||
for (let i = 1; i < steps; i++) {
|
||||
await dispatchTouch(client, 'touchMove', [
|
||||
{ x: finger1.x + i * finger1StepDistance.x, y: finger1.y + i * finger1StepDistance.y },
|
||||
{ x: finger2.x + i * finger2StepDistance.x, y: finger2.y + i * finger2StepDistance.y },
|
||||
])
|
||||
|
||||
await sleep(10)
|
||||
}
|
||||
|
||||
await dispatchTouch(client, 'touchEnd', finalTouch)
|
||||
await sleep(10)
|
||||
}
|
||||
|
||||
const scrollZoom = async ({
|
||||
page,
|
||||
currentZoomLevel,
|
||||
zoomDirection,
|
||||
steps,
|
||||
}: {
|
||||
page: Page
|
||||
currentZoomLevel: number
|
||||
zoomDirection: 'in' | 'out'
|
||||
steps: number
|
||||
}) => {
|
||||
page.keyboard.down('Control')
|
||||
// zooming with the scroll wheel is capped within the editor to make sure it's consistent between browsers,
|
||||
//scrolling further than 15px per event doesn't have any effect
|
||||
for (let i = 0; i < steps; i++) {
|
||||
await page.mouse.wheel(0, zoomDirection === 'out' ? -15 : 15)
|
||||
}
|
||||
page.keyboard.up('Control')
|
||||
// scrolling 15px on the mouse wheel changes the zoom level by 10%
|
||||
const expectedZoomLevel =
|
||||
zoomDirection === 'out' ? currentZoomLevel * 1.1 ** steps : currentZoomLevel / 1.1 ** steps
|
||||
|
||||
return expectedZoomLevel
|
||||
}
|
||||
|
||||
test.describe('camera', () => {
|
||||
test.beforeEach(setupOrReset)
|
||||
|
||||
test('panning', async ({ isMobile, page }) => {
|
||||
test.skip(!isMobile)
|
||||
client = await page.context().newCDPSession(page)
|
||||
expect(await page.evaluate(() => editor.inputs.getCurrentPagePoint())).toEqual({
|
||||
x: 50,
|
||||
y: 50,
|
||||
z: 0,
|
||||
})
|
||||
|
||||
// the fewer steps you use, the faster the pan. So the ratio of pixels moved is not one-to-one.
|
||||
await multiTouchGesture({
|
||||
start: [
|
||||
{ x: 50, y: 50 },
|
||||
{ x: 100, y: 100 },
|
||||
],
|
||||
end: [
|
||||
{ x: 100, y: 100 },
|
||||
{ x: 150, y: 150 },
|
||||
],
|
||||
steps: 50,
|
||||
})
|
||||
|
||||
// With current panning speed, the above gesture moves the camera by ~36px on each axis.
|
||||
// Use a tolerance to avoid brittleness across engines/emulation.
|
||||
const [x, y] = await page.evaluate(() => {
|
||||
const point = editor.inputs.getCurrentPagePoint()
|
||||
return [point.x, point.y]
|
||||
})
|
||||
expect(x).toBeGreaterThan(80)
|
||||
expect(x).toBeLessThan(95)
|
||||
expect(y).toBeGreaterThan(80)
|
||||
expect(y).toBeLessThan(95)
|
||||
})
|
||||
|
||||
test('pinching on trackpad', async ({ page, isMobile }) => {
|
||||
// Test is flaky, disabling.
|
||||
test.skip(true)
|
||||
|
||||
// pinching on trackpad is the same event as ctrl+scrollwheel
|
||||
test.skip(isMobile)
|
||||
expect(await page.evaluate(() => editor.getZoomLevel())).toBe(1)
|
||||
|
||||
let expectedZoomLevel = await scrollZoom({
|
||||
page,
|
||||
currentZoomLevel: 1,
|
||||
zoomDirection: 'in',
|
||||
steps: 3,
|
||||
})
|
||||
// toBeCloseTo is used to avoid floating point errors
|
||||
expect(await page.evaluate(() => editor.getZoomLevel())).toBeCloseTo(expectedZoomLevel, 1)
|
||||
|
||||
expectedZoomLevel = await scrollZoom({
|
||||
page,
|
||||
currentZoomLevel: expectedZoomLevel,
|
||||
zoomDirection: 'out',
|
||||
steps: 3,
|
||||
})
|
||||
|
||||
expect(await page.evaluate(() => editor.getZoomLevel())).toBeCloseTo(expectedZoomLevel, 1)
|
||||
})
|
||||
|
||||
test('pinching on touchscreen', async ({ page, isMobile }) => {
|
||||
test.skip(!isMobile)
|
||||
|
||||
client = await page.context().newCDPSession(page)
|
||||
|
||||
expect(await page.evaluate(() => editor.getZoomLevel())).toBe(1)
|
||||
|
||||
// zoom out
|
||||
await multiTouchGesture({
|
||||
start: [
|
||||
{ x: 100, y: 100 },
|
||||
{ x: 200, y: 200 },
|
||||
],
|
||||
end: [
|
||||
{ x: 149, y: 149 },
|
||||
{ x: 151, y: 151 },
|
||||
],
|
||||
steps: 50,
|
||||
})
|
||||
expect(await page.evaluate(() => editor.getZoomLevel())).toBe(0.05)
|
||||
|
||||
// now zoom in
|
||||
await multiTouchGesture({
|
||||
start: [
|
||||
{ x: 200, y: 200 },
|
||||
{ x: 200, y: 200 },
|
||||
],
|
||||
end: [
|
||||
{ x: 0, y: 0 },
|
||||
{ x: 400, y: 400 },
|
||||
],
|
||||
steps: 50,
|
||||
})
|
||||
expect(await page.evaluate(() => editor.getZoomLevel())).toBe(8)
|
||||
})
|
||||
|
||||
test('two-finger pinch does not change the selection', async ({ page, isMobile }) => {
|
||||
test.skip(!isMobile)
|
||||
|
||||
client = await page.context().newCDPSession(page)
|
||||
|
||||
// Finger 1 lands here; finger 2 above it. Both are kept inside the mobile
|
||||
// viewport so the gesture registers.
|
||||
const f1 = { x: 196, y: 470, id: 0 }
|
||||
const f2 = { x: 196, y: 300, id: 1 }
|
||||
|
||||
// Shape "a" (selected, off to the side) plus shape "b" placed directly under
|
||||
// finger 1, so the first finger's pointer_down selects it.
|
||||
await page.evaluate((f) => {
|
||||
const p = editor.screenToPage({ x: f.x, y: f.y })
|
||||
editor.createShapes([
|
||||
{
|
||||
id: 'shape:e2eA',
|
||||
type: 'geo',
|
||||
x: -1000,
|
||||
y: -1000,
|
||||
props: { w: 100, h: 100, fill: 'solid' },
|
||||
},
|
||||
{
|
||||
id: 'shape:e2eB',
|
||||
type: 'geo',
|
||||
x: p.x - 60,
|
||||
y: p.y - 60,
|
||||
props: { w: 120, h: 120, fill: 'solid' },
|
||||
},
|
||||
] as any)
|
||||
editor.setSelectedShapes(['shape:e2eA'] as any)
|
||||
}, f1)
|
||||
|
||||
expect(await page.evaluate(() => editor.getSelectedShapeIds())).toEqual(['shape:e2eA'])
|
||||
|
||||
// Finger 1 down on shape b — through the real touch/pointer pipeline this
|
||||
// changes the selection to b.
|
||||
await client.send('Input.dispatchTouchEvent', { type: 'touchStart', touchPoints: [f1] })
|
||||
await sleep(30)
|
||||
expect(await page.evaluate(() => editor.getSelectedShapeIds())).toEqual(['shape:e2eB'])
|
||||
|
||||
// Finger 2 joins, then both fingers move apart to pinch-zoom, then lift.
|
||||
await client.send('Input.dispatchTouchEvent', { type: 'touchStart', touchPoints: [f1, f2] })
|
||||
await sleep(30)
|
||||
// The pinch has started, so the incidental selection is already rolled back —
|
||||
// the original selection is shown during the gesture, not only after it ends.
|
||||
expect(await page.evaluate(() => editor.getSelectedShapeIds())).toEqual(['shape:e2eA'])
|
||||
const steps = 12
|
||||
for (let i = 1; i <= steps; i++) {
|
||||
await client.send('Input.dispatchTouchEvent', {
|
||||
type: 'touchMove',
|
||||
touchPoints: [
|
||||
{ x: f1.x, y: f1.y + i * 8, id: 0 }, // finger 1 moves down
|
||||
{ x: f2.x, y: f2.y - i * 8, id: 1 }, // finger 2 moves up
|
||||
],
|
||||
})
|
||||
await sleep(10)
|
||||
}
|
||||
await client.send('Input.dispatchTouchEvent', { type: 'touchEnd', touchPoints: [] })
|
||||
await sleep(100)
|
||||
|
||||
// The incidental selection of b is rolled back to the pre-gesture selection.
|
||||
expect(await page.evaluate(() => editor.getSelectedShapeIds())).toEqual(['shape:e2eA'])
|
||||
// ...and the pinch really did zoom (sanity check that a pinch happened).
|
||||
expect(await page.evaluate(() => editor.getZoomLevel())).not.toBe(1)
|
||||
})
|
||||
|
||||
test.fixme('minimap', async () => {
|
||||
// todo
|
||||
})
|
||||
|
||||
test.fixme('hand tool', async () => {
|
||||
// todo
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user