@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.
Files changed (117) hide show
  1. package/dist-cjs/bindings/TLBaseBinding.js.map +2 -2
  2. package/dist-cjs/createTLSchema.js.map +2 -2
  3. package/dist-cjs/index.d.ts +71 -242
  4. package/dist-cjs/index.js +1 -4
  5. package/dist-cjs/index.js.map +2 -2
  6. package/dist-cjs/misc/TLOpacity.js +5 -1
  7. package/dist-cjs/misc/TLOpacity.js.map +2 -2
  8. package/dist-cjs/misc/TLRichText.js +1 -5
  9. package/dist-cjs/misc/TLRichText.js.map +2 -2
  10. package/dist-cjs/records/TLAsset.js.map +1 -1
  11. package/dist-cjs/records/TLBinding.js.map +2 -2
  12. package/dist-cjs/records/TLShape.js.map +2 -2
  13. package/dist-cjs/shapes/ShapeWithCrop.js.map +1 -1
  14. package/dist-cjs/shapes/TLArrowShape.js +13 -26
  15. package/dist-cjs/shapes/TLArrowShape.js.map +2 -2
  16. package/dist-cjs/shapes/TLBaseShape.js.map +2 -2
  17. package/dist-cjs/shapes/TLDrawShape.js +4 -37
  18. package/dist-cjs/shapes/TLDrawShape.js.map +2 -2
  19. package/dist-cjs/shapes/TLEmbedShape.js +0 -17
  20. package/dist-cjs/shapes/TLEmbedShape.js.map +2 -2
  21. package/dist-cjs/shapes/TLGeoShape.js +1 -12
  22. package/dist-cjs/shapes/TLGeoShape.js.map +2 -2
  23. package/dist-cjs/shapes/TLHighlightShape.js +2 -29
  24. package/dist-cjs/shapes/TLHighlightShape.js.map +2 -2
  25. package/dist-cjs/shapes/TLNoteShape.js +1 -12
  26. package/dist-cjs/shapes/TLNoteShape.js.map +2 -2
  27. package/dist-cjs/shapes/TLTextShape.js +1 -12
  28. package/dist-cjs/shapes/TLTextShape.js.map +2 -2
  29. package/dist-cjs/store-migrations.js +15 -15
  30. package/dist-cjs/store-migrations.js.map +2 -2
  31. package/dist-esm/bindings/TLBaseBinding.mjs.map +2 -2
  32. package/dist-esm/createTLSchema.mjs.map +2 -2
  33. package/dist-esm/index.d.mts +71 -242
  34. package/dist-esm/index.mjs +1 -5
  35. package/dist-esm/index.mjs.map +2 -2
  36. package/dist-esm/misc/TLOpacity.mjs +5 -1
  37. package/dist-esm/misc/TLOpacity.mjs.map +2 -2
  38. package/dist-esm/misc/TLRichText.mjs +1 -5
  39. package/dist-esm/misc/TLRichText.mjs.map +2 -2
  40. package/dist-esm/records/TLAsset.mjs.map +1 -1
  41. package/dist-esm/records/TLBinding.mjs.map +2 -2
  42. package/dist-esm/records/TLShape.mjs.map +2 -2
  43. package/dist-esm/shapes/TLArrowShape.mjs +13 -26
  44. package/dist-esm/shapes/TLArrowShape.mjs.map +2 -2
  45. package/dist-esm/shapes/TLBaseShape.mjs.map +2 -2
  46. package/dist-esm/shapes/TLDrawShape.mjs +4 -37
  47. package/dist-esm/shapes/TLDrawShape.mjs.map +2 -2
  48. package/dist-esm/shapes/TLEmbedShape.mjs +0 -17
  49. package/dist-esm/shapes/TLEmbedShape.mjs.map +2 -2
  50. package/dist-esm/shapes/TLGeoShape.mjs +1 -12
  51. package/dist-esm/shapes/TLGeoShape.mjs.map +2 -2
  52. package/dist-esm/shapes/TLHighlightShape.mjs +2 -29
  53. package/dist-esm/shapes/TLHighlightShape.mjs.map +2 -2
  54. package/dist-esm/shapes/TLNoteShape.mjs +1 -12
  55. package/dist-esm/shapes/TLNoteShape.mjs.map +2 -2
  56. package/dist-esm/shapes/TLTextShape.mjs +1 -12
  57. package/dist-esm/shapes/TLTextShape.mjs.map +2 -2
  58. package/dist-esm/store-migrations.mjs +15 -15
  59. package/dist-esm/store-migrations.mjs.map +2 -2
  60. package/package.json +8 -8
  61. package/src/__tests__/migrationTestUtils.ts +3 -9
  62. package/src/assets/TLBookmarkAsset.test.ts +96 -0
  63. package/src/assets/TLImageAsset.test.ts +213 -0
  64. package/src/assets/TLVideoAsset.test.ts +105 -0
  65. package/src/bindings/TLArrowBinding.test.ts +55 -0
  66. package/src/bindings/TLBaseBinding.ts +14 -25
  67. package/src/createTLSchema.ts +2 -8
  68. package/src/index.ts +0 -9
  69. package/src/migrations.test.ts +1 -149
  70. package/src/misc/TLOpacity.ts +5 -1
  71. package/src/misc/TLRichText.ts +1 -6
  72. package/src/misc/id-validator.test.ts +50 -0
  73. package/src/records/TLAsset.test.ts +234 -0
  74. package/src/records/TLAsset.ts +2 -2
  75. package/src/records/TLBinding.test.ts +22 -0
  76. package/src/records/TLBinding.ts +23 -65
  77. package/src/records/TLCamera.test.ts +19 -0
  78. package/src/records/TLDocument.test.ts +35 -0
  79. package/src/records/TLInstance.test.ts +201 -0
  80. package/src/records/TLPage.test.ts +110 -0
  81. package/src/records/TLPageState.test.ts +228 -0
  82. package/src/records/TLPointer.test.ts +63 -0
  83. package/src/records/TLPresence.test.ts +190 -0
  84. package/src/records/TLRecord.test.ts +70 -0
  85. package/src/records/TLShape.test.ts +232 -0
  86. package/src/records/TLShape.ts +5 -100
  87. package/src/shapes/ShapeWithCrop.test.ts +18 -0
  88. package/src/shapes/ShapeWithCrop.ts +2 -2
  89. package/src/shapes/TLArrowShape.test.ts +505 -0
  90. package/src/shapes/TLArrowShape.ts +14 -28
  91. package/src/shapes/TLBaseShape.test.ts +142 -0
  92. package/src/shapes/TLBaseShape.ts +10 -34
  93. package/src/shapes/TLBookmarkShape.test.ts +122 -0
  94. package/src/shapes/TLDrawShape.test.ts +177 -0
  95. package/src/shapes/TLDrawShape.ts +12 -59
  96. package/src/shapes/TLEmbedShape.test.ts +286 -0
  97. package/src/shapes/TLEmbedShape.ts +0 -17
  98. package/src/shapes/TLFrameShape.test.ts +71 -0
  99. package/src/shapes/TLGeoShape.test.ts +247 -0
  100. package/src/shapes/TLGeoShape.ts +1 -14
  101. package/src/shapes/TLGroupShape.test.ts +59 -0
  102. package/src/shapes/TLHighlightShape.test.ts +325 -0
  103. package/src/shapes/TLHighlightShape.ts +0 -37
  104. package/src/shapes/TLImageShape.test.ts +534 -0
  105. package/src/shapes/TLLineShape.test.ts +269 -0
  106. package/src/shapes/TLNoteShape.test.ts +1568 -0
  107. package/src/shapes/TLNoteShape.ts +1 -15
  108. package/src/shapes/TLTextShape.test.ts +407 -0
  109. package/src/shapes/TLTextShape.ts +2 -16
  110. package/src/shapes/TLVideoShape.test.ts +112 -0
  111. package/src/store-migrations.ts +16 -17
  112. package/src/styles/TLColorStyle.test.ts +439 -0
  113. package/dist-cjs/misc/b64Vecs.js +0 -224
  114. package/dist-cjs/misc/b64Vecs.js.map +0 -7
  115. package/dist-esm/misc/b64Vecs.mjs +0 -204
  116. package/dist-esm/misc/b64Vecs.mjs.map +0 -7
  117. package/src/misc/b64Vecs.ts +0 -308
