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

This commit is contained in:
upstream-mirror
2026-09-23 16:12:52 +05:00
commit a774ac673a
4541 changed files with 770499 additions and 0 deletions

View File

@@ -0,0 +1,44 @@
import type { Editor } from 'tldraw'
declare const editor: Editor
const NATIVE_CONTEXT = 'NATIVE_APP'
/**
* Open the `/end-to-end` example (which exposes `window.editor`), wait for the
* canvas, and reset to a known state: no animations, zoom at 1, no shapes.
*/
export async function openEditor() {
const webContext = await currentWebContext()
await browser.switchContext(webContext)
if (!(await browser.getUrl()).includes('end-to-end')) {
await browser.url(`${browser.options.baseUrl}/end-to-end`)
}
await browser.$('.tl-canvas').waitForExist({ timeout: 30_000 })
await browser.execute(() => {
editor.user.updateUserPreferences({ animationSpeed: 0 })
editor.selectAll().deleteShapes(editor.getSelectedShapeIds())
editor.setCurrentTool('select')
editor.resetZoom()
})
}
/** Read the current zoom level from the live editor. */
export function getZoom(): Promise<number> {
return browser.execute(() => editor.getZoomLevel())
}
/** Read the currently selected shape ids. */
export function getSelectedShapeIds(): Promise<string[]> {
return browser.execute(() => editor.getSelectedShapeIds())
}
/** The webview context id changes per platform/run; find it rather than hardcode. */
export async function currentWebContext(): Promise<string> {
const contexts = (await browser.getContexts()) as string[]
const web = contexts.find((c) => c !== NATIVE_CONTEXT)
if (!web) throw new Error(`No web context found. Available: ${contexts.join(', ')}`)
return web
}

View File

@@ -0,0 +1,118 @@
import { currentWebContext } from './app'
const NATIVE_CONTEXT = 'NATIVE_APP'
/**
* Two-finger pinch over the tldraw canvas.
*
* Why this is the way it is: on both iOS (XCUITest) and Android (UiAutomator2),
* multi-touch W3C actions only operate in the NATIVE context, and only on native
* screen coordinates. So we:
*
* 1. (web context) measure the canvas rect + viewport in CSS pixels,
* 2. (native context) translate those to device coordinates and run two
* simultaneous touch pointers from the centre outwards (or inwards),
* 3. (web context) hand back so the caller can assert on `editor` state.
*
* The touches enter through the OS, so Safari/Chrome's own gesture recogniser
* sees them — that is the whole point, and what synthetic DOM touch can't do.
*
* @param scale >1 spreads the fingers apart (zoom in), <1 brings them together
* (zoom out).
*/
export async function pinchCanvas({ scale, steps = 12 }: { scale: number; steps?: number }) {
const web = await currentWebContext()
// 1. Measure in the web context (CSS pixels relative to the layout viewport).
await browser.switchContext(web)
const rect = await browser.execute(() => {
const el = document.querySelector('.tl-canvas')!
const r = el.getBoundingClientRect()
return {
cx: r.left + r.width / 2,
cy: r.top + r.height / 2,
dpr: window.devicePixelRatio,
// Offset of the web viewport inside the native window (browser chrome).
// visualViewport.offsetTop is the cleanest signal we have for this.
offsetTop: window.visualViewport?.offsetTop ?? 0,
}
})
// 2. Translate CSS px -> native device coordinates.
//
// iOS reports native coordinates in POINTS (already CSS-px-equivalent), so the
// scale factor is 1 and we only add the top inset for the Safari toolbar.
// Android UiAutomator2 reports PIXELS, so we multiply by devicePixelRatio.
//
// `TOP_INSET` is the one bit that may need a per-device tweak — the height of
// the browser chrome above the web viewport. Calibrate once per device class.
const isAndroid = browser.isAndroid
const pxScale = isAndroid ? rect.dpr : 1
const TOP_INSET = Number(process.env.DEVICE_TOP_INSET ?? (isAndroid ? 0 : 0))
const cx = rect.cx * pxScale
const cy = (rect.cy + rect.offsetTop) * pxScale + TOP_INSET
// Fingers start 60px either side of centre, end at 60*scale either side.
const startGap = 60 * pxScale
const endGap = startGap * scale
const finger = (id: string, dir: -1 | 1) =>
buildPinchPointer({
id,
cx,
cy,
startGap: dir * startGap,
endGap: dir * endGap,
steps,
})
// 3. Perform the gesture natively, then return to the web context.
await browser.switchContext(NATIVE_CONTEXT)
await browser.performActions([finger('finger1', -1), finger('finger2', 1)])
await browser.releaseActions()
await browser.switchContext(web)
}
/**
* One touch pointer that presses at `cx + startGap`, then moves in `steps` hops
* to `cx + endGap` along the x axis (so two mirrored pointers spread/contract).
*/
function buildPinchPointer({
id,
cx,
cy,
startGap,
endGap,
steps,
}: {
id: string
cx: number
cy: number
startGap: number
endGap: number
steps: number
}) {
const actions: Array<Record<string, unknown>> = [
{ type: 'pointerMove', duration: 0, x: Math.round(cx + startGap), y: Math.round(cy) },
{ type: 'pointerDown', button: 0 },
{ type: 'pause', duration: 50 },
]
for (let i = 1; i <= steps; i++) {
const gap = startGap + ((endGap - startGap) * i) / steps
actions.push({
type: 'pointerMove',
duration: 25,
x: Math.round(cx + gap),
y: Math.round(cy),
})
}
actions.push({ type: 'pointerUp', button: 0 })
return {
type: 'pointer',
id,
parameters: { pointerType: 'touch' },
actions,
}
}

