@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
|
@@ -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
|
+
})
|