@@ -142,7 +142,6 @@ const Versions = createShapePropsMigrationIds('note', {
142
142
  AddScale: 7,
143
143
  AddLabelColor: 8,
144
144
  AddRichText: 9,
145
- AddRichTextAttrs: 10,
146
145
  })
147
146
 
148
147
  /**
@@ -157,8 +156,7 @@ export { Versions as noteShapeVersions }
157
156
  * Migration sequence for note shapes. Handles schema evolution over time by defining
158
157
  * how to upgrade and downgrade note shape data between different versions. Includes
159
158
  * migrations for URL properties, text alignment changes, vertical alignment addition,
160
- * font size adjustments, scaling support, label color, the transition from plain text to rich text,
161
- * and support for attrs property on richText.
159
+ * font size adjustments, scaling support, label color, and the transition from plain text to rich text.
162
160
  *
163
161
  * @public
164
162
  */
@@ -253,17 +251,5 @@ export const noteShapeMigrations = createShapePropsMigrationSequence({
253
251
  // delete props.richText
254
252
  // },
255
253
  },
256
- {
257
- id: Versions.AddRichTextAttrs,
258
- up: (_props) => {
259
- // noop - attrs is optional so old records are valid
260
- },
261
- down: (props) => {
262
- // Remove attrs from richText when migrating down
263
- if (props.richText && 'attrs' in props.richText) {
264
- delete props.richText.attrs
265
- }
266
- },
267
- },
268
254
  ],