View File

@@ -0,0 +1,70 @@
import { currentWebContext } from './app'
const NATIVE_CONTEXT = 'NATIVE_APP'
/**
* Draw a single-finger freehand stroke across the canvas through the real OS
* touch pipeline (same native-context + coordinate-translation approach as
* pinch.ts). `from`/`to` are CSS-pixel offsets relative to the canvas centre.
*/
export async function drawTouchStroke({
from,
to,
steps = 12,
}: {
from: { x: number; y: number }
to: { x: number; y: number }
steps?: number
}) {
const web = await currentWebContext()
// Measure in the web context (CSS pixels relative to the layout viewport).
await browser.switchContext(web)
const rect = await browser.execute(() => {
const el = document.querySelector('.tl-canvas')!
const r = el.getBoundingClientRect()
return {
cx: r.left + r.width / 2,
cy: r.top + r.height / 2,
dpr: window.devicePixelRatio,
offsetTop: window.visualViewport?.offsetTop ?? 0,
}
})
// CSS px -> native device coordinates (iOS reports points, Android pixels).
const isAndroid = browser.isAndroid
const pxScale = isAndroid ? rect.dpr : 1
const TOP_INSET = Number(process.env.DEVICE_TOP_INSET ?? 0)
const cx = rect.cx * pxScale
const cy = (rect.cy + rect.offsetTop) * pxScale + TOP_INSET
const point = (p: { x: number; y: number }) => ({
x: Math.round(cx + p.x * pxScale),
y: Math.round(cy + p.y * pxScale),
})
const start = point(from)
const actions: Array<Record<string, unknown>> = [
{ type: 'pointerMove', duration: 0, ...start },
{ type: 'pointerDown', button: 0 },
{ type: 'pause', duration: 50 },
]
for (let i = 1; i <= steps; i++) {
actions.push({
type: 'pointerMove',
duration: 25,
...point({
x: from.x + ((to.x - from.x) * i) / steps,
y: from.y + ((to.y - from.y) * i) / steps,
}),
})
}
actions.push({ type: 'pointerUp', button: 0 })
await browser.switchContext(NATIVE_CONTEXT)
await browser.performActions([
{ type: 'pointer', id: 'finger1', parameters: { pointerType: 'touch' }, actions },
])
await browser.releaseActions()
await browser.switchContext(web)
}