Files
upstream-tldraw/apps/examples/e2e/tests/export-snapshots.spec.tsx
upstream-mirror a774ac673a
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
snapshot of https://github.com/tldraw/tldraw.git (main, 2026-09-23; blobs > 131072B omitted)
2026-09-23 16:12:52 +05:00

1226 lines
29 KiB
TypeScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import assert from 'assert'
import { rename, writeFile } from 'fs/promises'
/* eslint-disable tldraw/no-internal-imports, react/no-string-refs */
import { Page, expect } from '@playwright/test'
import { ReactElement } from 'react'
import {
DefaultColorStyle,
DefaultDashStyle,
DefaultFillStyle,
DefaultFontStyle,
Editor,
GeoShapeGeoStyle,
TLAsset,
TLBindingCreate,
TLShapePartial,
degreesToRadians,
mapObjectMapValues,
mockUniqueId,
toRichText,
} from 'tldraw'
import { TL, createDrawSegments, shapesFromJsx } from 'tldraw/src/test/test-jsx'
import { EndToEndApi } from '../../src/misc/EndToEndApi'
import test, { ApiFixture } from '../fixtures/fixtures'
import { hardResetEditor, setup } from '../shared-e2e'
import {
convexDrawShape,
frameContent,
heyDrawShape,
manAsset,
manCrop,
manCropAsCircle,
richText,
richTextForArrow,
} from './export-snapshots-data'
declare const editor: Editor
declare const tldrawApi: EndToEndApi
let nextNanoId = 0
mockUniqueId(() => `mock-${nextNanoId++}`)
interface Snapshots {
[name: string]: { [row: string]: { [testCase: string]: ReactElement } }
}
const snapshots: Snapshots = {
'Text rendering': {
'geo text': {
'leading line breaks': (
<TL.geo dash="solid" richText={toRichText('\n\n\n\n\n\ntext')} w={100} h={30} />
),
'trailing line breaks': (
<TL.geo dash="solid" richText={toRichText('text\n\n\n\n\n\n')} w={100} h={30} />
),
'mixed RTL': (
<TL.geo
dash="solid"
richText={toRichText('unicode is cool!\nكتابة باللغة العرب!')}
w={300}
h={300}
/>
),
'overflowing text with small scale': (
<TL.geo
dash="solid"
richText={toRichText('\n\n\n\n\n\ntext')}
w={100}
h={30}
scale={0.25}
/>
),
'overflowing text with large scale': (
<TL.geo dash="solid" richText={toRichText('\n\n\n\n\n\ntext')} w={200} h={30} scale={2} />
),
'rich text': <TL.geo dash="solid" richText={richText} align="start" w={300} h={300} />,
},
},
// A dedicated group (its own snapshot image) for RTL export alignment (#7720): `start` must
// flush lines to the right and `end` to the left for RTL — matching the canvas, not the
// reversed physical mapping that used to ship. The LTR pair is the mirror-image control.
'Text alignment': {
rtl: {
'start (flush right)': (
<TL.text
richText={toRichText('مرحباً بكم في تطبيق تلدرو\nسطر قصير')}
font="draw"
size="l"
textAlign="start"
color="black"
/>
),
'end (flush left)': (
<TL.text
richText={toRichText('مرحباً بكم في تطبيق تلدرو\nسطر قصير')}
font="draw"
size="l"
textAlign="end"
color="black"
/>
),
},
ltr: {
'start (flush left)': (
<TL.text
richText={toRichText('The quick brown fox\nshort')}
font="draw"
size="l"
textAlign="start"
color="black"
/>
),
'end (flush right)': (
<TL.text
richText={toRichText('The quick brown fox\nshort')}
font="draw"
size="l"
textAlign="end"
color="black"
/>
),
},
},
Fills: Object.fromEntries(
DefaultFillStyle.values.map((fill) => [
`fill=${fill}`,
{
geo: <TL.geo dash="solid" fill={fill} color="green" w={100} h={100} />,
arrow: (
<TL.arrow
fill={fill}
color="light-green"
arrowheadStart="square"
arrowheadEnd="dot"
start={{ x: 0, y: 0 }}
end={{ x: 100, y: 100 }}
bend={20}
/>
),
draw: (
<TL.draw
fill={fill}
color="light-violet"
segments={createDrawSegments(
[
[{ x: 0, y: 0 }],
[
{ x: 0, y: 0 },
{ x: 100, y: 0 },
],
[
{ x: 100, y: 0 },
{ x: 0, y: 100 },
],
[
{ x: 0, y: 100 },
{ x: 100, y: 100 },
],
[
{ x: 100, y: 100 },
{ x: 0, y: 0 },
],
],
'straight'
)}
isClosed
isComplete
/>
),
},
])
),
Fonts: Object.fromEntries(
DefaultFontStyle.values.map((font) => [
`font=${font}`,
{
geo: <TL.geo dash="solid" font={font} richText={toRichText('test')} w={100} h={100} />,
arrow: (
<TL.arrow
font={font}
color="blue"
fill="solid"
arrowheadStart="square"
arrowheadEnd="arrow"
start={{ x: 0, y: 0 }}
end={{ x: 100, y: 100 }}
bend={20}
richText={toRichText('test')}
/>
),
note: <TL.note font={font} color="violet" richText={toRichText('test')} />,
text: <TL.text font={font} color="red" richText={toRichText('test')} />,
},
])
),
'Geo shapes': Object.fromEntries(
GeoShapeGeoStyle.values.map((geoStyle, y) => [
geoStyle,
Object.fromEntries(
DefaultDashStyle.values.map((dashStyle, x) => [
dashStyle,
<TL.geo
key={x}
w={100}
h={100}
fill="solid"
color={DefaultColorStyle.values[(x + y) % DefaultColorStyle.values.length]}
geo={geoStyle}
dash={dashStyle}
/>,
])
),
])
),
// Directional geo shapes must mirror their geometry (not just their position) when flipped,
// and the label must stay upright/unmirrored (#9373). These rows lock in the mirrored path
// and the readable label for each flip combination across the SVG export.
'Geo flips': Object.fromEntries(
(
[
'rhombus',
'rhombus-2',
'trapezoid',
'triangle',
'arrow-right',
'arrow-left',
'arrow-up',
'arrow-down',
] as const
).map((geoStyle) => [
geoStyle,
{
none: (
<TL.geo
geo={geoStyle}
dash="solid"
fill="solid"
color="blue"
w={100}
h={100}
richText={toRichText('flip')}
/>
),
flipX: (
<TL.geo
geo={geoStyle}
dash="solid"
fill="solid"
color="blue"
w={100}
h={100}
flipX
richText={toRichText('flip')}
/>
),
flipY: (
<TL.geo
geo={geoStyle}
dash="solid"
fill="solid"
color="blue"
w={100}
h={100}
flipY
richText={toRichText('flip')}
/>
),
flipXY: (
<TL.geo
geo={geoStyle}
dash="solid"
fill="solid"
color="blue"
w={100}
h={100}
flipX
flipY
richText={toRichText('flip')}
/>
),
},
])
),
Frames: {
'': {
empty: <TL.frame w={100} h={100} />,
labelled: <TL.frame w={100} h={100} name="test" />,
content: (
<TL.frame w={100} h={100} name="test">
{frameContent}
</TL.frame>
),
nested: (
<TL.frame w={200} h={100} name="tilted" rotation={degreesToRadians(10)}>
<TL.geo dash="solid" x={-10} y={-10} w={220} h={120} fill="solid" color="light-blue" />
{frameContent}
<TL.frame
x={140}
y={15}
w={200}
h={100}
name="ttiilltteedd"
rotation={degreesToRadians(10)}
>
<TL.geo dash="solid" x={-10} y={-10} w={220} h={120} fill="solid" color="light-green" />
{frameContent}
</TL.frame>
</TL.frame>
),
},
'label rotation': Object.fromEntries(
[10, 70, 130, 190, 150, 310].map((rotation, i) => [
`${rotation}°`,
<TL.frame key={i} w={100} h={100} name="test" rotation={degreesToRadians(rotation)}>
{frameContent}
</TL.frame>,
])
),
},
Images: {
Uncropped: {
'no asset': <TL.image w={100} h={200} />,
asset: <TL.image w={100} h={200} assetId={manAsset} />,
flipX: <TL.image w={100} h={200} assetId={manAsset} flipX />,
flipY: <TL.image w={100} h={200} assetId={manAsset} flipY />,
flipXY: <TL.image w={100} h={200} assetId={manAsset} flipX flipY />,
rotated: <TL.image w={100} h={200} assetId={manAsset} rotation={degreesToRadians(45)} />,
},
Cropped: {
'no asset': <TL.image w={100} h={100} crop={manCrop} />,
asset: <TL.image w={100} h={100} assetId={manAsset} crop={manCrop} />,
flipX: <TL.image w={100} h={100} assetId={manAsset} flipX crop={manCrop} />,
flipY: <TL.image w={100} h={100} assetId={manAsset} flipY crop={manCrop} />,
flipXY: <TL.image w={100} h={100} assetId={manAsset} flipX flipY crop={manCrop} />,
withCircle: (
<TL.image w={100} h={100} assetId={manAsset} flipX flipY crop={manCropAsCircle} />
),
rotated: (
<TL.image
w={100}
h={100}
assetId={manAsset}
rotation={degreesToRadians(45)}
crop={manCrop}
/>
),
},
},
Bookmarks: {
'not rotated': {
full: (
<TL.bookmark
url="https://www.tldraw.com"
assetId={
<TL.asset.bookmark
title="title"
description="description"
image="/man.png"
favicon="/heart-icon.svg"
src="https://www.tldraw.com"
/>
}
/>
),
long: (
<TL.bookmark
url="https://www.tldraw.com"
assetId={
<TL.asset.bookmark
title="Add a tldraw canvas to your React app in just 5 minutes. You can use the tldraw SDK to craft infinite canvas experiences for the web. It's perfect for collaborative whiteboards but you can use it for lots of other things, too."
description="At this point, you have a complete working single-user whiteboard. To add support for multiple users collaborating in realtime, you can use the tldraw sync extension library."
image="/man.png"
favicon="/heart-icon.svg"
src="https://www.tldraw.com"
/>
}
/>
),
'no image': (
<TL.bookmark
url="https://www.tldraw.com"
assetId={
<TL.asset.bookmark
title="title"
description="description"
image=""
favicon="/man.png"
src="https://www.tldraw.com"
/>
}
/>
),
'no favicon': (
<TL.bookmark
url="https://www.tldraw.com"
assetId={
<TL.asset.bookmark
title="title"
description="description"
image="/man.png"
favicon=""
src="https://www.tldraw.com"
/>
}
/>
),
'no meta': (
<TL.bookmark
url="https://www.tldraw.com"
assetId={
<TL.asset.bookmark
title=""
description=""
image="/man.png"
favicon="/man.png"
src="https://www.tldraw.com"
/>
}
/>
),
empty: (
<TL.bookmark
url="https://www.tldraw.com"
assetId={
<TL.asset.bookmark
title=""
description=""
image=""
favicon=""
src="https://www.tldraw.com"
/>
}
/>
),
},
'rotated (note shadow)': {
'30°': (
<TL.bookmark
rotation={degreesToRadians(30)}
url="https://www.tldraw.com"
assetId={
<TL.asset.bookmark
title="title"
description="description"
image="/tldraw.png"
favicon="/heart-icon.svg"
src="https://www.tldraw.com"
/>
}
/>
),
'135°': (
<TL.bookmark
rotation={degreesToRadians(135)}
url="https://www.tldraw.com"
assetId={
<TL.asset.bookmark
title="title"
description="description"
image="/tldraw.png"
favicon="/heart-icon.svg"
src="https://www.tldraw.com"
/>
}
/>
),
'255°': (
<TL.bookmark
rotation={degreesToRadians(255)}
url="https://www.tldraw.com"
assetId={
<TL.asset.bookmark
title="title"
description="description"
image="/tldraw.png"
favicon="/heart-icon.svg"
src="https://www.tldraw.com"
/>
}
/>
),
},
},
'Foreign objects': {
'HTML & CSS': {
simple: (
<TL.html
html="Hello, <span>world</span>!"
css="#self {color: green;} #self span { color: red; }"
/>
),
pseudos: (
<TL.html
html="Hello, <span>world</span>!"
css={`
#self {
color: green;
}
#self span {
color: red;
}
#self::before {
content: 'before ';
color: blue;
}
#self::after {
content: ' after';
color: purple;
}
`}
/>
),
transforms: (
<TL.html
html="Hello, <span>world</span>!"
css={`
#self {
color: green;
transform: rotate(30deg) scale(1.5);
}
#self span {
color: red;
display: inline-block;
transform: rotate(-30deg) scale(0.5);
}
`}
/>
),
'@font-face': (
<TL.html
html="Hello, <span>world</span>!"
css={`
@font-face {
font-family: 'font_#self';
src: url('/ComicMono.woff');
}
#self span {
color: red;
font-family: 'font_#self';
font-size: 20px;
}
`}
/>
),
'custom elements': (
<TL.html
w={200}
h={200}
html={`
Hello!
<custom-element>
<ul slot="list">
<li class="selected">Apples</li>
<li>Pears</li>
<li>Bananas</li>
</ul>
<p slot="choice" data-name="Apples">
A common, sweet, crunchy fruit, usually green or yellow in color.
</p>
<p data-name="Pears">
A fairly common, sweet, usually green fruit, usually softer than Apples.
</p>
<p data-name="Bananas">A long, curved, yellow fruit, with a fairly gentle flavor.</p>
</custom-element>
`}
css={`
#self {
background-color: lightcoral;
}
#self custom-element {
background-color: plum;
}
#self .selected {
font-size: 20px;
}
`}
/>
),
},
Embeds: {
Video: <TL.html w={300} h={169} html='<video src="/bonk.webm" width="300" height="169" />' />,
Image: <TL.html w={100} h={200} html='<img src="/man.png" width="100" />' />,
Background: (
<TL.html
w={200}
h={200}
html="Hello, world!"
css={`
#self {
width: 100%;
height: 100%;
background: url('/man.png') repeat center center / 15px 30px;
display: flex;
justify-content: center;
align-items: center;
}
`}
/>
),
},
},
Arrows: {
'': {
Arrow1: (
<TL.arrow
start={{ x: 200, y: 0 }}
end={{ x: 200, y: 300 }}
size="xl"
arrowheadStart="arrow"
arrowheadEnd="triangle"
/>
),
Arrow2: (
<TL.arrow
start={{ x: 250, y: 0 }}
end={{ x: 250, y: 300 }}
size="xl"
arrowheadStart="square"
arrowheadEnd="dot"
/>
),
Arrow3: (
<TL.arrow
start={{ x: 300, y: 0 }}
end={{ x: 300, y: 300 }}
size="xl"
arrowheadStart="pipe"
arrowheadEnd="diamond"
richText={richTextForArrow}
/>
),
Arrow4: (
<TL.arrow
start={{ x: 350, y: 0 }}
end={{ x: 350, y: 300 }}
size="xl"
arrowheadStart="inverted"
arrowheadEnd="bar"
/>
),
Arrow5: (
<TL.arrow
start={{ x: 400, y: 0 }}
end={{ x: 400, y: 300 }}
size="xl"
arrowheadStart="none"
arrowheadEnd="none"
/>
),
},
},
Regressions: {
'#5020': {
'scaled text within a frame': (
<TL.frame w={300} h={200}>
<TL.text richText={toRichText('the text')} x={-30} y={0} />
<TL.text richText={toRichText('the text')} x={-60} y={50} scale={2} />
<TL.text richText={toRichText('the text')} x={-90} y={100} scale={3} />
</TL.frame>
),
},
'#6391': {
'Circles at 45deg from each other': (
<>
<TL.geo geo="ellipse" ref="a" w={40} h={40} />
<TL.geo geo="ellipse" ref="b" w={40} h={40} x={80} y={80} />
<TL.arrow>
<TL.binding.arrow to="a" terminal="start" />
<TL.binding.arrow to="b" terminal="end" />
</TL.arrow>
<TL.geo geo="ellipse" ref="c" w={40} h={40} x={200} />
<TL.geo geo="ellipse" ref="d" w={40} h={40} x={280} y={80} />
<TL.arrow bend={10}>
<TL.binding.arrow to="c" terminal="start" />
<TL.binding.arrow to="d" terminal="end" />
</TL.arrow>
</>
),
},
},
'Weird elbow arrows': {
'Small segments, contained': {
A: (
<>
<TL.geo dash="solid" w={150} h={150} ref="a" />
<TL.geo dash="solid" w={30} h={30} x={15} y={15} ref="b" />
<TL.arrow dash="solid" kind="elbow">
<TL.binding.arrow to="a" terminal="start" />
<TL.binding.arrow
to="b"
terminal="end"
isPrecise={true}
normalizedAnchor={{ x: 1, y: 0.5 }}
snap="edge-point"
/>
</TL.arrow>
</>
),
B: (
<>
<TL.geo dash="solid" w={150} h={150} ref="a" />
<TL.geo dash="solid" w={30} h={30} x={25} y={15} ref="b" />
<TL.arrow dash="solid" kind="elbow">
<TL.binding.arrow to="a" terminal="start" />
<TL.binding.arrow
to="b"
terminal="end"
isPrecise={true}
normalizedAnchor={{ x: 1, y: 0.5 }}
snap="edge-point"
/>
</TL.arrow>
</>
),
'Exact match': (
<>
<TL.geo dash="solid" w={150} h={150} ref="a" />
<TL.geo dash="solid" w={30} h={30} x={45} y={15} ref="b" />
<TL.arrow dash="solid" kind="elbow">
<TL.binding.arrow to="a" terminal="start" />
<TL.binding.arrow
to="b"
terminal="end"
isPrecise={true}
normalizedAnchor={{ x: 1, y: 0.5 }}
snap="edge-point"
/>
</TL.arrow>
</>
),
C: (
<>
<TL.geo dash="solid" w={150} h={150} ref="a" />
<TL.geo dash="solid" w={30} h={30} x={50} y={15} ref="b" />
<TL.arrow dash="solid" kind="elbow">
<TL.binding.arrow to="a" terminal="start" />
<TL.binding.arrow
to="b"
terminal="end"
isPrecise={true}
normalizedAnchor={{ x: 1, y: 0.5 }}
snap="edge-point"
/>
</TL.arrow>
</>
),
D: (
<>
<TL.geo dash="solid" w={150} h={150} ref="a" />
<TL.geo dash="solid" w={30} h={30} x={70} y={15} ref="b" />
<TL.arrow dash="solid" kind="elbow">
<TL.binding.arrow to="a" terminal="start" />
<TL.binding.arrow
to="b"
terminal="end"
isPrecise={true}
normalizedAnchor={{ x: 1, y: 0.5 }}
snap="edge-point"
/>
</TL.arrow>
</>
),
},
'Draw shapes': {
'Hey, near': (
<>
{heyDrawShape}
<TL.arrow dash="solid" kind="elbow" x={-50} y={20}>
<TL.binding.arrow
to="hey"
terminal="end"
isPrecise={true}
normalizedAnchor={{ x: 0.09381928931000422, y: 0.21685530426808908 }}
snap="edge"
/>
</TL.arrow>
</>
),
'Hey, far': (
<>
{heyDrawShape}
<TL.arrow dash="solid" kind="elbow" x={-50} y={20}>
<TL.binding.arrow
to="hey"
terminal="end"
isPrecise={true}
normalizedAnchor={{ x: 0.9994593548811798, y: 0.3061474422212521 }}
snap="edge"
/>
</TL.arrow>
</>
),
'Hey, within': (
<>
{heyDrawShape}
<TL.arrow dash="solid" kind="elbow" x={-50} y={20}>
<TL.binding.arrow
to="hey"
terminal="end"
isPrecise={true}
normalizedAnchor={{ x: 0.38733552172499264, y: 0.5318288846695827 }}
snap="edge"
/>
</TL.arrow>
</>
),
'Convex, far': (
<>
{convexDrawShape}
<TL.arrow dash="solid" kind="elbow" x={-50} y={20}>
<TL.binding.arrow
to="convex"
terminal="end"
isPrecise={true}
normalizedAnchor={{ x: 0.9919587061623889, y: 0.4908357026818453 }}
snap="edge"
/>
</TL.arrow>
</>
),
'Convex, edge': (
<>
{convexDrawShape}
<TL.arrow dash="solid" kind="elbow" x={-50} y={20}>
<TL.binding.arrow
to="convex"
terminal="end"
isPrecise={true}
normalizedAnchor={{ x: 0.319774673658207, y: 0.9983952589963239 }}
snap="edge"
/>
</TL.arrow>
</>
),
},
'Self-bound': {
'Draw shape,\nsame-edge': (
<>
{convexDrawShape}
<TL.arrow dash="solid" kind="elbow">
<TL.binding.arrow
to="convex"
terminal="start"
isPrecise={true}
normalizedAnchor={{ x: 0.7187653636319187, y: 0.07579613336130757 }}
snap="edge"
/>
<TL.binding.arrow
to="convex"
terminal="end"
isPrecise={true}
normalizedAnchor={{ x: 0.04948958738107296, y: 0 }}
snap="edge"
/>
</TL.arrow>
</>
),
'Box,\nsame edge': (
<>
<TL.geo dash="solid" w={100} h={100} ref="a" />
<TL.arrow dash="solid" kind="elbow">
<TL.binding.arrow
to="a"
terminal="start"
isPrecise={true}
normalizedAnchor={{ x: 0.2, y: 1 }}
snap="edge"
/>
<TL.binding.arrow
to="a"
terminal="end"
isPrecise={true}
normalizedAnchor={{ x: 0.8, y: 1 }}
snap="edge"
/>
</TL.arrow>
</>
),
'Triangle,\nopposites': (
<>
<TL.geo dash="solid" w={100} h={100} geo="triangle" ref="a" />
<TL.arrow dash="solid" kind="elbow">
<TL.binding.arrow
to="a"
terminal="start"
isPrecise={true}
normalizedAnchor={{ x: 0.75, y: 0.5 }}
snap="edge"
/>
<TL.binding.arrow
to="a"
terminal="end"
isPrecise={true}
normalizedAnchor={{ x: 0.25, y: 0.5 }}
snap="edge"
/>
</TL.arrow>
</>
),
Outside: (
<>
<TL.geo dash="solid" w={100} h={100} geo="triangle" ref="a" />
<TL.arrow dash="solid" kind="elbow">
<TL.binding.arrow
to="a"
terminal="start"
isPrecise={true}
normalizedAnchor={{ x: 1, y: 0.5 }}
snap="edge"
/>
<TL.binding.arrow
to="a"
terminal="end"
isPrecise={true}
normalizedAnchor={{ x: 0, y: 0.5 }}
snap="edge"
/>
</TL.arrow>
</>
),
},
'Regressions 1': {
'ENG-3332 ✅': (
<>
<TL.geo dash="solid" w={100} h={100} ref="a" />
<TL.geo dash="solid" w={30} h={30} x={60} y={10} ref="b" />
<TL.arrow dash="solid" kind="elbow">
<TL.binding.arrow
to="a"
terminal="start"
isPrecise={true}
normalizedAnchor={{ x: 0.5, y: 0 }}
snap="edge"
/>
<TL.binding.arrow
to="b"
terminal="end"
isPrecise={true}
normalizedAnchor={{ x: 0, y: 0.5 }}
snap="edge"
/>
</TL.arrow>
</>
),
'ENG-3333 🚫': (
<>
<TL.geo dash="solid" w={50} h={50} ref="box" />
<TL.text richText={toRichText('text')} x={70} y={30} ref="text" />
<TL.arrow dash="solid" kind="elbow">
<TL.binding.arrow
to="box"
terminal="start"
isPrecise={true}
normalizedAnchor={{ x: 1, y: 0.5 }}
snap="edge"
/>
<TL.binding.arrow
to="text"
terminal="end"
isPrecise={true}
normalizedAnchor={{ x: 1, y: 0.5 }}
snap="edge"
/>
</TL.arrow>
</>
),
'ENG-3335 🚫': (
<>
<TL.geo dash="solid" w={100} h={100} ref="a" />
<TL.geo dash="solid" w={30} h={30} x={-30} y={70} ref="b" />
<TL.arrow dash="solid" kind="elbow" arrowheadStart="arrow" arrowheadEnd="arrow">
<TL.binding.arrow
to="a"
terminal="start"
isPrecise={true}
normalizedAnchor={{ x: 0, y: 0.5 }}
snap="edge"
/>
<TL.binding.arrow
to="b"
terminal="end"
isPrecise={true}
normalizedAnchor={{ x: 0, y: 0.5 }}
snap="edge"
/>
</TL.arrow>
</>
),
'ENG-3336 ✅': (
// this one isn't actually an elbow arrow issue but was introduced by elbow arrows
// work so is still included here.
<>
<TL.text richText={toRichText('one')} ref="a" />
<TL.text richText={toRichText('two')} y={50} ref="b" />
<TL.arrow dash="solid" kind="arc" arrowheadStart="arrow" arrowheadEnd="arrow" bend={10}>
<TL.binding.arrow to="a" terminal="start" />
<TL.binding.arrow to="b" terminal="end" />
</TL.arrow>
</>
),
'ENG-3342 ✅': (
<>
<TL.geo dash="solid" w={50} h={50} ref="a" />
<TL.geo dash="solid" w={50} h={50} x={40} y={-10} ref="b" />
<TL.arrow dash="solid" kind="elbow">
<TL.binding.arrow
to="a"
terminal="start"
isPrecise={true}
normalizedAnchor={{ x: 1, y: 0.5 }}
snap="edge"
/>
<TL.binding.arrow
to="b"
terminal="end"
isPrecise={true}
normalizedAnchor={{ x: 0.5, y: 0 }}
snap="edge"
/>
</TL.arrow>
</>
),
},
'Regressions 2': {
'ENG-3366 ✅': (
<>
<TL.geo dash="solid" w={30} h={30} ref="a" />
<TL.geo dash="solid" w={30} h={30} x={80} y={10} ref="b" />
<TL.arrow dash="solid" kind="elbow">
<TL.binding.arrow to="a" terminal="start" />
<TL.binding.arrow
to="b"
terminal="end"
isPrecise={true}
normalizedAnchor={{ x: 0, y: 0.5 }}
snap="edge"
/>
</TL.arrow>
</>
),
'ENG-3371 ✅': (
<>
<TL.text ref="text" richText={toRichText('hi')} />
<TL.arrow dash="solid" kind="elbow" end={{ x: 70, y: 30 }}>
<TL.binding.arrow to="text" terminal="start" />
</TL.arrow>
</>
),
'ENG-3375 ✅': (
<>
{heyDrawShape}
<TL.arrow dash="solid" kind="elbow">
<TL.binding.arrow
to="hey"
terminal="start"
isPrecise={true}
normalizedAnchor={{ x: 0.09381928931000422, y: 0.21685530426808908 }}
snap="edge"
/>
<TL.binding.arrow
to="hey"
terminal="end"
isPrecise={true}
normalizedAnchor={{ x: 0.38733552172499264, y: 0.5318288846695827 }}
snap="edge"
/>
</TL.arrow>
</>
),
},
},
}
interface SnapshotWithoutJsx {
[row: string]: {
[testCase: string]: { shapes: TLShapePartial[]; assets: TLAsset[]; bindings: TLBindingCreate[] }
}
}
test.describe('Export snapshots', () => {
const snapshotsToTest = Object.entries(snapshots)
test.beforeEach(async ({ page, context, api }) => {
const url = page.url()
if (!url.includes('end-to-end')) {
await setup({ page, context } as any)
} else {
await hardResetEditor(page)
}
// These snapshots lay shapes out by their measured bounds, and text is measured from
// the loaded font - so load the fonts before anything is measured. Otherwise a shape
// can be measured with fallback-font metrics, which shifts the layout and the export's
// size and makes the screenshot diff flaky.
await api.preloadFonts()
})
for (const [name, snapshotWithJsx] of snapshotsToTest) {
for (const colorScheme of ['light', 'dark'] as const) {
test(`${name} (${colorScheme})`, async ({ page, api }) => {
nextNanoId = 0
const snapshot: SnapshotWithoutJsx = mapObjectMapValues(snapshotWithJsx, (i, row) =>
mapObjectMapValues(row, (j, testCase) => {
const { shapes, assets, bindings } = shapesFromJsx(testCase, `${i}-${j}`)
return { shapes, assets, bindings }
})
)
await page.evaluate(
({
colorScheme,
name,
snapshot,
}: {
colorScheme: 'light' | 'dark'
name: string
snapshot: SnapshotWithoutJsx
}) => {
editor.user.updateUserPreferences({ colorScheme })
editor
.updateInstanceState({ exportBackground: true })
.selectAll()
.deleteShapes(editor.getSelectedShapeIds())
const titleId = tldrawApi.createShapeId()
editor.createShape({
id: titleId,
type: 'text',
x: 0,
y: 0,
props: { richText: tldrawApi.toRichText(name), font: 'mono', size: 'xl' },
})
let y = editor.getShapePageBounds(titleId)!.maxY + 30
for (const [rowName, testCases] of Object.entries(snapshot)) {
const rowTitleId = tldrawApi.createShapeId()
editor.createShape({
id: rowTitleId,
type: 'text',
x: 0,
y,
props: { richText: tldrawApi.toRichText(rowName), font: 'mono', size: 'm' },
})
y = editor.getShapePageBounds(rowTitleId)!.maxY + 20
let x = 0
let bottom = y
for (const [testCaseName, { shapes, assets, bindings }] of Object.entries(
testCases
)) {
const testCaseTitleId = tldrawApi.createShapeId()
editor.createShape({
id: testCaseTitleId,
type: 'text',
x,
y,
props: {
richText: tldrawApi.toRichText(testCaseName),
font: 'mono',
size: 's',
},
})
const testCastTitleBounds = editor.getShapePageBounds(testCaseTitleId)!
editor.createAssets(assets)
editor.createShapes(shapes)
editor.createBindings(bindings)
const topLevelShapeIds = shapes
.filter((shape) => !shape.parentId)
.map((shape) => shape.id)
editor.setSelectedShapes(topLevelShapeIds)
let bounds = editor.getSelectionPageBounds()!
editor.nudgeShapes(topLevelShapeIds, {
x: x - bounds.minX,
y: testCastTitleBounds.maxY + 25 - bounds.minY,
})
bounds = editor.getSelectionPageBounds()!
x = Math.max(testCastTitleBounds.maxX, bounds.maxX) + 60
bottom = Math.max(bottom, bounds.maxY)
}
y = bottom + 40
}
tldrawApi.markAllArrowBindings()
editor.selectAll()
},
{ colorScheme, name, snapshot } as any
)
await snapshotTest(page, api)
})
}
}
async function snapshotTest(page: Page, api: ApiFixture) {
const downloadAndSnapshot = page.waitForEvent('download').then(async (download) => {
const path = (await download.path()) as string
assert(path)
await rename(path, path + '.svg')
await writeFile(
path + '.html',
`
<!DOCTYPE html>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<img src="${path}.svg" />
`,
'utf-8'
)
await page.goto(`file://${path}.html`)
const clip = await page.$eval('img', (img) => img.getBoundingClientRect())
await expect(page).toHaveScreenshot({
omitBackground: true,
clip,
fullPage: true,
})
})
await api.exportAsSvg()
await downloadAndSnapshot
}
})