269
255
  })
@@ -0,0 +1,407 @@
1
+ import { T } from '@tldraw/validate'
2
+ import { describe, expect, it } from 'vitest'
3
+ import { getTestMigration } from '../__tests__/migrationTestUtils'
4
+ import { TLRichText, toRichText } from '../misc/TLRichText'
5
+ import { DefaultColorStyle } from '../styles/TLColorStyle'
6
+ import { DefaultFontStyle } from '../styles/TLFontStyle'
7
+ import { DefaultSizeStyle } from '../styles/TLSizeStyle'
8
+ import { DefaultTextAlignStyle } from '../styles/TLTextAlignStyle'
9
+ import { textShapeMigrations, textShapeProps, textShapeVersions } from './TLTextShape'
10
+
11
+ describe('TLTextShape', () => {
12
+ describe('textShapeProps validation schema', () => {
13
+ it('should validate using comprehensive object validator', () => {
14
+ const fullValidator = T.object(textShapeProps)
15
+
16
+ const validPropsObject = {
17
+ color: 'red' as const,
18
+ size: 's' as const,
19
+ font: 'mono' as const,
20
+ textAlign: 'end' as const,
21
+ w: 250,
22
+ richText: toRichText('Complete validation test') as TLRichText,
23
+ scale: 0.8,
24
+ autoSize: true,
25
+ }
26
+
27
+ expect(() => fullValidator.validate(validPropsObject)).not.toThrow()
28
+ const result = fullValidator.validate(validPropsObject)
29
+ expect(result).toEqual(validPropsObject)
30
+ })
31
+
32
+ it('should validate width as nonZeroNumber', () => {
33
+ // Valid non-zero positive numbers
34
+ const validWidths = [0.1, 1, 50, 100, 1000, 0.001]
35
+
36
+ validWidths.forEach((w) => {
37
+ expect(() => textShapeProps.w.validate(w)).not.toThrow()
38
+ })
39
+
40
+ // Invalid widths (zero, negative numbers, and non-numbers)
41
+ const invalidWidths = [0, -1, -0.1, 'not-number', null, undefined, {}, [], true, false]
42
+
43
+ invalidWidths.forEach((w) => {
44
+ expect(() => textShapeProps.w.validate(w)).toThrow()
45
+ })
46
+ })
47
+
48
+ it('should validate scale as nonZeroNumber', () => {
49
+ // Valid non-zero positive numbers
50
+ const validScales = [0.1, 0.5, 1, 1.5, 2, 10]
51
+
52
+ validScales.forEach((scale) => {
53
+ expect(() => textShapeProps.scale.validate(scale)).not.toThrow()
54
+ })
55
+
56
+ // Invalid scales (zero, negative numbers, and non-numbers)
57
+ const invalidScales = [0, -0.5, -1, -2, 'not-number', null, undefined, {}, [], true, false]
58
+
59
+ invalidScales.forEach((scale) => {
60
+ expect(() => textShapeProps.scale.validate(scale)).toThrow()
61
+ })
62
+ })
63
+
64
+ it('should use correct default style validators', () => {
65
+ // Verify that the props schema uses the expected style validators
66
+ expect(textShapeProps.color).toBe(DefaultColorStyle)
67
+ expect(textShapeProps.size).toBe(DefaultSizeStyle)
68
+ expect(textShapeProps.font).toBe(DefaultFontStyle)
69
+ expect(textShapeProps.textAlign).toBe(DefaultTextAlignStyle)
70
+ })
71
+
72
+ it('should use correct primitive validators', () => {
73
+ // Check that non-style properties use correct T validators
74
+ expect(textShapeProps.w).toBe(T.nonZeroNumber)
75
+ expect(textShapeProps.scale).toBe(T.nonZeroNumber)
76
+ expect(textShapeProps.autoSize).toBe(T.boolean)
77
+ })
78
+ })
79
+
80
+ describe('textShapeVersions', () => {
81
+ it('should have all expected migration versions', () => {
82
+ const expectedVersions: Array<keyof typeof textShapeVersions> = [
83
+ 'RemoveJustify',
84
+ 'AddTextAlign',
85
+ 'AddRichText',
86
+ ]
87
+
88
+ expectedVersions.forEach((version) => {
89
+ expect(textShapeVersions[version]).toBeDefined()
90
+ expect(typeof textShapeVersions[version]).toBe('string')
91
+ })
92
+ })
93
+ })
94
+
95
+ describe('textShapeMigrations', () => {
96
+ it('should have migrations for all version IDs', () => {
97
+ const migrationIds = textShapeMigrations.sequence
98
+ .filter((migration) => 'id' in migration)
99
+ .map((migration) => ('id' in migration ? migration.id : null))
100
+ .filter(Boolean)
101
+
102
+ const versionIds = Object.values(textShapeVersions)
103
+
104
+ versionIds.forEach((versionId) => {
105
+ expect(migrationIds).toContain(versionId)
106
+ })
107
+ })
108
+ })
109
+
110
+ describe('textShapeMigrations - RemoveJustify migration', () => {
111
+ const { up, down } = getTestMigration(textShapeVersions.RemoveJustify)
112
+
113
+ describe('RemoveJustify up migration', () => {
114
+ it('should convert justify alignment to start', () => {
115
+ const oldRecord = {
116
+ id: 'shape:text1',
117
+ props: {
118
+ color: 'black',
119
+ font: 'draw',
120
+ size: 'm',
121
+ align: 'justify',
122
+ w: 200,
123
+ text: 'Test text',
124
+ scale: 1,
125
+ autoSize: true,
126
+ },
127
+ }
128
+
129
+ const result = up(oldRecord)
130
+ expect(result.props.align).toBe('start')
131
+ expect(result.props.color).toBe('black') // Preserve other props
132
+ expect(result.props.text).toBe('Test text')
133
+ })
134
+
135
+ it('should preserve non-justify alignments', () => {
136
+ const alignments = ['start', 'middle', 'end']
137
+
138
+ alignments.forEach((align) => {
139
+ const oldRecord = {
140
+ id: 'shape:text1',
141
+ props: {
142
+ color: 'red',
143
+ font: 'sans',
144
+ size: 'l',
145
+ align,
146
+ w: 300,
147
+ text: 'Aligned text',
148
+ scale: 1,
149
+ autoSize: false,
150
+ },
151
+ }
152
+
153
+ const result = up(oldRecord)
154
+ expect(result.props.align).toBe(align)
155
+ })
156
+ })
157
+
158
+ it('should preserve all other properties during migration', () => {
159
+ const oldRecord = {
160
+ id: 'shape:text1',
161
+ props: {
162
+ color: 'blue',
163
+ font: 'serif',
164
+ size: 'xl',
165
+ align: 'justify',
166
+ w: 400,
167
+ text: 'Justified text becomes start aligned',
168
+ scale: 1.5,
169
+ autoSize: false,
170
+ },
171
+ }
172
+
173
+ const result = up(oldRecord)
174
+ expect(result.props.align).toBe('start')
175
+ expect(result.props.color).toBe('blue')
176
+ expect(result.props.font).toBe('serif')
177
+ expect(result.props.size).toBe('xl')
178
+ expect(result.props.w).toBe(400)
179
+ expect(result.props.text).toBe('Justified text becomes start aligned')
180
+ expect(result.props.scale).toBe(1.5)
181
+ expect(result.props.autoSize).toBe(false)
182
+ })
183
+ })
184
+
185
+ describe('RemoveJustify down migration', () => {
186
+ it('should be retired (no down migration)', () => {
187
+ expect(() => {
188
+ down({})
189
+ }).toThrow('Migration com.tldraw.shape.text/1 does not have a down function')
190
+ })
191
+ })
192
+ })
193
+
194
+ describe('textShapeMigrations - AddTextAlign migration', () => {
195
+ const { up, down } = getTestMigration(textShapeVersions.AddTextAlign)
196
+
197
+ describe('AddTextAlign up migration', () => {
198
+ it('should migrate align to textAlign', () => {
199
+ const oldRecord = {
200
+ id: 'shape:text1',
201
+ props: {
202
+ color: 'black',
203
+ font: 'draw',
204
+ size: 'm',
205
+ align: 'start',
206
+ w: 200,
207
+ text: 'Test text',
208
+ scale: 1,
209
+ autoSize: true,
210
+ },
211
+ }
212
+
213
+ const result = up(oldRecord)
214
+ expect(result.props.textAlign).toBe('start')
215
+ expect(result.props.align).toBeUndefined()
216
+ expect(result.props.color).toBe('black') // Preserve other props
217
+ })
218
+
219
+ it('should handle all alignment values', () => {
220
+ const alignments = ['start', 'middle', 'end']
221
+
222
+ alignments.forEach((align) => {
223
+ const oldRecord = {
224
+ id: 'shape:text1',
225
+ props: {
226
+ color: 'red',
227
+ align,
228
+ w: 200,
229
+ text: 'Test',
230
+ },
231
+ }
232
+
233
+ const result = up(oldRecord)
234
+ expect(result.props.textAlign).toBe(align)
235
+ expect(result.props.align).toBeUndefined()
236
+ })
237
+ })
238
+
239
+ it('should preserve all other properties during migration', () => {
240
+ const oldRecord = {
241
+ id: 'shape:text1',
242
+ props: {
243
+ color: 'green',
244
+ font: 'mono',
245
+ size: 's',
246
+ align: 'middle',
247
+ w: 150,
248
+ text: 'Migration test',
249
+ scale: 2,
250
+ autoSize: false,
251
+ },
252
+ }
253
+
254
+ const result = up(oldRecord)
255
+ expect(result.props.textAlign).toBe('middle')
256
+ expect(result.props.align).toBeUndefined()
257
+ expect(result.props.color).toBe('green')
258
+ expect(result.props.font).toBe('mono')
259
+ expect(result.props.size).toBe('s')
260
+ expect(result.props.w).toBe(150)
261
+ expect(result.props.text).toBe('Migration test')
262
+ expect(result.props.scale).toBe(2)
263
+ expect(result.props.autoSize).toBe(false)
264
+ })
265
+ })
266
+
267
+ describe('AddTextAlign down migration', () => {
268
+ it('should migrate textAlign back to align', () => {
269
+ const newRecord = {
270
+ id: 'shape:text1',
271
+ props: {
272
+ color: 'black',
273
+ font: 'draw',
274
+ size: 'm',
275
+ textAlign: 'start',
276
+ w: 200,
277
+ text: 'Test text',
278
+ scale: 1,
279
+ autoSize: true,
280
+ },
281
+ }
282
+
283
+ const result = down(newRecord)
284
+ expect(result.props.align).toBe('start')
285
+ expect(result.props.textAlign).toBeUndefined()
286
+ expect(result.props.color).toBe('black') // Preserve other props
287
+ })
288
+
289
+ it('should handle all textAlign values during down migration', () => {
290
+ const alignments = ['start', 'middle', 'end']
291
+
292
+ alignments.forEach((textAlign) => {
293
+ const newRecord = {
294
+ id: 'shape:text1',
295
+ props: {
296
+ color: 'blue',
297
+ textAlign,
298
+ w: 200,
299
+ text: 'Test',
300
+ },
301
+ }
302
+
303
+ const result = down(newRecord)
304
+ expect(result.props.align).toBe(textAlign)
305
+ expect(result.props.textAlign).toBeUndefined()
306
+ })
307
+ })
308
+ })
309
+ })
310
+
311
+ describe('textShapeMigrations - AddRichText migration', () => {
312
+ const { up } = getTestMigration(textShapeVersions.AddRichText)
313
+
314
+ describe('AddRichText up migration', () => {
315
+ it('should convert text property to richText', () => {
316
+ const oldRecord = {
317
+ id: 'shape:text1',
318
+ props: {
319
+ color: 'black',
320
+ font: 'draw',
321
+ size: 'm',
322
+ textAlign: 'start',
323
+ w: 200,
324
+ text: 'Simple text content',
325
+ scale: 1,
326
+ autoSize: true,
327
+ },
328
+ }
329
+
330
+ const result = up(oldRecord)
331
+ expect(result.props.richText).toBeDefined()
332
+ expect(result.props.text).toBeUndefined()
333
+ expect(result.props.color).toBe('black') // Preserve other props
334
+ })
335
+
336
+ it('should handle empty text', () => {
337
+ const oldRecord = {
338
+ id: 'shape:text1',
339
+ props: {
340
+ color: 'red',
341
+ font: 'sans',
342
+ size: 'l',
343
+ textAlign: 'middle',
344
+ w: 300,
345
+ text: '',
346
+ scale: 1,
347
+ autoSize: false,
348
+ },
349
+ }
350
+
351
+ const result = up(oldRecord)
352
+ expect(result.props.richText).toBeDefined()
353
+ expect(result.props.text).toBeUndefined()
354
+ })
355
+
356
+ it('should handle multi-line text', () => {
357
+ const oldRecord = {
358
+ id: 'shape:text1',
359
+ props: {
360
+ color: 'blue',
361
+ font: 'serif',
362
+ size: 'xl',
363
+ textAlign: 'end',
364
+ w: 400,
365
+ text: 'Line 1\nLine 2\nLine 3',
366
+ scale: 1.2,
367
+ autoSize: true,
368
+ },
369
+ }
370
+
371
+ const result = up(oldRecord)
372
+ expect(result.props.richText).toBeDefined()
373
+ expect(result.props.text).toBeUndefined()
374
+ })
375
+
376
+ it('should preserve all other properties during migration', () => {
377
+ const oldRecord = {
378
+ id: 'shape:text1',
379
+ props: {
380
+ color: 'green',
381
+ font: 'mono',
382
+ size: 's',
383
+ textAlign: 'start',
384
+ w: 250,
385
+ text: 'Rich text migration test',
386
+ scale: 0.8,
387
+ autoSize: false,
388
+ },
389
+ }
390
+
391
+ const result = up(oldRecord)
392
+ expect(result.props.richText).toBeDefined()
393
+ expect(result.props.text).toBeUndefined()
394
+ expect(result.props.color).toBe('green')
395
+ expect(result.props.font).toBe('mono')
396
+ expect(result.props.size).toBe('s')
397
+ expect(result.props.textAlign).toBe('start')
398
+ expect(result.props.w).toBe(250)
399
+ expect(result.props.scale).toBe(0.8)
400
+ expect(result.props.autoSize).toBe(false)
401
+ })
402
+ })
403
+
404
+ // Note: The down migration is explicitly not defined (forced client update)
405
+ // so we don't test it
406
+ })
407
+ })
@@ -104,7 +104,6 @@ const Versions = createShapePropsMigrationIds('text', {
104
104
  RemoveJustify: 1,
105
105
  AddTextAlign: 2,
106
106
  AddRichText: 3,
107
- AddRichTextAttrs: 4,
108
107
  })
