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,160 @@
import test, { expect, Page } from '@playwright/test'
import { Editor } from 'tldraw'
import { setup } from '../shared-e2e'
declare const editor: Editor
// Regression coverage for #7720: RTL text alignment was reversed in PNG/SVG exports — `start`
// rendered the text flush-left and `end` flush-right, the opposite of how the canvas (and the
// logical `text-align` the editor uses) lays RTL text out. The hardcoded physical
// `.tl-text-wrapper[data-align] { text-align: left|right }` rules that caused it were removed in
// #8410 (the theme-system refactor), which moved alignment to inline logical `text-align`; this
// test locks that in so the physical mapping can't sneak back.
//
// It exports a two-line shape (a long line over a short line) and checks which edge the short line
// flushes to by scanning painted pixels per line-band — fully self-relative, so it doesn't depend
// on the (fallback) font that renders Arabic.
interface LineBands {
imageWidth: number
long: { left: number; right: number }
short: { left: number; right: number }
}
// Export a two-paragraph (long, short) autosize text shape and return the painted ink extents of
// the top (long) and bottom (short) lines.
async function measureLineBands(
page: Page,
opts: { long: string; short: string; textAlign: 'start' | 'end' }
): Promise<LineBands> {
return await page.evaluate(
async ({ long, short, textAlign }) => {
const ed = editor as any
ed.selectAll().deleteShapes(ed.getSelectedShapeIds())
ed.createShape({
id: 'shape:align',
type: 'text',
x: 0,
y: 0,
props: {
richText: {
type: 'doc',
content: [
{ type: 'paragraph', content: [{ type: 'text', text: long }] },
{ type: 'paragraph', content: [{ type: 'text', text: short }] },
],
},
font: 'draw',
size: 'xl',
autoSize: true,
textAlign,
color: 'black',
},
})
await ed.fonts.loadRequiredFontsForCurrentPage()
await ed.getContainerDocument().fonts.ready
const { url } = await ed.toImageDataUrl(['shape:align'], {
format: 'png',
background: false,
padding: 0,
scale: 2,
})
const img = new Image()
await new Promise((res, rej) => {
img.onload = res
img.onerror = rej
img.src = url
})
const w = img.naturalWidth
const h = img.naturalHeight
const cv = ed.getContainerDocument().createElement('canvas')
cv.width = w
cv.height = h
const ctx = cv.getContext('2d', { willReadFrequently: true })
ctx.drawImage(img, 0, 0)
const data = ctx.getImageData(0, 0, w, h).data
// Scan a horizontal band [y0, y1) for the left/right extent of painted ink. The two lines
// stack, so the top ~45% is the long line and the bottom ~45% is the short line; the
// middle is skipped to keep ascenders/descenders from bleeding across the boundary.
function band(y0: number, y1: number) {
let left = Infinity
let right = -Infinity
for (let y = y0; y < y1; y++) {
for (let x = 0; x < w; x++) {
if (data[(y * w + x) * 4 + 3] > 10) {
if (x < left) left = x
if (x > right) right = x
}
}
}
return { left, right }
}
return {
imageWidth: w,
long: band(0, Math.floor(h * 0.45)),
short: band(Math.ceil(h * 0.55), h),
}
},
{ long: opts.long, short: opts.short, textAlign: opts.textAlign }
)
}
const ARABIC_LONG = 'مرحباً بكم في تطبيق تلدرو الرائع'
const ARABIC_SHORT = 'سطر قصير'
const LATIN_LONG = 'The quick brown fox jumps over'
const LATIN_SHORT = 'short'
// Edges align (same flush side) within a few px; the indent on the other side is large (the lines
// differ a lot in length), so a generous gap keeps the direction check unambiguous.
const FLUSH_TOL = 6
const MIN_INDENT = 30
test.describe('RTL text export alignment (#7720)', () => {
test.beforeEach(setup)
test('RTL start aligns lines flush-right', async ({ page }) => {
const { long, short } = await measureLineBands(page, {
long: ARABIC_LONG,
short: ARABIC_SHORT,
textAlign: 'start',
})
// start === right for RTL: the short line's right edge meets the long line's, and it's
// indented from the left.
expect(Math.abs(short.right - long.right)).toBeLessThanOrEqual(FLUSH_TOL)
expect(short.left - long.left).toBeGreaterThan(MIN_INDENT)
})
test('RTL end aligns lines flush-left', async ({ page }) => {
const { long, short } = await measureLineBands(page, {
long: ARABIC_LONG,
short: ARABIC_SHORT,
textAlign: 'end',
})
// end === left for RTL.
expect(Math.abs(short.left - long.left)).toBeLessThanOrEqual(FLUSH_TOL)
expect(long.right - short.right).toBeGreaterThan(MIN_INDENT)
})
test('LTR start aligns lines flush-left', async ({ page }) => {
const { long, short } = await measureLineBands(page, {
long: LATIN_LONG,
short: LATIN_SHORT,
textAlign: 'start',
})
expect(Math.abs(short.left - long.left)).toBeLessThanOrEqual(FLUSH_TOL)
expect(long.right - short.right).toBeGreaterThan(MIN_INDENT)
})
test('LTR end aligns lines flush-right', async ({ page }) => {
const { long, short } = await measureLineBands(page, {
long: LATIN_LONG,
short: LATIN_SHORT,
textAlign: 'end',
})
expect(Math.abs(short.right - long.right)).toBeLessThanOrEqual(FLUSH_TOL)
expect(short.left - long.left).toBeGreaterThan(MIN_INDENT)
})
})