@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
@@ -0,0 +1,286 @@
1
+ import { describe, expect, it } from 'vitest'
2
+ import { getTestMigration } from '../__tests__/migrationTestUtils'
3
+ import { embedShapeProps, embedShapeVersions } from './TLEmbedShape'
4
+
5
+ describe('TLEmbedShape', () => {
6
+ describe('embedShapeProps validation schema', () => {
7
+ it('should validate width as nonZeroNumber', () => {
8
+ const validWidths = [0.1, 0.5, 1, 10, 100, 560, 1920, 1000.5, 9999.99]
9
+
10
+ validWidths.forEach((w) => {
11
+ expect(() => embedShapeProps.w.validate(w)).not.toThrow()
12
+ })
13
+
14
+ const invalidWidths = [0, -1, -10, -0.1, 'not-number', null, undefined, {}, [], true, false]
15
+
16
+ invalidWidths.forEach((w) => {
17
+ expect(() => embedShapeProps.w.validate(w)).toThrow()
18
+ })
19
+ })
20
+
21
+ it('should validate height as nonZeroNumber', () => {
22
+ const validHeights = [0.1, 0.5, 1, 10, 100, 315, 1080, 1000.5, 9999.99]
23
+
24
+ validHeights.forEach((h) => {
25
+ expect(() => embedShapeProps.h.validate(h)).not.toThrow()
26
+ })
27
+
28
+ const invalidHeights = [0, -1, -10, -0.1, 'not-number', null, undefined, {}, [], true, false]
29
+
30
+ invalidHeights.forEach((h) => {
31
+ expect(() => embedShapeProps.h.validate(h)).toThrow()
32
+ })
33
+ })
34
+
35
+ it('should validate url as string', () => {
36
+ const validUrls = [
37
+ '',
38
+ 'https://www.youtube.com/watch?v=dQw4w9WgXcQ',
39
+ 'https://codepen.io/team/codepen/pen/PNaGbb',
40
+ 'https://codesandbox.io/s/new',
41
+ 'https://vimeo.com/123456789',
42
+ 'https://tldraw.com/r/room123',
43
+ 'invalid-url-format', // Still valid as string
44
+ 'javascript:alert("test")', // Still valid as string
45
+ 'file:///local/file', // Still valid as string
46
+ 'relative/path',
47
+ 'text without protocol',
48
+ ]
49
+
50
+ validUrls.forEach((url) => {
51
+ expect(() => embedShapeProps.url.validate(url)).not.toThrow()
52
+ })
53
+
54
+ const invalidUrls = [123, null, undefined, {}, [], true, false]
55
+
56
+ invalidUrls.forEach((url) => {
57
+ expect(() => embedShapeProps.url.validate(url)).toThrow()
58
+ })
59
+ })
60
+ })
61
+
62
+ describe('embedShapeMigrations - GenOriginalUrlInEmbed migration', () => {
63
+ const { up, down } = getTestMigration(embedShapeVersions.GenOriginalUrlInEmbed)
64
+
65
+ describe('GenOriginalUrlInEmbed up migration', () => {
66
+ it('should extract original URL from tldraw embed URLs', () => {
67
+ const tldrawUrls = [
68
+ 'https://tldraw.com/r/room123',
69
+ 'https://beta.tldraw.com/r/room456',
70
+ 'http://localhost:3000/r/local-room',
71
+ ]
72
+
73
+ tldrawUrls.forEach((url) => {
74
+ const oldRecord = {
75
+ id: 'shape:embed1',
76
+ props: {
77
+ w: 560,
78
+ h: 315,
79
+ url,
80
+ },
81
+ }
82
+
83
+ const result = up(oldRecord)
84
+ expect(result.props.url).toBe(url) // Should keep the URL as-is for tldraw
85
+ expect(result.props.tmpOldUrl).toBe(url)
86
+ })
87
+ })
88
+
89
+ it('should extract original URL from YouTube embed URLs', () => {
90
+ const testCases = [
91
+ {
92
+ embed: 'https://www.youtube.com/embed/dQw4w9WgXcQ',
93
+ expected: 'https://www.youtube.com/watch?v=dQw4w9WgXcQ',
94
+ },
95
+ {
96
+ embed: 'https://youtube.com/embed/abc123',
97
+ expected: 'https://www.youtube.com/watch?v=abc123',
98
+ },
99
+ ]
100
+
101
+ testCases.forEach(({ embed, expected }) => {
102
+ const oldRecord = {
103
+ id: 'shape:embed1',
104
+ props: {
105
+ w: 560,
106
+ h: 315,
107
+ url: embed,
108
+ },
109
+ }
110
+
111
+ const result = up(oldRecord)
112
+ expect(result.props.url).toBe(expected)
113
+ expect(result.props.tmpOldUrl).toBe(embed)
114
+ })
115
+ })
116
+
117
+ it('should extract original URL from CodePen embed URLs', () => {
118
+ const oldRecord = {
119
+ id: 'shape:embed1',
120
+ props: {
121
+ w: 560,
122
+ h: 315,
123
+ url: 'https://codepen.io/user/embed/abcdef',
124
+ },
125
+ }
126
+
127
+ const result = up(oldRecord)
128
+ expect(result.props.url).toBe('https://codepen.io/user/pen/abcdef')
129
+ expect(result.props.tmpOldUrl).toBe('https://codepen.io/user/embed/abcdef')
130
+ })
131
+
132
+ it('should handle Google Maps embed URLs (documents hostname matching limitation)', () => {
133
+ const oldRecord = {
134
+ id: 'shape:embed1',
135
+ props: {
136
+ w: 560,
137
+ h: 315,
138
+ url: 'https://www.google.com/maps/embed/v1/view?center=40.7128,-74.0060&zoom=10',
139
+ },
140
+ }
141
+
142
+ const result = up(oldRecord)
143
+ // NOTE: The wildcard 'google.*' doesn't match 'google.com' due to exact string matching
144
+ // The URL is valid and parseable, so it goes through normal flow but doesn't match any hostname
145
+ expect(result.props.url).toBe('') // originalUrl is undefined, so becomes empty string
146
+ expect(result.props.tmpOldUrl).toBe(
147
+ 'https://www.google.com/maps/embed/v1/view?center=40.7128,-74.0060&zoom=10'
148
+ )
149
+ })
150
+
151
+ it('should extract original URL from Vimeo embed URLs', () => {
152
+ const oldRecord = {
153
+ id: 'shape:embed1',
154
+ props: {
155
+ w: 560,
156
+ h: 315,
157
+ url: 'https://player.vimeo.com/video/123456789',
158
+ },
159
+ }
160
+
161
+ const result = up(oldRecord)
162
+ expect(result.props.url).toBe('https://vimeo.com/123456789')
163
+ expect(result.props.tmpOldUrl).toBe('https://player.vimeo.com/video/123456789')
164
+ })
165
+ })
166
+
167
+ describe('GenOriginalUrlInEmbed down migration', () => {
168
+ it('should be retired (no down migration)', () => {
169
+ expect(() => {
170
+ down({})
171
+ }).toThrow('Migration com.tldraw.shape.embed/1 does not have a down function')
172
+ })
173
+ })
174
+ })
175
+
176
+ describe('embedShapeMigrations - RemoveDoesResize migration', () => {
177
+ const { up, down } = getTestMigration(embedShapeVersions.RemoveDoesResize)
178
+
179
+ describe('RemoveDoesResize up migration', () => {
180
+ it('should remove doesResize property', () => {
181
+ const oldRecord = {
182
+ id: 'shape:embed1',
183
+ props: {
184
+ w: 560,
185
+ h: 315,
186
+ url: 'https://example.com',
187
+ doesResize: true,
188
+ },
189
+ }
190
+
191
+ const result = up(oldRecord)
192
+ expect(result.props.doesResize).toBeUndefined()
193
+ })
194
+ })
195
+
196
+ describe('RemoveDoesResize down migration', () => {
197
+ it('should be retired (no down migration)', () => {
198
+ expect(() => {
199
+ down({})
200
+ }).toThrow('Migration com.tldraw.shape.embed/2 does not have a down function')
201
+ })
202
+ })
203
+ })
204
+
205
+ describe('embedShapeMigrations - RemoveTmpOldUrl migration', () => {
206
+ const { up, down } = getTestMigration(embedShapeVersions.RemoveTmpOldUrl)
207
+
208
+ describe('RemoveTmpOldUrl up migration', () => {
209
+ it('should remove tmpOldUrl property', () => {
210
+ const oldRecord = {
211
+ id: 'shape:embed1',
212
+ props: {
213
+ w: 560,
214
+ h: 315,
215
+ url: 'https://example.com',
216
+ tmpOldUrl: 'https://old-url.com',
217
+ },
218
+ }
219
+
220
+ const result = up(oldRecord)
221
+ expect(result.props.tmpOldUrl).toBeUndefined()
222
+ })
223
+ })
224
+
225
+ describe('RemoveTmpOldUrl down migration', () => {
226
+ it('should be retired (no down migration)', () => {
227
+ expect(() => {
228
+ down({})
229
+ }).toThrow('Migration com.tldraw.shape.embed/3 does not have a down function')
230
+ })
231
+ })
232
+ })
233
+
234
+ describe('embedShapeMigrations - RemovePermissionOverrides migration', () => {
235
+ const { up, down } = getTestMigration(embedShapeVersions.RemovePermissionOverrides)
236
+
237
+ describe('RemovePermissionOverrides up migration', () => {
238
+ it('should remove overridePermissions property', () => {
239
+ const oldRecord = {
240
+ id: 'shape:embed1',
241
+ props: {
242
+ w: 560,
243
+ h: 315,
244
+ url: 'https://example.com',
245
+ overridePermissions: { allowScripts: true },
246
+ },
247
+ }
248
+
249
+ const result = up(oldRecord)
250
+ expect(result.props.overridePermissions).toBeUndefined()
251
+ })
252
+ })
253
+
254
+ describe('RemovePermissionOverrides down migration', () => {
255
+ it('should be retired (no down migration)', () => {
256
+ expect(() => {
257
+ down({})
258
+ }).toThrow('Migration com.tldraw.shape.embed/4 does not have a down function')
259
+ })
260
+ })
261
+ })
262
+
263
+ describe('edge cases and error handling', () => {
264
+ it('should handle zero dimension validation correctly', () => {
265
+ // Zero should be invalid for width and height (nonZeroNumber)
266
+ expect(() => embedShapeProps.w.validate(0)).toThrow()
267
+ expect(() => embedShapeProps.h.validate(0)).toThrow()
268
+
269
+ // Negative numbers should also be invalid
270
+ expect(() => embedShapeProps.w.validate(-1)).toThrow()
271
+ expect(() => embedShapeProps.h.validate(-10.5)).toThrow()
272
+ })
273
+
274
+ it('should handle migration errors when props is null', () => {
275
+ const malformedRecord = {
276
+ id: 'shape:malformed',
277
+ props: null,
278
+ }
279
+
280
+ expect(() => {
281
+ const migration = getTestMigration(embedShapeVersions.GenOriginalUrlInEmbed)
282
+ migration.up(malformedRecord)
283
+ }).toThrow('Cannot set properties of null')
284
+ })
285
+ })
286
+ })
@@ -10,7 +10,6 @@ const TLDRAW_APP_RE = /(^\/r\/[^/]+\/?$)/
10
10
  const EMBED_DEFINITIONS = [
11
11
  {
12
12
  hostnames: ['beta.tldraw.com', 'tldraw.com', 'localhost:3000'],
13
- canEditWhileLocked: true,
14
13
  fromEmbedUrl: (url: string) => {
15
14
  const urlObj = safeParseUrl(url)
16
15
  if (urlObj && urlObj.pathname.match(TLDRAW_APP_RE)) {
@@ -21,7 +20,6 @@ const EMBED_DEFINITIONS = [
21
20
  },
22
21
  {
23
22
  hostnames: ['figma.com'],
24
- canEditWhileLocked: true,
25
23
  fromEmbedUrl: (url: string) => {
26
24
  const urlObj = safeParseUrl(url)
27
25
  if (urlObj && urlObj.pathname.match(/^\/embed\/?$/)) {
@@ -35,7 +33,6 @@ const EMBED_DEFINITIONS = [
35
33
  },
36
34
  {
37
35
  hostnames: ['google.*'],
38
- canEditWhileLocked: true,
39
36
  fromEmbedUrl: (url: string) => {
40
37
  const urlObj = safeParseUrl(url)
41
38
  if (!urlObj) return
@@ -51,7 +48,6 @@ const EMBED_DEFINITIONS = [
51
48
  },
52
49
  {
53
50
  hostnames: ['val.town'],
54
- canEditWhileLocked: true,
55
51
  fromEmbedUrl: (url: string) => {
56
52
  const urlObj = safeParseUrl(url)
57
53
  // e.g. extract "steveruizok/mathFact" from https://www.val.town/v/steveruizok/mathFact
@@ -64,7 +60,6 @@ const EMBED_DEFINITIONS = [
64
60
  },
65
61
  {
66
62
  hostnames: ['codesandbox.io'],
67
- canEditWhileLocked: true,
68
63
  fromEmbedUrl: (url: string) => {
69
64
  const urlObj = safeParseUrl(url)
70
65
  const matches = urlObj && urlObj.pathname.match(/\/embed\/([^/]+)\/?/)
@@ -76,7 +71,6 @@ const EMBED_DEFINITIONS = [
76
71
  },
77
72
  {
78
73
  hostnames: ['codepen.io'],
79
- canEditWhileLocked: true,
80
74
  fromEmbedUrl: (url: string) => {
81
75
  const CODEPEN_EMBED_REGEXP = /https:\/\/codepen.io\/([^/]+)\/embed\/([^/]+)/
82
76
  const matches = url.match(CODEPEN_EMBED_REGEXP)
@@ -89,7 +83,6 @@ const EMBED_DEFINITIONS = [
89
83
  },
90
84
  {
91
85
  hostnames: ['scratch.mit.edu'],
92
- canEditWhileLocked: true,
93
86
  fromEmbedUrl: (url: string) => {
94
87
  const SCRATCH_EMBED_REGEXP = /https:\/\/scratch.mit.edu\/projects\/embed\/([^/]+)/
95
88
  const matches = url.match(SCRATCH_EMBED_REGEXP)
@@ -102,7 +95,6 @@ const EMBED_DEFINITIONS = [
102
95
  },
103
96
  {
104
97
  hostnames: ['*.youtube.com', 'youtube.com', 'youtu.be'],
105
- canEditWhileLocked: true,
106
98
  fromEmbedUrl: (url: string) => {
107
99
  const urlObj = safeParseUrl(url)
108
100
  if (!urlObj) return
@@ -119,7 +111,6 @@ const EMBED_DEFINITIONS = [
119
111
  },
120
112
  {
121
113
  hostnames: ['calendar.google.*'],
122
- canEditWhileLocked: true,
123
114
  fromEmbedUrl: (url: string) => {
124
115
  const urlObj = safeParseUrl(url)
125
116
  const srcQs = urlObj?.searchParams.get('src')
@@ -138,7 +129,6 @@ const EMBED_DEFINITIONS = [
138
129
  },
139
130
  {
140
131
  hostnames: ['docs.google.*'],
141
- canEditWhileLocked: true,
142
132
  fromEmbedUrl: (url: string) => {
143
133
  const urlObj = safeParseUrl(url)
144
134
 
@@ -155,7 +145,6 @@ const EMBED_DEFINITIONS = [
155
145
  },
156
146
  {
157
147
  hostnames: ['gist.github.com'],
158
- canEditWhileLocked: true,
159
148
  fromEmbedUrl: (url: string) => {
160
149
  const urlObj = safeParseUrl(url)
161
150
  if (urlObj && urlObj.pathname.match(/\/([^/]+)\/([^/]+)/)) {
@@ -167,7 +156,6 @@ const EMBED_DEFINITIONS = [
167
156
  },
168
157
  {
169
158
  hostnames: ['replit.com'],
170
- canEditWhileLocked: true,
171
159
  fromEmbedUrl: (url: string) => {
172
160
  const urlObj = safeParseUrl(url)
173
161
  if (
@@ -183,7 +171,6 @@ const EMBED_DEFINITIONS = [
183
171
  },
184
172
  {
185
173
  hostnames: ['felt.com'],
186
- canEditWhileLocked: true,
187
174
  fromEmbedUrl: (url: string) => {
188
175
  const urlObj = safeParseUrl(url)
189
176
  if (urlObj && urlObj.pathname.match(/^\/embed\/map\//)) {
@@ -195,7 +182,6 @@ const EMBED_DEFINITIONS = [
195
182
  },
196
183
  {
197
184
  hostnames: ['open.spotify.com'],
198
- canEditWhileLocked: true,
199
185
  fromEmbedUrl: (url: string) => {
200
186
  const urlObj = safeParseUrl(url)
201
187
  if (urlObj && urlObj.pathname.match(/^\/embed\/(artist|album)\//)) {
@@ -206,7 +192,6 @@ const EMBED_DEFINITIONS = [
206
192
  },
207
193
  {
208
194
  hostnames: ['vimeo.com', 'player.vimeo.com'],
209
- canEditWhileLocked: true,
210
195
  fromEmbedUrl: (url: string) => {
211
196
  const urlObj = safeParseUrl(url)
212
197
  if (urlObj && urlObj.hostname === 'player.vimeo.com') {
@@ -220,7 +205,6 @@ const EMBED_DEFINITIONS = [
220
205
  },
221
206
  {
222
207
  hostnames: ['observablehq.com'],
223
- canEditWhileLocked: true,
224
208
  fromEmbedUrl: (url: string) => {
225
209
  const urlObj = safeParseUrl(url)
226
210
  if (urlObj && urlObj.pathname.match(/^\/embed\/@([^/]+)\/([^/]+)\/?$/)) {
@@ -235,7 +219,6 @@ const EMBED_DEFINITIONS = [
235
219
  },
236
220
  {
237
221
  hostnames: ['desmos.com'],
238
- canEditWhileLocked: true,
239
222
  fromEmbedUrl: (url: string) => {
240
223
  const urlObj = safeParseUrl(url)
241
224
  if (
@@ -0,0 +1,71 @@
1
+ import { T } from '@tldraw/validate'
2
+ import { describe, expect, it } from 'vitest'
3
+ import { getTestMigration } from '../__tests__/migrationTestUtils'
4
+ import { frameShapeProps, frameShapeVersions } from './TLFrameShape'
5
+
6
+ describe('TLFrameShape', () => {
7
+ describe('frameShapeProps validation', () => {
8
+ it('should validate valid frame props', () => {
9
+ const validProps = {
10
+ w: 400,
11
+ h: 300,
12
+ name: 'Test Frame',
13
+ color: 'blue' as const,
14
+ }
15
+
16
+ const validator = T.object(frameShapeProps)
17
+ expect(() => validator.validate(validProps)).not.toThrow()
18
+ })
19
+
20
+ it('should reject invalid dimensions', () => {
21
+ // Zero and negative values should be rejected
22
+ expect(() => frameShapeProps.w.validate(0)).toThrow()
23
+ expect(() => frameShapeProps.h.validate(-1)).toThrow()
24
+ })
25
+
26
+ it('should reject invalid colors', () => {
27
+ // Invalid color values
28
+ expect(() => frameShapeProps.color.validate('invalid-color')).toThrow()
29
+ expect(() => frameShapeProps.color.validate('')).toThrow()
30
+ })
31
+ })
32
+
33
+ describe('AddColorProp migration', () => {
34
+ const { up, down } = getTestMigration(frameShapeVersions.AddColorProp)
35
+
36
+ it('should add color property with default value "black"', () => {
37
+ const oldRecord = {
38
+ id: 'shape:frame1',
39
+ props: {
40
+ w: 400,
41
+ h: 300,
42
+ name: 'Test Frame',
43
+ },
44
+ }
45
+
46
+ const result = up(oldRecord)
47
+ expect(result.props.color).toBe('black')
48
+ expect(result.props.w).toBe(400)
49
+ expect(result.props.h).toBe(300)
50
+ expect(result.props.name).toBe('Test Frame')
51
+ })
52
+
53
+ it('should remove color property on down migration', () => {
54
+ const newRecord = {
55
+ id: 'shape:frame1',
56
+ props: {
57
+ w: 400,
58
+ h: 300,
59
+ name: 'Test Frame',
60
+ color: 'blue',
61
+ },
62
+ }
63
+
64
+ const result = down(newRecord)
65
+ expect(result.props.color).toBeUndefined()
66
+ expect(result.props.w).toBe(400)
67
+ expect(result.props.h).toBe(300)
68
+ expect(result.props.name).toBe('Test Frame')
69
+ })
70
+ })
71
+ })