109
108
 
110
109
  /**
@@ -117,8 +116,8 @@ const Versions = createShapePropsMigrationIds('text', {
117
116
  * import { textShapeVersions } from '@tldraw/tlschema'
118
117
  *
119
118
  * // Check if shape data needs migration
120
- * if (shapeVersion < textShapeVersions.AddRichTextAttrs) {
121
- * // Apply rich text attrs migration
119
+ * if (shapeVersion < textShapeVersions.AddRichText) {
120
+ * // Apply rich text migration
122
121
  * }
123
122
  * ```
124
123
  */
@@ -132,7 +131,6 @@ export { Versions as textShapeVersions }
132
131
  * - RemoveJustify: Replaced 'justify' alignment with 'start'
133
132
  * - AddTextAlign: Migrated from 'align' to 'textAlign' property
134
133
  * - AddRichText: Converted plain text to rich text format
135
- * - AddRichTextAttrs: Added support for attrs property on richText
136
134
  *
137
135
  * @public
138
136
  */
@@ -169,17 +167,5 @@ export const textShapeMigrations = createShapePropsMigrationSequence({
169
167
  // delete props.richText
170
168
  // },
171
169
  },
172
- {
173
- id: Versions.AddRichTextAttrs,
174
- up: (_props) => {
175
- // noop - attrs is optional so old records are valid
176
- },
177
- down: (props) => {
178
- // Remove attrs from richText when migrating down
179
- if (props.richText && 'attrs' in props.richText) {
180
- delete props.richText.attrs
181
- }
182
- },
183
- },
184
170
  ],
