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:
261
apps/examples/e2e/tests/test-overlay-snapshots.spec.ts
Normal file
261
apps/examples/e2e/tests/test-overlay-snapshots.spec.ts
Normal file
@@ -0,0 +1,261 @@
|
||||
import { Page, expect } from '@playwright/test'
|
||||
import { Editor, IndexKey } from 'tldraw'
|
||||
import test from '../fixtures/fixtures'
|
||||
import { hardResetEditor, setup } from '../shared-e2e'
|
||||
|
||||
declare const editor: Editor
|
||||
|
||||
// Screenshot-based regression tests for canvas overlay appearance.
|
||||
//
|
||||
// Overlays render to a single <canvas class="tl-canvas-overlays"> element, so
|
||||
// we screenshot the `.tl-canvas` region and let the image compare cover every
|
||||
// overlay kind (selection foreground, shape handles, brush, snap indicators,
|
||||
// scribble, etc.) at once.
|
||||
//
|
||||
// Tests use direct editor state injection (brush/zoomBrush via
|
||||
// updateInstanceState, scribbles via ScribbleManager, snap indicators via
|
||||
// SnapManager.setIndicators) rather than simulating drags. This is what the
|
||||
// unit tests for each OverlayUtil already do, and it keeps the states frame-
|
||||
// exact rather than timing-dependent.
|
||||
|
||||
async function prepare(page: Page) {
|
||||
await page.evaluate(() => {
|
||||
editor.user.updateUserPreferences({ colorScheme: 'light', animationSpeed: 0 })
|
||||
editor.setCamera({ x: 0, y: 0, z: 1 }, { immediate: true })
|
||||
editor.updateInstanceState({ isGridMode: false })
|
||||
})
|
||||
// Move mouse out of any shape to avoid hover indicators.
|
||||
await page.mouse.move(0, 0)
|
||||
}
|
||||
|
||||
async function snapshotCanvas(page: Page) {
|
||||
// Wait for two rAFs so reactive overlay canvases have committed their paint.
|
||||
await page.evaluate(
|
||||
() =>
|
||||
new Promise<void>((resolve) =>
|
||||
requestAnimationFrame(() => requestAnimationFrame(() => resolve()))
|
||||
)
|
||||
)
|
||||
// Allow a small tolerance to absorb sub-pixel anti-aliasing variance on
|
||||
// rotated or curved overlay strokes. A real overlay regression (missing
|
||||
// handle, wrong color, moved stroke) changes far more than 0.5% of pixels.
|
||||
// Screenshot capture also waits for the canvas element to become stable,
|
||||
// which can exceed the global 2s expect timeout on cold preview runs.
|
||||
await expect(page.locator('.tl-canvas')).toHaveScreenshot({
|
||||
maxDiffPixelRatio: 0.005,
|
||||
timeout: 10_000,
|
||||
})
|
||||
}
|
||||
|
||||
test.describe('Overlay snapshots', () => {
|
||||
test.skip(
|
||||
({ isMobile }) => isMobile,
|
||||
'Overlay snapshots are desktop-only; mobile overlays render differently (coarse-pointer rotate handle, different resize handle visibility).'
|
||||
)
|
||||
|
||||
test.beforeEach(async ({ page, context }) => {
|
||||
const url = page.url()
|
||||
if (!url.includes('end-to-end')) {
|
||||
await setup({ page, context } as any)
|
||||
} else {
|
||||
await hardResetEditor(page)
|
||||
}
|
||||
await prepare(page)
|
||||
})
|
||||
|
||||
test('selection foreground: single shape', async ({ page }) => {
|
||||
await page.evaluate(() => {
|
||||
editor.createShape({
|
||||
type: 'geo',
|
||||
x: 220,
|
||||
y: 160,
|
||||
props: { w: 240, h: 160, geo: 'rectangle', dash: 'solid', fill: 'none' },
|
||||
})
|
||||
editor.selectAll()
|
||||
})
|
||||
await snapshotCanvas(page)
|
||||
})
|
||||
|
||||
test('selection foreground: multiple shapes', async ({ page }) => {
|
||||
await page.evaluate(() => {
|
||||
editor.createShapes([
|
||||
{
|
||||
type: 'geo',
|
||||
x: 180,
|
||||
y: 140,
|
||||
props: { w: 120, h: 100, geo: 'rectangle', dash: 'solid', fill: 'none' },
|
||||
},
|
||||
{
|
||||
type: 'geo',
|
||||
x: 360,
|
||||
y: 200,
|
||||
props: { w: 140, h: 120, geo: 'ellipse', dash: 'solid', fill: 'none' },
|
||||
},
|
||||
])
|
||||
editor.selectAll()
|
||||
})
|
||||
await snapshotCanvas(page)
|
||||
})
|
||||
|
||||
test('selection foreground: rotated shape', async ({ page }) => {
|
||||
await page.evaluate(() => {
|
||||
editor.createShape({
|
||||
type: 'geo',
|
||||
x: 260,
|
||||
y: 180,
|
||||
rotation: Math.PI / 6,
|
||||
props: { w: 220, h: 140, geo: 'rectangle', dash: 'solid', fill: 'none' },
|
||||
})
|
||||
editor.selectAll()
|
||||
})
|
||||
await snapshotCanvas(page)
|
||||
})
|
||||
|
||||
test('shape handles: arrow endpoints', async ({ page }) => {
|
||||
await page.evaluate(() => {
|
||||
editor.createShape({
|
||||
type: 'arrow',
|
||||
x: 220,
|
||||
y: 220,
|
||||
props: {
|
||||
start: { x: 0, y: 0 },
|
||||
end: { x: 280, y: 0 },
|
||||
dash: 'solid',
|
||||
},
|
||||
})
|
||||
editor.selectAll()
|
||||
})
|
||||
await snapshotCanvas(page)
|
||||
})
|
||||
|
||||
test('shape handles: line with vertices', async ({ page }) => {
|
||||
await page.evaluate(() => {
|
||||
editor.createShape({
|
||||
type: 'line',
|
||||
x: 200,
|
||||
y: 260,
|
||||
props: {
|
||||
dash: 'solid',
|
||||
points: {
|
||||
a1: { id: 'a1', index: 'a1' as IndexKey, x: 0, y: 0 },
|
||||
a2: { id: 'a2', index: 'a2' as IndexKey, x: 180, y: -60 },
|
||||
a3: { id: 'a3', index: 'a3' as IndexKey, x: 360, y: 0 },
|
||||
},
|
||||
},
|
||||
})
|
||||
editor.selectAll()
|
||||
})
|
||||
await snapshotCanvas(page)
|
||||
})
|
||||
|
||||
test('shape handles: line endpoints and hovered center handle', async ({ page }) => {
|
||||
// Capture the two states a user sees on a selected line: the idle state
|
||||
// where only the vertex handles at the start and end are visible, and
|
||||
// the hover state where the midpoint create handle becomes visible
|
||||
// with its hover halo.
|
||||
await page.evaluate(() => {
|
||||
editor.createShape({
|
||||
type: 'line',
|
||||
x: 200,
|
||||
y: 200,
|
||||
props: {
|
||||
dash: 'solid',
|
||||
points: {
|
||||
a1: { id: 'a1', index: 'a1' as IndexKey, x: 0, y: 0 },
|
||||
a2: { id: 'a2', index: 'a2' as IndexKey, x: 280, y: 280 },
|
||||
},
|
||||
},
|
||||
})
|
||||
editor.selectAll()
|
||||
})
|
||||
|
||||
await snapshotCanvas(page)
|
||||
|
||||
// Force-hover the midpoint 'create' handle so the screenshot is frame-
|
||||
// exact regardless of the browser's pointer coordinate rounding.
|
||||
await page.evaluate(() => {
|
||||
const shape = editor.getOnlySelectedShape()!
|
||||
const handles = editor.getShapeHandles(shape)!
|
||||
const midHandle = handles.find((h) => h.type === 'create')!
|
||||
editor.overlays.setHoveredOverlay(`handle:${shape.id}:${midHandle.id}`)
|
||||
})
|
||||
await snapshotCanvas(page)
|
||||
})
|
||||
|
||||
test('brush overlay', async ({ page }) => {
|
||||
await page.evaluate(() => {
|
||||
editor.updateInstanceState({ brush: { x: 180, y: 140, w: 320, h: 200 } })
|
||||
})
|
||||
await snapshotCanvas(page)
|
||||
})
|
||||
|
||||
test('zoom brush overlay', async ({ page }) => {
|
||||
await page.evaluate(() => {
|
||||
editor.updateInstanceState({ zoomBrush: { x: 180, y: 140, w: 320, h: 200 } })
|
||||
})
|
||||
await snapshotCanvas(page)
|
||||
})
|
||||
|
||||
test('snap indicator overlay: point snaps', async ({ page }) => {
|
||||
await page.evaluate(() => {
|
||||
editor.createShapes([
|
||||
{
|
||||
type: 'geo',
|
||||
x: 180,
|
||||
y: 160,
|
||||
props: { w: 100, h: 100, geo: 'rectangle', dash: 'solid', fill: 'none' },
|
||||
},
|
||||
{
|
||||
type: 'geo',
|
||||
x: 380,
|
||||
y: 160,
|
||||
props: { w: 100, h: 100, geo: 'rectangle', dash: 'solid', fill: 'none' },
|
||||
},
|
||||
])
|
||||
editor.snaps.setIndicators([
|
||||
{
|
||||
id: 'p-top',
|
||||
type: 'points',
|
||||
points: [
|
||||
{ x: 230, y: 160 },
|
||||
{ x: 430, y: 160 },
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'p-bottom',
|
||||
type: 'points',
|
||||
points: [
|
||||
{ x: 230, y: 260 },
|
||||
{ x: 430, y: 260 },
|
||||
],
|
||||
},
|
||||
])
|
||||
})
|
||||
await snapshotCanvas(page)
|
||||
})
|
||||
|
||||
test('scribble overlay', async ({ page }) => {
|
||||
await page.evaluate(async () => {
|
||||
const item = editor.scribbles.addScribble(
|
||||
{ color: 'accent', size: 20, opacity: 0.8, taper: true },
|
||||
'snapshot-scribble'
|
||||
)
|
||||
// Build a smooth arc of points. Advance the tick between each
|
||||
// addPoint so ScribbleManager moves `next` into `scribble.points`.
|
||||
const pts: Array<[number, number]> = []
|
||||
for (let i = 0; i <= 20; i++) {
|
||||
const t = i / 20
|
||||
const x = 200 + t * 320
|
||||
const y = 240 + Math.sin(t * Math.PI) * -60
|
||||
pts.push([x, y])
|
||||
}
|
||||
for (const [x, y] of pts) {
|
||||
editor.scribbles.addPoint(item.id, x, y, 0.5)
|
||||
editor.scribbles.tick(16)
|
||||
}
|
||||
editor.scribbles.complete(item.id)
|
||||
editor.scribbles.tick(16)
|
||||
})
|
||||
await snapshotCanvas(page)
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user