@tldraw/tlschema 4.2.2 → 4.2.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist-cjs/bindings/TLBaseBinding.js.map +2 -2
- package/dist-cjs/createTLSchema.js.map +2 -2
- package/dist-cjs/index.d.ts +71 -242
- package/dist-cjs/index.js +1 -4
- package/dist-cjs/index.js.map +2 -2
- package/dist-cjs/misc/TLOpacity.js +5 -1
- package/dist-cjs/misc/TLOpacity.js.map +2 -2
- package/dist-cjs/misc/TLRichText.js +1 -5
- package/dist-cjs/misc/TLRichText.js.map +2 -2
- package/dist-cjs/records/TLAsset.js.map +1 -1
- package/dist-cjs/records/TLBinding.js.map +2 -2
- package/dist-cjs/records/TLShape.js.map +2 -2
- package/dist-cjs/shapes/ShapeWithCrop.js.map +1 -1
- package/dist-cjs/shapes/TLArrowShape.js +13 -26
- package/dist-cjs/shapes/TLArrowShape.js.map +2 -2
- package/dist-cjs/shapes/TLBaseShape.js.map +2 -2
- package/dist-cjs/shapes/TLDrawShape.js +4 -37
- package/dist-cjs/shapes/TLDrawShape.js.map +2 -2
- package/dist-cjs/shapes/TLEmbedShape.js +0 -17
- package/dist-cjs/shapes/TLEmbedShape.js.map +2 -2
- package/dist-cjs/shapes/TLGeoShape.js +1 -12
- package/dist-cjs/shapes/TLGeoShape.js.map +2 -2
- package/dist-cjs/shapes/TLHighlightShape.js +2 -29
- package/dist-cjs/shapes/TLHighlightShape.js.map +2 -2
- package/dist-cjs/shapes/TLNoteShape.js +1 -12
- package/dist-cjs/shapes/TLNoteShape.js.map +2 -2
- package/dist-cjs/shapes/TLTextShape.js +1 -12
- package/dist-cjs/shapes/TLTextShape.js.map +2 -2
- package/dist-cjs/store-migrations.js +15 -15
- package/dist-cjs/store-migrations.js.map +2 -2
- package/dist-esm/bindings/TLBaseBinding.mjs.map +2 -2
- package/dist-esm/createTLSchema.mjs.map +2 -2
- package/dist-esm/index.d.mts +71 -242
- package/dist-esm/index.mjs +1 -5
- package/dist-esm/index.mjs.map +2 -2
- package/dist-esm/misc/TLOpacity.mjs +5 -1
- package/dist-esm/misc/TLOpacity.mjs.map +2 -2
- package/dist-esm/misc/TLRichText.mjs +1 -5
- package/dist-esm/misc/TLRichText.mjs.map +2 -2
- package/dist-esm/records/TLAsset.mjs.map +1 -1
- package/dist-esm/records/TLBinding.mjs.map +2 -2
- package/dist-esm/records/TLShape.mjs.map +2 -2
- package/dist-esm/shapes/TLArrowShape.mjs +13 -26
- package/dist-esm/shapes/TLArrowShape.mjs.map +2 -2
- package/dist-esm/shapes/TLBaseShape.mjs.map +2 -2
- package/dist-esm/shapes/TLDrawShape.mjs +4 -37
- package/dist-esm/shapes/TLDrawShape.mjs.map +2 -2
- package/dist-esm/shapes/TLEmbedShape.mjs +0 -17
- package/dist-esm/shapes/TLEmbedShape.mjs.map +2 -2
- package/dist-esm/shapes/TLGeoShape.mjs +1 -12
- package/dist-esm/shapes/TLGeoShape.mjs.map +2 -2
- package/dist-esm/shapes/TLHighlightShape.mjs +2 -29
- package/dist-esm/shapes/TLHighlightShape.mjs.map +2 -2
- package/dist-esm/shapes/TLNoteShape.mjs +1 -12
- package/dist-esm/shapes/TLNoteShape.mjs.map +2 -2
- package/dist-esm/shapes/TLTextShape.mjs +1 -12
- package/dist-esm/shapes/TLTextShape.mjs.map +2 -2
- package/dist-esm/store-migrations.mjs +15 -15
- package/dist-esm/store-migrations.mjs.map +2 -2
- package/package.json +8 -8
- package/src/__tests__/migrationTestUtils.ts +3 -9
- package/src/assets/TLBookmarkAsset.test.ts +96 -0
- package/src/assets/TLImageAsset.test.ts +213 -0
- package/src/assets/TLVideoAsset.test.ts +105 -0
- package/src/bindings/TLArrowBinding.test.ts +55 -0
- package/src/bindings/TLBaseBinding.ts +14 -25
- package/src/createTLSchema.ts +2 -8
- package/src/index.ts +0 -9
- package/src/migrations.test.ts +1 -149
- package/src/misc/TLOpacity.ts +5 -1
- package/src/misc/TLRichText.ts +1 -6
- package/src/misc/id-validator.test.ts +50 -0
- package/src/records/TLAsset.test.ts +234 -0
- package/src/records/TLAsset.ts +2 -2
- package/src/records/TLBinding.test.ts +22 -0
- package/src/records/TLBinding.ts +23 -65
- package/src/records/TLCamera.test.ts +19 -0
- package/src/records/TLDocument.test.ts +35 -0
- package/src/records/TLInstance.test.ts +201 -0
- package/src/records/TLPage.test.ts +110 -0
- package/src/records/TLPageState.test.ts +228 -0
- package/src/records/TLPointer.test.ts +63 -0
- package/src/records/TLPresence.test.ts +190 -0
- package/src/records/TLRecord.test.ts +70 -0
- package/src/records/TLShape.test.ts +232 -0
- package/src/records/TLShape.ts +5 -100
- package/src/shapes/ShapeWithCrop.test.ts +18 -0
- package/src/shapes/ShapeWithCrop.ts +2 -2
- package/src/shapes/TLArrowShape.test.ts +505 -0
- package/src/shapes/TLArrowShape.ts +14 -28
- package/src/shapes/TLBaseShape.test.ts +142 -0
- package/src/shapes/TLBaseShape.ts +10 -34
- package/src/shapes/TLBookmarkShape.test.ts +122 -0
- package/src/shapes/TLDrawShape.test.ts +177 -0
- package/src/shapes/TLDrawShape.ts +12 -59
- package/src/shapes/TLEmbedShape.test.ts +286 -0
- package/src/shapes/TLEmbedShape.ts +0 -17
- package/src/shapes/TLFrameShape.test.ts +71 -0
- package/src/shapes/TLGeoShape.test.ts +247 -0
- package/src/shapes/TLGeoShape.ts +1 -14
- package/src/shapes/TLGroupShape.test.ts +59 -0
- package/src/shapes/TLHighlightShape.test.ts +325 -0
- package/src/shapes/TLHighlightShape.ts +0 -37
- package/src/shapes/TLImageShape.test.ts +534 -0
- package/src/shapes/TLLineShape.test.ts +269 -0
- package/src/shapes/TLNoteShape.test.ts +1568 -0
- package/src/shapes/TLNoteShape.ts +1 -15
- package/src/shapes/TLTextShape.test.ts +407 -0
- package/src/shapes/TLTextShape.ts +2 -16
- package/src/shapes/TLVideoShape.test.ts +112 -0
- package/src/store-migrations.ts +16 -17
- package/src/styles/TLColorStyle.test.ts +439 -0
- package/dist-cjs/misc/b64Vecs.js +0 -224
- package/dist-cjs/misc/b64Vecs.js.map +0 -7
- package/dist-esm/misc/b64Vecs.mjs +0 -204
- package/dist-esm/misc/b64Vecs.mjs.map +0 -7
- package/src/misc/b64Vecs.ts +0 -308
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import { T } from '@tldraw/validate'
|
|
2
|
-
import { b64Vecs } from '../misc/b64Vecs'
|
|
3
2
|
import { createShapePropsMigrationIds, createShapePropsMigrationSequence } from '../records/TLShape'
|
|
4
3
|
import { RecordProps } from '../recordsWithProps'
|
|
5
4
|
import { DefaultColorStyle, TLDefaultColorStyle } from '../styles/TLColorStyle'
|
|
@@ -37,10 +36,6 @@ export interface TLHighlightShapeProps {
|
|
|
37
36
|
isPen: boolean
|
|
38
37
|
/** Scale factor applied to the highlight shape for display */
|
|
39
38
|
scale: number
|
|
40
|
-
/** Horizontal scale factor for lazy resize */
|
|
41
|
-
scaleX: number
|
|
42
|
-
/** Vertical scale factor for lazy resize */
|
|
43
|
-
scaleY: number
|
|
44
39
|
}
|
|
45
40
|
|
|
46
41
|
/**
|
|
@@ -89,7 +84,6 @@ export type TLHighlightShape = TLBaseShape<'highlight', TLHighlightShapeProps>
|
|
|
89
84
|
* const validatedProps = validator.validate(someHighlightProps)
|
|
90
85
|
* ```
|
|
91
86
|
*/
|
|
92
|
-
/** @public */
|
|
93
87
|
export const highlightShapeProps: RecordProps<TLHighlightShape> = {
|
|
94
88
|
color: DefaultColorStyle,
|
|
95
89
|
size: DefaultSizeStyle,
|
|
@@ -97,13 +91,10 @@ export const highlightShapeProps: RecordProps<TLHighlightShape> = {
|
|
|
97
91
|
isComplete: T.boolean,
|
|
98
92
|
isPen: T.boolean,
|
|
99
93
|
scale: T.nonZeroNumber,
|
|
100
|
-
scaleX: T.nonZeroFiniteNumber,
|
|
101
|
-
scaleY: T.nonZeroFiniteNumber,
|
|
102
94
|
}
|
|
103
95
|
|
|
104
96
|
const Versions = createShapePropsMigrationIds('highlight', {
|
|
105
97
|
AddScale: 1,
|
|
106
|
-
Base64: 2,
|
|
107
98
|
})
|
|
108
99
|
|
|
109
100
|
/**
|
|
@@ -131,33 +122,5 @@ export const highlightShapeMigrations = createShapePropsMigrationSequence({
|
|
|
131
122
|
delete props.scale
|
|
132
123
|
},
|
|
133
124
|
},
|
|
134
|
-
{
|
|
135
|
-
id: Versions.Base64,
|
|
136
|
-
up: (props) => {
|
|
137
|
-
props.segments = props.segments.map((segment: any) => {
|
|
138
|
-
return {
|
|
139
|
-
...segment,
|
|
140
|
-
// Only encode if points is an array (not already base64 string)
|
|
141
|
-
points:
|
|
142
|
-
typeof segment.points === 'string'
|
|
143
|
-
? segment.points
|
|
144
|
-
: b64Vecs.encodePoints(segment.points),
|
|
145
|
-
}
|
|
146
|
-
})
|
|
147
|
-
props.scaleX = props.scaleX ?? 1
|
|
148
|
-
props.scaleY = props.scaleY ?? 1
|
|
149
|
-
},
|
|
150
|
-
down: (props) => {
|
|
151
|
-
props.segments = props.segments.map((segment: any) => ({
|
|
152
|
-
...segment,
|
|
153
|
-
// Only decode if points is a string (not already VecModel[])
|
|
154
|
-
points: Array.isArray(segment.points)
|
|
155
|
-
? segment.points
|
|
156
|
-
: b64Vecs.decodePoints(segment.points),
|
|
157
|
-
}))
|
|
158
|
-
delete props.scaleX
|
|
159
|
-
delete props.scaleY
|
|
160
|
-
},
|
|
161
|
-
},
|
|
162
125
|
],
|
|
163
126
|
})
|
|
@@ -0,0 +1,534 @@
|
|
|
1
|
+
import { T } from '@tldraw/validate'
|
|
2
|
+
import { describe, expect, it } from 'vitest'
|
|
3
|
+
import { getTestMigration } from '../__tests__/migrationTestUtils'
|
|
4
|
+
import { TLAssetId } from '../records/TLAsset'
|
|
5
|
+
import { TLShapeCrop } from './ShapeWithCrop'
|
|
6
|
+
import { ImageShapeCrop, imageShapeProps, imageShapeVersions } from './TLImageShape'
|
|
7
|
+
|
|
8
|
+
describe('TLImageShape', () => {
|
|
9
|
+
describe('ImageShapeCrop validator', () => {
|
|
10
|
+
it('should validate valid crop data', () => {
|
|
11
|
+
const validCrop: TLShapeCrop = {
|
|
12
|
+
topLeft: { x: 0.1, y: 0.1 },
|
|
13
|
+
bottomRight: { x: 0.9, y: 0.9 },
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
expect(() => ImageShapeCrop.validate(validCrop)).not.toThrow()
|
|
17
|
+
const result = ImageShapeCrop.validate(validCrop)
|
|
18
|
+
expect(result.topLeft).toEqual({ x: 0.1, y: 0.1 })
|
|
19
|
+
expect(result.bottomRight).toEqual({ x: 0.9, y: 0.9 })
|
|
20
|
+
})
|
|
21
|
+
|
|
22
|
+
it('should validate crop data with isCircle flag', () => {
|
|
23
|
+
const cropWithCircle: TLShapeCrop = {
|
|
24
|
+
topLeft: { x: 0, y: 0 },
|
|
25
|
+
bottomRight: { x: 1, y: 1 },
|
|
26
|
+
isCircle: true,
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
expect(() => ImageShapeCrop.validate(cropWithCircle)).not.toThrow()
|
|
30
|
+
const result = ImageShapeCrop.validate(cropWithCircle)
|
|
31
|
+
expect(result.isCircle).toBe(true)
|
|
32
|
+
})
|
|
33
|
+
|
|
34
|
+
it('should validate crop data without isCircle flag', () => {
|
|
35
|
+
const cropWithoutCircle: TLShapeCrop = {
|
|
36
|
+
topLeft: { x: 0.25, y: 0.25 },
|
|
37
|
+
bottomRight: { x: 0.75, y: 0.75 },
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
expect(() => ImageShapeCrop.validate(cropWithoutCircle)).not.toThrow()
|
|
41
|
+
const result = ImageShapeCrop.validate(cropWithoutCircle)
|
|
42
|
+
expect(result.isCircle).toBeUndefined()
|
|
43
|
+
})
|
|
44
|
+
|
|
45
|
+
it('should validate crop data with explicit false isCircle', () => {
|
|
46
|
+
const cropWithFalseCircle: TLShapeCrop = {
|
|
47
|
+
topLeft: { x: 0, y: 0.5 },
|
|
48
|
+
bottomRight: { x: 1, y: 1 },
|
|
49
|
+
isCircle: false,
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
expect(() => ImageShapeCrop.validate(cropWithFalseCircle)).not.toThrow()
|
|
53
|
+
const result = ImageShapeCrop.validate(cropWithFalseCircle)
|
|
54
|
+
expect(result.isCircle).toBe(false)
|
|
55
|
+
})
|
|
56
|
+
|
|
57
|
+
it('should validate edge case coordinate values', () => {
|
|
58
|
+
const edgeCaseCrops = [
|
|
59
|
+
{
|
|
60
|
+
topLeft: { x: 0, y: 0 },
|
|
61
|
+
bottomRight: { x: 1, y: 1 },
|
|
62
|
+
},
|
|
63
|
+
{
|
|
64
|
+
topLeft: { x: 0.5, y: 0.5 },
|
|
65
|
+
bottomRight: { x: 0.5, y: 0.5 },
|
|
66
|
+
},
|
|
67
|
+
{
|
|
68
|
+
topLeft: { x: 0.001, y: 0.999 },
|
|
69
|
+
bottomRight: { x: 0.999, y: 0.001 },
|
|
70
|
+
},
|
|
71
|
+
]
|
|
72
|
+
|
|
73
|
+
edgeCaseCrops.forEach((crop, _index) => {
|
|
74
|
+
expect(() => ImageShapeCrop.validate(crop)).not.toThrow()
|
|
75
|
+
})
|
|
76
|
+
})
|
|
77
|
+
|
|
78
|
+
it('should reject invalid crop data structures', () => {
|
|
79
|
+
const invalidCrops = [
|
|
80
|
+
{}, // Missing required properties
|
|
81
|
+
{ topLeft: { x: 0.1, y: 0.1 } }, // Missing bottomRight
|
|
82
|
+
{ bottomRight: { x: 0.9, y: 0.9 } }, // Missing topLeft
|
|
83
|
+
{
|
|
84
|
+
topLeft: { x: 'invalid', y: 0.1 },
|
|
85
|
+
bottomRight: { x: 0.9, y: 0.9 },
|
|
86
|
+
}, // Invalid coordinate type
|
|
87
|
+
{
|
|
88
|
+
topLeft: { x: 0.1, y: 0.1 },
|
|
89
|
+
bottomRight: { x: 0.9, y: 'invalid' },
|
|
90
|
+
}, // Invalid coordinate type
|
|
91
|
+
{
|
|
92
|
+
topLeft: { x: 0.1 }, // Missing y
|
|
93
|
+
bottomRight: { x: 0.9, y: 0.9 },
|
|
94
|
+
},
|
|
95
|
+
{
|
|
96
|
+
topLeft: { x: 0.1, y: 0.1 },
|
|
97
|
+
bottomRight: { y: 0.9 }, // Missing x
|
|
98
|
+
},
|
|
99
|
+
{
|
|
100
|
+
topLeft: { x: 0.1, y: 0.1 },
|
|
101
|
+
bottomRight: { x: 0.9, y: 0.9 },
|
|
102
|
+
isCircle: 'not-boolean', // Invalid isCircle type
|
|
103
|
+
},
|
|
104
|
+
null,
|
|
105
|
+
undefined,
|
|
106
|
+
'not-an-object',
|
|
107
|
+
123,
|
|
108
|
+
[],
|
|
109
|
+
]
|
|
110
|
+
|
|
111
|
+
invalidCrops.forEach((crop) => {
|
|
112
|
+
expect(() => ImageShapeCrop.validate(crop)).toThrow()
|
|
113
|
+
})
|
|
114
|
+
})
|
|
115
|
+
})
|
|
116
|
+
|
|
117
|
+
describe('imageShapeMigrations - AddUrlProp migration', () => {
|
|
118
|
+
const { up, down } = getTestMigration(imageShapeVersions.AddUrlProp)
|
|
119
|
+
|
|
120
|
+
describe('AddUrlProp up migration', () => {
|
|
121
|
+
it('should add url property with empty string default', () => {
|
|
122
|
+
const oldRecord = {
|
|
123
|
+
id: 'shape:image1',
|
|
124
|
+
typeName: 'shape',
|
|
125
|
+
type: 'image',
|
|
126
|
+
x: 100,
|
|
127
|
+
y: 200,
|
|
128
|
+
rotation: 0,
|
|
129
|
+
index: 'a1',
|
|
130
|
+
parentId: 'page:main',
|
|
131
|
+
isLocked: false,
|
|
132
|
+
opacity: 1,
|
|
133
|
+
props: {
|
|
134
|
+
w: 400,
|
|
135
|
+
h: 300,
|
|
136
|
+
playing: true,
|
|
137
|
+
assetId: 'asset:image123',
|
|
138
|
+
},
|
|
139
|
+
meta: {},
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
const result = up(oldRecord)
|
|
143
|
+
expect(result.props.url).toBe('')
|
|
144
|
+
expect(result.props.w).toBe(400) // Preserve other props
|
|
145
|
+
expect(result.props.playing).toBe(true)
|
|
146
|
+
})
|
|
147
|
+
|
|
148
|
+
it('should preserve all existing properties during migration', () => {
|
|
149
|
+
const oldRecord = {
|
|
150
|
+
id: 'shape:image2',
|
|
151
|
+
props: {
|
|
152
|
+
w: 500,
|
|
153
|
+
h: 400,
|
|
154
|
+
playing: false,
|
|
155
|
+
assetId: null,
|
|
156
|
+
},
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
const result = up(oldRecord)
|
|
160
|
+
expect(result.props.url).toBe('')
|
|
161
|
+
expect(result.props.w).toBe(500)
|
|
162
|
+
expect(result.props.h).toBe(400)
|
|
163
|
+
expect(result.props.playing).toBe(false)
|
|
164
|
+
expect(result.props.assetId).toBeNull()
|
|
165
|
+
})
|
|
166
|
+
})
|
|
167
|
+
|
|
168
|
+
describe('AddUrlProp down migration', () => {
|
|
169
|
+
it('should be retired (no down migration)', () => {
|
|
170
|
+
expect(() => {
|
|
171
|
+
down({})
|
|
172
|
+
}).toThrow('Migration com.tldraw.shape.image/1 does not have a down function')
|
|
173
|
+
})
|
|
174
|
+
})
|
|
175
|
+
})
|
|
176
|
+
|
|
177
|
+
describe('imageShapeMigrations - AddCropProp migration', () => {
|
|
178
|
+
const { up, down } = getTestMigration(imageShapeVersions.AddCropProp)
|
|
179
|
+
|
|
180
|
+
describe('AddCropProp up migration', () => {
|
|
181
|
+
it('should add crop property with null default', () => {
|
|
182
|
+
const oldRecord = {
|
|
183
|
+
id: 'shape:image1',
|
|
184
|
+
props: {
|
|
185
|
+
w: 300,
|
|
186
|
+
h: 200,
|
|
187
|
+
playing: true,
|
|
188
|
+
url: 'https://example.com/image.jpg',
|
|
189
|
+
assetId: 'asset:image123',
|
|
190
|
+
},
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
const result = up(oldRecord)
|
|
194
|
+
expect(result.props.crop).toBeNull()
|
|
195
|
+
expect(result.props.w).toBe(300) // Preserve other props
|
|
196
|
+
expect(result.props.url).toBe('https://example.com/image.jpg')
|
|
197
|
+
})
|
|
198
|
+
|
|
199
|
+
it('should preserve all existing properties during migration', () => {
|
|
200
|
+
const oldRecord = {
|
|
201
|
+
id: 'shape:image2',
|
|
202
|
+
props: {
|
|
203
|
+
w: 400,
|
|
204
|
+
h: 300,
|
|
205
|
+
playing: false,
|
|
206
|
+
url: '',
|
|
207
|
+
assetId: null,
|
|
208
|
+
},
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
const result = up(oldRecord)
|
|
212
|
+
expect(result.props.crop).toBeNull()
|
|
213
|
+
expect(result.props.w).toBe(400)
|
|
214
|
+
expect(result.props.playing).toBe(false)
|
|
215
|
+
})
|
|
216
|
+
})
|
|
217
|
+
|
|
218
|
+
describe('AddCropProp down migration', () => {
|
|
219
|
+
it('should remove crop property', () => {
|
|
220
|
+
const newRecord = {
|
|
221
|
+
id: 'shape:image1',
|
|
222
|
+
props: {
|
|
223
|
+
w: 300,
|
|
224
|
+
h: 200,
|
|
225
|
+
playing: true,
|
|
226
|
+
url: 'https://example.com/image.jpg',
|
|
227
|
+
assetId: 'asset:image123',
|
|
228
|
+
crop: {
|
|
229
|
+
topLeft: { x: 0.1, y: 0.1 },
|
|
230
|
+
bottomRight: { x: 0.9, y: 0.9 },
|
|
231
|
+
},
|
|
232
|
+
},
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
const result = down(newRecord)
|
|
236
|
+
expect(result.props.crop).toBeUndefined()
|
|
237
|
+
expect(result.props.w).toBe(300) // Preserve other props
|
|
238
|
+
})
|
|
239
|
+
})
|
|
240
|
+
})
|
|
241
|
+
|
|
242
|
+
describe('imageShapeMigrations - MakeUrlsValid migration', () => {
|
|
243
|
+
const { up, down } = getTestMigration(imageShapeVersions.MakeUrlsValid)
|
|
244
|
+
|
|
245
|
+
describe('MakeUrlsValid up migration', () => {
|
|
246
|
+
it('should clear invalid URLs', () => {
|
|
247
|
+
const oldRecord = {
|
|
248
|
+
id: 'shape:image1',
|
|
249
|
+
props: {
|
|
250
|
+
w: 300,
|
|
251
|
+
h: 200,
|
|
252
|
+
playing: true,
|
|
253
|
+
url: 'invalid-url-format',
|
|
254
|
+
assetId: 'asset:image123',
|
|
255
|
+
crop: null,
|
|
256
|
+
},
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
const result = up(oldRecord)
|
|
260
|
+
expect(result.props.url).toBe('')
|
|
261
|
+
expect(result.props.w).toBe(300) // Preserve other props
|
|
262
|
+
})
|
|
263
|
+
|
|
264
|
+
it('should preserve valid URLs', () => {
|
|
265
|
+
const validUrls = [
|
|
266
|
+
'',
|
|
267
|
+
'https://example.com/image.jpg',
|
|
268
|
+
'http://test.com/photo.png',
|
|
269
|
+
'https://subdomain.example.com/path/image.gif',
|
|
270
|
+
]
|
|
271
|
+
|
|
272
|
+
validUrls.forEach((url) => {
|
|
273
|
+
const oldRecord = {
|
|
274
|
+
id: 'shape:image1',
|
|
275
|
+
props: {
|
|
276
|
+
w: 300,
|
|
277
|
+
h: 200,
|
|
278
|
+
playing: true,
|
|
279
|
+
url,
|
|
280
|
+
assetId: 'asset:image123',
|
|
281
|
+
crop: null,
|
|
282
|
+
},
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
const result = up(oldRecord)
|
|
286
|
+
expect(result.props.url).toBe(url)
|
|
287
|
+
})
|
|
288
|
+
})
|
|
289
|
+
|
|
290
|
+
it('should preserve all other properties during migration', () => {
|
|
291
|
+
const oldRecord = {
|
|
292
|
+
id: 'shape:image1',
|
|
293
|
+
props: {
|
|
294
|
+
w: 400,
|
|
295
|
+
h: 300,
|
|
296
|
+
playing: false,
|
|
297
|
+
url: 'not-valid-url',
|
|
298
|
+
assetId: null,
|
|
299
|
+
crop: {
|
|
300
|
+
topLeft: { x: 0.2, y: 0.2 },
|
|
301
|
+
bottomRight: { x: 0.8, y: 0.8 },
|
|
302
|
+
},
|
|
303
|
+
},
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
const result = up(oldRecord)
|
|
307
|
+
expect(result.props.url).toBe('')
|
|
308
|
+
expect(result.props.w).toBe(400)
|
|
309
|
+
expect(result.props.playing).toBe(false)
|
|
310
|
+
expect(result.props.crop).toEqual({
|
|
311
|
+
topLeft: { x: 0.2, y: 0.2 },
|
|
312
|
+
bottomRight: { x: 0.8, y: 0.8 },
|
|
313
|
+
})
|
|
314
|
+
})
|
|
315
|
+
})
|
|
316
|
+
|
|
317
|
+
describe('MakeUrlsValid down migration', () => {
|
|
318
|
+
it('should be a no-op migration', () => {
|
|
319
|
+
const newRecord = {
|
|
320
|
+
id: 'shape:image1',
|
|
321
|
+
props: {
|
|
322
|
+
w: 300,
|
|
323
|
+
h: 200,
|
|
324
|
+
playing: true,
|
|
325
|
+
url: 'https://example.com/image.jpg',
|
|
326
|
+
assetId: 'asset:image123',
|
|
327
|
+
crop: null,
|
|
328
|
+
},
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
const result = down(newRecord)
|
|
332
|
+
expect(result).toEqual(newRecord)
|
|
333
|
+
})
|
|
334
|
+
})
|
|
335
|
+
})
|
|
336
|
+
|
|
337
|
+
describe('imageShapeMigrations - AddFlipProps migration', () => {
|
|
338
|
+
const { up, down } = getTestMigration(imageShapeVersions.AddFlipProps)
|
|
339
|
+
|
|
340
|
+
describe('AddFlipProps up migration', () => {
|
|
341
|
+
it('should add flipX and flipY properties with false defaults', () => {
|
|
342
|
+
const oldRecord = {
|
|
343
|
+
id: 'shape:image1',
|
|
344
|
+
props: {
|
|
345
|
+
w: 300,
|
|
346
|
+
h: 200,
|
|
347
|
+
playing: true,
|
|
348
|
+
url: 'https://example.com/image.jpg',
|
|
349
|
+
assetId: 'asset:image123',
|
|
350
|
+
crop: null,
|
|
351
|
+
},
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
const result = up(oldRecord)
|
|
355
|
+
expect(result.props.flipX).toBe(false)
|
|
356
|
+
expect(result.props.flipY).toBe(false)
|
|
357
|
+
expect(result.props.w).toBe(300) // Preserve other props
|
|
358
|
+
})
|
|
359
|
+
|
|
360
|
+
it('should preserve all existing properties during migration', () => {
|
|
361
|
+
const oldRecord = {
|
|
362
|
+
id: 'shape:image2',
|
|
363
|
+
props: {
|
|
364
|
+
w: 400,
|
|
365
|
+
h: 300,
|
|
366
|
+
playing: false,
|
|
367
|
+
url: '',
|
|
368
|
+
assetId: null,
|
|
369
|
+
crop: {
|
|
370
|
+
topLeft: { x: 0, y: 0 },
|
|
371
|
+
bottomRight: { x: 1, y: 1 },
|
|
372
|
+
isCircle: true,
|
|
373
|
+
},
|
|
374
|
+
},
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
const result = up(oldRecord)
|
|
378
|
+
expect(result.props.flipX).toBe(false)
|
|
379
|
+
expect(result.props.flipY).toBe(false)
|
|
380
|
+
expect(result.props.w).toBe(400)
|
|
381
|
+
expect(result.props.crop?.isCircle).toBe(true)
|
|
382
|
+
})
|
|
383
|
+
})
|
|
384
|
+
|
|
385
|
+
describe('AddFlipProps down migration', () => {
|
|
386
|
+
it('should remove flipX and flipY properties', () => {
|
|
387
|
+
const newRecord = {
|
|
388
|
+
id: 'shape:image1',
|
|
389
|
+
props: {
|
|
390
|
+
w: 300,
|
|
391
|
+
h: 200,
|
|
392
|
+
playing: true,
|
|
393
|
+
url: 'https://example.com/image.jpg',
|
|
394
|
+
assetId: 'asset:image123',
|
|
395
|
+
crop: null,
|
|
396
|
+
flipX: true,
|
|
397
|
+
flipY: false,
|
|
398
|
+
},
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
const result = down(newRecord)
|
|
402
|
+
expect(result.props.flipX).toBeUndefined()
|
|
403
|
+
expect(result.props.flipY).toBeUndefined()
|
|
404
|
+
expect(result.props.w).toBe(300) // Preserve other props
|
|
405
|
+
})
|
|
406
|
+
})
|
|
407
|
+
})
|
|
408
|
+
|
|
409
|
+
describe('imageShapeMigrations - AddAltText migration', () => {
|
|
410
|
+
const { up, down } = getTestMigration(imageShapeVersions.AddAltText)
|
|
411
|
+
|
|
412
|
+
describe('AddAltText up migration', () => {
|
|
413
|
+
it('should add altText property with empty string default', () => {
|
|
414
|
+
const oldRecord = {
|
|
415
|
+
id: 'shape:image1',
|
|
416
|
+
props: {
|
|
417
|
+
w: 300,
|
|
418
|
+
h: 200,
|
|
419
|
+
playing: true,
|
|
420
|
+
url: 'https://example.com/image.jpg',
|
|
421
|
+
assetId: 'asset:image123',
|
|
422
|
+
crop: null,
|
|
423
|
+
flipX: false,
|
|
424
|
+
flipY: true,
|
|
425
|
+
},
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
const result = up(oldRecord)
|
|
429
|
+
expect(result.props.altText).toBe('')
|
|
430
|
+
expect(result.props.flipY).toBe(true) // Preserve other props
|
|
431
|
+
})
|
|
432
|
+
|
|
433
|
+
it('should preserve all existing properties during migration', () => {
|
|
434
|
+
const oldRecord = {
|
|
435
|
+
id: 'shape:image2',
|
|
436
|
+
props: {
|
|
437
|
+
w: 500,
|
|
438
|
+
h: 400,
|
|
439
|
+
playing: false,
|
|
440
|
+
url: '',
|
|
441
|
+
assetId: null,
|
|
442
|
+
crop: {
|
|
443
|
+
topLeft: { x: 0.25, y: 0.25 },
|
|
444
|
+
bottomRight: { x: 0.75, y: 0.75 },
|
|
445
|
+
},
|
|
446
|
+
flipX: true,
|
|
447
|
+
flipY: false,
|
|
448
|
+
},
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
const result = up(oldRecord)
|
|
452
|
+
expect(result.props.altText).toBe('')
|
|
453
|
+
expect(result.props.flipX).toBe(true)
|
|
454
|
+
expect(result.props.crop).toEqual({
|
|
455
|
+
topLeft: { x: 0.25, y: 0.25 },
|
|
456
|
+
bottomRight: { x: 0.75, y: 0.75 },
|
|
457
|
+
})
|
|
458
|
+
})
|
|
459
|
+
})
|
|
460
|
+
|
|
461
|
+
describe('AddAltText down migration', () => {
|
|
462
|
+
it('should remove altText property', () => {
|
|
463
|
+
const newRecord = {
|
|
464
|
+
id: 'shape:image1',
|
|
465
|
+
props: {
|
|
466
|
+
w: 300,
|
|
467
|
+
h: 200,
|
|
468
|
+
playing: true,
|
|
469
|
+
url: 'https://example.com/image.jpg',
|
|
470
|
+
assetId: 'asset:image123',
|
|
471
|
+
crop: null,
|
|
472
|
+
flipX: false,
|
|
473
|
+
flipY: false,
|
|
474
|
+
altText: 'Sample image description',
|
|
475
|
+
},
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
const result = down(newRecord)
|
|
479
|
+
expect(result.props.altText).toBeUndefined()
|
|
480
|
+
expect(result.props.flipX).toBe(false) // Preserve other props
|
|
481
|
+
})
|
|
482
|
+
})
|
|
483
|
+
})
|
|
484
|
+
|
|
485
|
+
describe('integration tests', () => {
|
|
486
|
+
it('should work with complete image shape record validation', () => {
|
|
487
|
+
const completeValidator = T.object({
|
|
488
|
+
id: T.string,
|
|
489
|
+
typeName: T.literal('shape'),
|
|
490
|
+
type: T.literal('image'),
|
|
491
|
+
x: T.number,
|
|
492
|
+
y: T.number,
|
|
493
|
+
rotation: T.number,
|
|
494
|
+
index: T.string,
|
|
495
|
+
parentId: T.string,
|
|
496
|
+
isLocked: T.boolean,
|
|
497
|
+
opacity: T.number,
|
|
498
|
+
props: T.object(imageShapeProps),
|
|
499
|
+
meta: T.jsonValue,
|
|
500
|
+
})
|
|
501
|
+
|
|
502
|
+
const validImageShape = {
|
|
503
|
+
id: 'shape:image123',
|
|
504
|
+
typeName: 'shape' as const,
|
|
505
|
+
type: 'image' as const,
|
|
506
|
+
x: 100,
|
|
507
|
+
y: 200,
|
|
508
|
+
rotation: 0.5,
|
|
509
|
+
index: 'a1',
|
|
510
|
+
parentId: 'page:main',
|
|
511
|
+
isLocked: false,
|
|
512
|
+
opacity: 0.8,
|
|
513
|
+
props: {
|
|
514
|
+
w: 400,
|
|
515
|
+
h: 300,
|
|
516
|
+
playing: true,
|
|
517
|
+
url: 'https://example.com/image.jpg',
|
|
518
|
+
assetId: 'asset:image123' as TLAssetId,
|
|
519
|
+
crop: {
|
|
520
|
+
topLeft: { x: 0.1, y: 0.1 },
|
|
521
|
+
bottomRight: { x: 0.9, y: 0.9 },
|
|
522
|
+
isCircle: false,
|
|
523
|
+
},
|
|
524
|
+
flipX: false,
|
|
525
|
+
flipY: true,
|
|
526
|
+
altText: 'Sample image',
|
|
527
|
+
},
|
|
528
|
+
meta: { custom: 'data' },
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
expect(() => completeValidator.validate(validImageShape)).not.toThrow()
|
|
532
|
+
})
|
|
533
|
+
})
|
|
534
|
+
})
|