185
171
  })
@@ -0,0 +1,112 @@
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 { videoShapeMigrations, videoShapeProps, videoShapeVersions } from './TLVideoShape'
6
+
7
+ describe('TLVideoShape', () => {
8
+ describe('videoShapeProps validation', () => {
9
+ it('should validate dimensions as nonZeroNumber', () => {
10
+ expect(() => videoShapeProps.w.validate(1)).not.toThrow()
11
+ expect(() => videoShapeProps.h.validate(100)).not.toThrow()
12
+ expect(() => videoShapeProps.w.validate(0)).toThrow()
13
+ expect(() => videoShapeProps.h.validate(-1)).toThrow()
14
+ })
15
+
16
+ it('should validate time as number including negatives', () => {
17
+ expect(() => videoShapeProps.time.validate(0)).not.toThrow()
18
+ expect(() => videoShapeProps.time.validate(-5)).not.toThrow()
19
+ expect(() => videoShapeProps.time.validate(30.5)).not.toThrow()
20
+ expect(() => videoShapeProps.time.validate('not-number')).toThrow()
21
+ })
22
+
23
+ it('should validate boolean properties', () => {
24
+ expect(() => videoShapeProps.playing.validate(true)).not.toThrow()
25
+ expect(() => videoShapeProps.autoplay.validate(false)).not.toThrow()
26
+ expect(() => videoShapeProps.playing.validate('true')).toThrow()
27
+ })
28
+
29
+ it('should validate URLs and assetIds', () => {
30
+ expect(() => videoShapeProps.url.validate('')).not.toThrow()
31
+ expect(() => videoShapeProps.url.validate('https://example.com/video.mp4')).not.toThrow()
32
+ expect(() => videoShapeProps.assetId.validate(null)).not.toThrow()
33
+ expect(() => videoShapeProps.assetId.validate('asset:video123' as TLAssetId)).not.toThrow()
34
+ expect(() => videoShapeProps.altText.validate('Alt text')).not.toThrow()
35
+ })
36
+
37
+ it('should validate complete props object', () => {
38
+ const validator = T.object(videoShapeProps)
39
+ const validProps = {
40
+ w: 640,
41
+ h: 480,
42
+ time: 0,
43
+ playing: false,
44
+ autoplay: true,
45
+ url: 'https://example.com/video.mp4',
46
+ assetId: 'asset:video123' as TLAssetId,
47
+ altText: 'Test video',
48
+ }
49
+ expect(() => validator.validate(validProps)).not.toThrow()
50
+ })
51
+ })
52
+
53
+ describe('videoShapeVersions', () => {
54
+ it('should have expected migration versions', () => {
55
+ expect(videoShapeVersions.AddUrlProp).toBeDefined()
56
+ expect(videoShapeVersions.MakeUrlsValid).toBeDefined()
57
+ expect(videoShapeVersions.AddAltText).toBeDefined()
58
+ expect(videoShapeVersions.AddAutoplay).toBeDefined()
59
+ })
60
+ })
61
+
62
+ describe('videoShapeMigrations', () => {
63
+ it('should add url property in AddUrlProp migration', () => {
64
+ const { up } = getTestMigration(videoShapeVersions.AddUrlProp)
65
+ const oldRecord = {
66
+ props: { w: 640, h: 480, time: 30, playing: true, assetId: 'asset:video123' },
67
+ }
68
+ const result = up(oldRecord)
69
+ expect(result.props.url).toBe('')
70
+ expect(result.props.w).toBe(640)
71
+ })
72
+
73
+ it('should clear invalid URLs in MakeUrlsValid migration', () => {
74
+ const { up } = getTestMigration(videoShapeVersions.MakeUrlsValid)
75
+ const oldRecord = {
76
+ props: { url: 'invalid-url', w: 400, h: 300 },
77
+ }
78
+ const result = up(oldRecord)
79
+ expect(result.props.url).toBe('')
80
+ })
81
+
82
+ it('should add altText in AddAltText migration', () => {
83
+ const { up, down } = getTestMigration(videoShapeVersions.AddAltText)
84
+ const oldRecord = { props: { w: 800, h: 600 } }
85
+ const result = up(oldRecord)
86
+ expect(result.props.altText).toBe('')
87
+
88
+ // Test down migration removes altText
89
+ const newRecord = { props: { w: 640, h: 480, altText: 'Test' } }
90
+ const downResult = down(newRecord)
91
+ expect(downResult.props.altText).toBeUndefined()
92
+ })
93
+
94
+ it('should add autoplay in AddAutoplay migration', () => {
95
+ const { up, down } = getTestMigration(videoShapeVersions.AddAutoplay)
96
+ const oldRecord = { props: { w: 480, h: 270 } }
97
+ const result = up(oldRecord)
98
+ expect(result.props.autoplay).toBe(true)
99
+
100
+ // Test down migration removes autoplay
101
+ const newRecord = { props: { w: 800, h: 450, autoplay: false } }
102
+ const downResult = down(newRecord)
103
+ expect(downResult.props.autoplay).toBeUndefined()
104
+ })
105
+
106
+ it('should have migrations for all versions', () => {
107
+ expect(videoShapeMigrations.sequence).toBeDefined()
108
+ expect(Array.isArray(videoShapeMigrations.sequence)).toBe(true)
109
+ expect(videoShapeMigrations.sequence.length).toBeGreaterThanOrEqual(4)
110
+ })
111
+ })
112
+ })
@@ -67,36 +67,35 @@ export const storeMigrations = createMigrationSequence({
67
67
  sequence: [
68
68
  {
69
69
  id: Versions.RemoveCodeAndIconShapeTypes,
70
- scope: 'storage',
71
- up: (storage) => {
72
- for (const [id, record] of storage.entries()) {
70
+ scope: 'store',
71
+ up: (store) => {
72
+ for (const [id, record] of objectMapEntries(store)) {
73
73
  if (
74
74
  record.typeName === 'shape' &&
75
- 'type' in record &&
76
- (record.type === 'icon' || record.type === 'code')
75
+ ((record as TLShape).type === 'icon' || (record as TLShape).type === 'code')
77
76
  ) {
78
- storage.delete(id)
77
+ delete store[id]
79
78
  }
80
79
  }
81
80
  },
82
81
  },
83
82
  {
84
83
  id: Versions.AddInstancePresenceType,
85
- scope: 'storage',
86
- up(_storage) {
84
+ scope: 'store',
85
+ up(_store) {
87
86
  // noop
88
87
  // there used to be a down migration for this but we made down migrations optional
89
- // and we don't use them on storage-level migrations so we can just remove it
88
+ // and we don't use them on store-level migrations so we can just remove it
90
89
  },
91
90
  },
92
91
  {
93
92
  // remove user and presence records and add pointer records
94
93
  id: Versions.RemoveTLUserAndPresenceAndAddPointer,
95
- scope: 'storage',
96
- up: (storage) => {
97
- for (const [id, record] of storage.entries()) {
94
+ scope: 'store',
95
+ up: (store) => {
96
+ for (const [id, record] of objectMapEntries(store)) {
98
97
  if (record.typeName.match(/^(user|user_presence)$/)) {
99
- storage.delete(id)
98
+ delete store[id]
100
99
  }
101
100
  }
102
101
  },
@@ -104,11 +103,11 @@ export const storeMigrations = createMigrationSequence({
104
103
  {
105
104
  // remove user document records
106
105
  id: Versions.RemoveUserDocument,
107
- scope: 'storage',
108
- up: (storage) => {
109
- for (const [id, record] of storage.entries()) {
106
+ scope: 'store',
107
+ up: (store) => {
108
+ for (const [id, record] of objectMapEntries(store)) {
110
109
  if (record.typeName.match('user_document')) {
111
- storage.delete(id)
110
+ delete store[id]
112
111
  }
113
112
  }
114
113
  },