@tldraw/tlschema 4.2.1 → 4.2.2
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 +242 -71
- package/dist-cjs/index.js +4 -1
- package/dist-cjs/index.js.map +2 -2
- package/dist-cjs/misc/TLOpacity.js +1 -5
- package/dist-cjs/misc/TLOpacity.js.map +2 -2
- package/dist-cjs/misc/TLRichText.js +5 -1
- package/dist-cjs/misc/TLRichText.js.map +2 -2
- package/dist-cjs/misc/b64Vecs.js +224 -0
- package/dist-cjs/misc/b64Vecs.js.map +7 -0
- 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 +26 -13
- package/dist-cjs/shapes/TLArrowShape.js.map +2 -2
- package/dist-cjs/shapes/TLBaseShape.js.map +2 -2
- package/dist-cjs/shapes/TLDrawShape.js +37 -4
- package/dist-cjs/shapes/TLDrawShape.js.map +2 -2
- package/dist-cjs/shapes/TLEmbedShape.js +17 -0
- package/dist-cjs/shapes/TLEmbedShape.js.map +2 -2
- package/dist-cjs/shapes/TLGeoShape.js +12 -1
- package/dist-cjs/shapes/TLGeoShape.js.map +2 -2
- package/dist-cjs/shapes/TLHighlightShape.js +29 -2
- package/dist-cjs/shapes/TLHighlightShape.js.map +2 -2
- package/dist-cjs/shapes/TLNoteShape.js +12 -1
- package/dist-cjs/shapes/TLNoteShape.js.map +2 -2
- package/dist-cjs/shapes/TLTextShape.js +12 -1
- 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 +242 -71
- package/dist-esm/index.mjs +5 -1
- package/dist-esm/index.mjs.map +2 -2
- package/dist-esm/misc/TLOpacity.mjs +1 -5
- package/dist-esm/misc/TLOpacity.mjs.map +2 -2
- package/dist-esm/misc/TLRichText.mjs +5 -1
- package/dist-esm/misc/TLRichText.mjs.map +2 -2
- package/dist-esm/misc/b64Vecs.mjs +204 -0
- package/dist-esm/misc/b64Vecs.mjs.map +7 -0
- 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 +26 -13
- package/dist-esm/shapes/TLArrowShape.mjs.map +2 -2
- package/dist-esm/shapes/TLBaseShape.mjs.map +2 -2
- package/dist-esm/shapes/TLDrawShape.mjs +37 -4
- package/dist-esm/shapes/TLDrawShape.mjs.map +2 -2
- package/dist-esm/shapes/TLEmbedShape.mjs +17 -0
- package/dist-esm/shapes/TLEmbedShape.mjs.map +2 -2
- package/dist-esm/shapes/TLGeoShape.mjs +12 -1
- package/dist-esm/shapes/TLGeoShape.mjs.map +2 -2
- package/dist-esm/shapes/TLHighlightShape.mjs +29 -2
- package/dist-esm/shapes/TLHighlightShape.mjs.map +2 -2
- package/dist-esm/shapes/TLNoteShape.mjs +12 -1
- package/dist-esm/shapes/TLNoteShape.mjs.map +2 -2
- package/dist-esm/shapes/TLTextShape.mjs +12 -1
- 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 +9 -3
- package/src/bindings/TLBaseBinding.ts +25 -14
- package/src/createTLSchema.ts +8 -2
- package/src/index.ts +9 -0
- package/src/migrations.test.ts +149 -1
- package/src/misc/TLOpacity.ts +1 -5
- package/src/misc/TLRichText.ts +6 -1
- package/src/misc/b64Vecs.ts +308 -0
- package/src/records/TLAsset.ts +2 -2
- package/src/records/TLBinding.ts +65 -23
- package/src/records/TLShape.ts +100 -5
- package/src/shapes/ShapeWithCrop.ts +2 -2
- package/src/shapes/TLArrowShape.ts +28 -14
- package/src/shapes/TLBaseShape.ts +34 -10
- package/src/shapes/TLDrawShape.ts +59 -12
- package/src/shapes/TLEmbedShape.ts +17 -0
- package/src/shapes/TLGeoShape.ts +14 -1
- package/src/shapes/TLHighlightShape.ts +37 -0
- package/src/shapes/TLNoteShape.ts +15 -1
- package/src/shapes/TLTextShape.ts +16 -2
- package/src/store-migrations.ts +17 -16
- package/src/assets/TLBookmarkAsset.test.ts +0 -96
- package/src/assets/TLImageAsset.test.ts +0 -213
- package/src/assets/TLVideoAsset.test.ts +0 -105
- package/src/bindings/TLArrowBinding.test.ts +0 -55
- package/src/misc/id-validator.test.ts +0 -50
- package/src/records/TLAsset.test.ts +0 -234
- package/src/records/TLBinding.test.ts +0 -22
- package/src/records/TLCamera.test.ts +0 -19
- package/src/records/TLDocument.test.ts +0 -35
- package/src/records/TLInstance.test.ts +0 -201
- package/src/records/TLPage.test.ts +0 -110
- package/src/records/TLPageState.test.ts +0 -228
- package/src/records/TLPointer.test.ts +0 -63
- package/src/records/TLPresence.test.ts +0 -190
- package/src/records/TLRecord.test.ts +0 -70
- package/src/records/TLShape.test.ts +0 -232
- package/src/shapes/ShapeWithCrop.test.ts +0 -18
- package/src/shapes/TLArrowShape.test.ts +0 -505
- package/src/shapes/TLBaseShape.test.ts +0 -142
- package/src/shapes/TLBookmarkShape.test.ts +0 -122
- package/src/shapes/TLDrawShape.test.ts +0 -177
- package/src/shapes/TLEmbedShape.test.ts +0 -286
- package/src/shapes/TLFrameShape.test.ts +0 -71
- package/src/shapes/TLGeoShape.test.ts +0 -247
- package/src/shapes/TLGroupShape.test.ts +0 -59
- package/src/shapes/TLHighlightShape.test.ts +0 -325
- package/src/shapes/TLImageShape.test.ts +0 -534
- package/src/shapes/TLLineShape.test.ts +0 -269
- package/src/shapes/TLNoteShape.test.ts +0 -1568
- package/src/shapes/TLTextShape.test.ts +0 -407
- package/src/shapes/TLVideoShape.test.ts +0 -112
- package/src/styles/TLColorStyle.test.ts +0 -439
|
@@ -1,122 +0,0 @@
|
|
|
1
|
-
import { describe, expect, it } from 'vitest'
|
|
2
|
-
import { getTestMigration } from '../__tests__/migrationTestUtils'
|
|
3
|
-
import { bookmarkShapeProps, bookmarkShapeVersions } from './TLBookmarkShape'
|
|
4
|
-
|
|
5
|
-
describe('TLBookmarkShape', () => {
|
|
6
|
-
describe('bookmarkShapeProps validation', () => {
|
|
7
|
-
it('should validate width as nonZeroNumber', () => {
|
|
8
|
-
// Valid non-zero positive numbers
|
|
9
|
-
expect(() => bookmarkShapeProps.w.validate(0.1)).not.toThrow()
|
|
10
|
-
expect(() => bookmarkShapeProps.w.validate(100)).not.toThrow()
|
|
11
|
-
|
|
12
|
-
// Invalid: zero and negative
|
|
13
|
-
expect(() => bookmarkShapeProps.w.validate(0)).toThrow()
|
|
14
|
-
expect(() => bookmarkShapeProps.w.validate(-1)).toThrow()
|
|
15
|
-
})
|
|
16
|
-
|
|
17
|
-
it('should validate height as nonZeroNumber', () => {
|
|
18
|
-
// Valid non-zero positive numbers
|
|
19
|
-
expect(() => bookmarkShapeProps.h.validate(0.1)).not.toThrow()
|
|
20
|
-
expect(() => bookmarkShapeProps.h.validate(100)).not.toThrow()
|
|
21
|
-
|
|
22
|
-
// Invalid: zero and negative
|
|
23
|
-
expect(() => bookmarkShapeProps.h.validate(0)).toThrow()
|
|
24
|
-
expect(() => bookmarkShapeProps.h.validate(-1)).toThrow()
|
|
25
|
-
})
|
|
26
|
-
|
|
27
|
-
it('should validate assetId as nullable asset ID', () => {
|
|
28
|
-
// Valid asset IDs
|
|
29
|
-
expect(() => bookmarkShapeProps.assetId.validate(null)).not.toThrow()
|
|
30
|
-
expect(() => bookmarkShapeProps.assetId.validate('asset:bookmark123')).not.toThrow()
|
|
31
|
-
|
|
32
|
-
// Invalid asset IDs
|
|
33
|
-
expect(() => bookmarkShapeProps.assetId.validate('shape:notasset')).toThrow()
|
|
34
|
-
expect(() => bookmarkShapeProps.assetId.validate('bookmark123')).toThrow()
|
|
35
|
-
expect(() => bookmarkShapeProps.assetId.validate(undefined)).toThrow()
|
|
36
|
-
})
|
|
37
|
-
|
|
38
|
-
it('should validate url as linkUrl', () => {
|
|
39
|
-
// Valid URLs
|
|
40
|
-
expect(() => bookmarkShapeProps.url.validate('')).not.toThrow()
|
|
41
|
-
expect(() => bookmarkShapeProps.url.validate('https://example.com')).not.toThrow()
|
|
42
|
-
|
|
43
|
-
// Invalid URLs
|
|
44
|
-
expect(() => bookmarkShapeProps.url.validate('not-a-url')).toThrow()
|
|
45
|
-
expect(() => bookmarkShapeProps.url.validate('javascript:alert("xss")')).toThrow()
|
|
46
|
-
})
|
|
47
|
-
})
|
|
48
|
-
|
|
49
|
-
describe('NullAssetId migration', () => {
|
|
50
|
-
const { up, down } = getTestMigration(bookmarkShapeVersions.NullAssetId)
|
|
51
|
-
|
|
52
|
-
it('should add assetId as null when undefined', () => {
|
|
53
|
-
const oldRecord = {
|
|
54
|
-
props: {
|
|
55
|
-
w: 300,
|
|
56
|
-
h: 320,
|
|
57
|
-
url: 'https://example.com',
|
|
58
|
-
// assetId undefined
|
|
59
|
-
},
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
const result = up(oldRecord)
|
|
63
|
-
expect(result.props.assetId).toBeNull()
|
|
64
|
-
})
|
|
65
|
-
|
|
66
|
-
it('should preserve existing assetId when present', () => {
|
|
67
|
-
const oldRecord = {
|
|
68
|
-
props: {
|
|
69
|
-
w: 300,
|
|
70
|
-
h: 320,
|
|
71
|
-
url: 'https://example.com',
|
|
72
|
-
assetId: 'asset:existing123',
|
|
73
|
-
},
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
const result = up(oldRecord)
|
|
77
|
-
expect(result.props.assetId).toBe('asset:existing123')
|
|
78
|
-
})
|
|
79
|
-
|
|
80
|
-
it('should throw on retired down migration', () => {
|
|
81
|
-
expect(() => down({})).toThrow()
|
|
82
|
-
})
|
|
83
|
-
})
|
|
84
|
-
|
|
85
|
-
describe('MakeUrlsValid migration', () => {
|
|
86
|
-
const { up, down } = getTestMigration(bookmarkShapeVersions.MakeUrlsValid)
|
|
87
|
-
|
|
88
|
-
it('should set invalid URLs to empty string', () => {
|
|
89
|
-
const oldRecord = {
|
|
90
|
-
props: {
|
|
91
|
-
w: 300,
|
|
92
|
-
h: 320,
|
|
93
|
-
assetId: null,
|
|
94
|
-
url: 'not-a-valid-url',
|
|
95
|
-
},
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
const result = up(oldRecord)
|
|
99
|
-
expect(result.props.url).toBe('')
|
|
100
|
-
})
|
|
101
|
-
|
|
102
|
-
it('should preserve valid URLs', () => {
|
|
103
|
-
const oldRecord = {
|
|
104
|
-
props: {
|
|
105
|
-
w: 300,
|
|
106
|
-
h: 320,
|
|
107
|
-
assetId: null,
|
|
108
|
-
url: 'https://example.com',
|
|
109
|
-
},
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
const result = up(oldRecord)
|
|
113
|
-
expect(result.props.url).toBe('https://example.com')
|
|
114
|
-
})
|
|
115
|
-
|
|
116
|
-
it('should be noop for down migration', () => {
|
|
117
|
-
const newRecord = { props: { url: 'https://example.com' } }
|
|
118
|
-
const result = down(newRecord)
|
|
119
|
-
expect(result).toEqual(newRecord)
|
|
120
|
-
})
|
|
121
|
-
})
|
|
122
|
-
})
|
|
@@ -1,177 +0,0 @@
|
|
|
1
|
-
import { T } from '@tldraw/validate'
|
|
2
|
-
import { describe, expect, it } from 'vitest'
|
|
3
|
-
import { getTestMigration } from '../__tests__/migrationTestUtils'
|
|
4
|
-
import { VecModel } from '../misc/geometry-types'
|
|
5
|
-
import { DefaultColorStyle } from '../styles/TLColorStyle'
|
|
6
|
-
import { DefaultDashStyle } from '../styles/TLDashStyle'
|
|
7
|
-
import { DefaultFillStyle } from '../styles/TLFillStyle'
|
|
8
|
-
import { DefaultSizeStyle } from '../styles/TLSizeStyle'
|
|
9
|
-
import { DrawShapeSegment, drawShapeProps, drawShapeVersions } from './TLDrawShape'
|
|
10
|
-
|
|
11
|
-
describe('TLDrawShape', () => {
|
|
12
|
-
describe('DrawShapeSegment validator', () => {
|
|
13
|
-
it('should validate valid segment structures', () => {
|
|
14
|
-
const validSegments = [
|
|
15
|
-
{ type: 'free', points: [{ x: 0, y: 0 }] },
|
|
16
|
-
{ type: 'straight', points: [{ x: 0, y: 0, z: 0.5 }] },
|
|
17
|
-
{ type: 'free', points: [] },
|
|
18
|
-
]
|
|
19
|
-
|
|
20
|
-
validSegments.forEach((segment) => {
|
|
21
|
-
expect(() => DrawShapeSegment.validate(segment)).not.toThrow()
|
|
22
|
-
})
|
|
23
|
-
})
|
|
24
|
-
|
|
25
|
-
it('should reject invalid segment types and points', () => {
|
|
26
|
-
const invalidSegments = [
|
|
27
|
-
{ type: 'invalid', points: [{ x: 0, y: 0 }] },
|
|
28
|
-
{ type: 'free', points: [{ x: 'invalid', y: 0 }] },
|
|
29
|
-
{ type: 'free', points: 'not-array' },
|
|
30
|
-
{}, // Missing required fields
|
|
31
|
-
]
|
|
32
|
-
|
|
33
|
-
invalidSegments.forEach((segment) => {
|
|
34
|
-
expect(() => DrawShapeSegment.validate(segment)).toThrow()
|
|
35
|
-
})
|
|
36
|
-
})
|
|
37
|
-
})
|
|
38
|
-
|
|
39
|
-
describe('drawShapeProps validation schema', () => {
|
|
40
|
-
it('should validate complete valid props object', () => {
|
|
41
|
-
const fullValidator = T.object(drawShapeProps)
|
|
42
|
-
|
|
43
|
-
const validProps = {
|
|
44
|
-
color: 'red' as const,
|
|
45
|
-
fill: 'solid' as const,
|
|
46
|
-
dash: 'dashed' as const,
|
|
47
|
-
size: 'l' as const,
|
|
48
|
-
segments: [{ type: 'free' as const, points: [{ x: 0, y: 0, z: 0.5 }] as VecModel[] }],
|
|
49
|
-
isComplete: true,
|
|
50
|
-
isClosed: true,
|
|
51
|
-
isPen: true,
|
|
52
|
-
scale: 1.5,
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
expect(() => fullValidator.validate(validProps)).not.toThrow()
|
|
56
|
-
})
|
|
57
|
-
|
|
58
|
-
it('should reject invalid property values', () => {
|
|
59
|
-
// Test key invalid cases that matter for business logic
|
|
60
|
-
expect(() => drawShapeProps.scale.validate(0)).toThrow() // zero scale invalid
|
|
61
|
-
expect(() => drawShapeProps.scale.validate(-1)).toThrow() // negative scale invalid
|
|
62
|
-
expect(() => drawShapeProps.segments.validate('not-array')).toThrow()
|
|
63
|
-
expect(() => drawShapeProps.segments.validate([{ type: 'invalid' }])).toThrow()
|
|
64
|
-
})
|
|
65
|
-
|
|
66
|
-
it('should use correct default style validators', () => {
|
|
67
|
-
expect(drawShapeProps.color).toBe(DefaultColorStyle)
|
|
68
|
-
expect(drawShapeProps.fill).toBe(DefaultFillStyle)
|
|
69
|
-
expect(drawShapeProps.dash).toBe(DefaultDashStyle)
|
|
70
|
-
expect(drawShapeProps.size).toBe(DefaultSizeStyle)
|
|
71
|
-
})
|
|
72
|
-
})
|
|
73
|
-
|
|
74
|
-
describe('AddInPen migration', () => {
|
|
75
|
-
const { up } = getTestMigration(drawShapeVersions.AddInPen)
|
|
76
|
-
|
|
77
|
-
it('should detect pen from non-standard pressure values', () => {
|
|
78
|
-
const recordWithPen = {
|
|
79
|
-
props: {
|
|
80
|
-
segments: [
|
|
81
|
-
{
|
|
82
|
-
type: 'free',
|
|
83
|
-
points: [
|
|
84
|
-
{ x: 0, y: 0, z: 0.3 }, // Non-standard pressure
|
|
85
|
-
{ x: 10, y: 10, z: 0.7 }, // Non-standard pressure
|
|
86
|
-
],
|
|
87
|
-
},
|
|
88
|
-
],
|
|
89
|
-
},
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
const result = up(recordWithPen)
|
|
93
|
-
expect(result.props.isPen).toBe(true)
|
|
94
|
-
})
|
|
95
|
-
|
|
96
|
-
it('should not detect pen from standard pressure values', () => {
|
|
97
|
-
const recordWithoutPen = {
|
|
98
|
-
props: {
|
|
99
|
-
segments: [
|
|
100
|
-
{
|
|
101
|
-
type: 'free',
|
|
102
|
-
points: [
|
|
103
|
-
{ x: 0, y: 0, z: 0 }, // Standard mouse pressure
|
|
104
|
-
{ x: 10, y: 10, z: 0.5 }, // Standard touch pressure
|
|
105
|
-
],
|
|
106
|
-
},
|
|
107
|
-
],
|
|
108
|
-
},
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
const result = up(recordWithoutPen)
|
|
112
|
-
expect(result.props.isPen).toBe(false)
|
|
113
|
-
})
|
|
114
|
-
|
|
115
|
-
it('should handle empty segments', () => {
|
|
116
|
-
const recordEmpty = {
|
|
117
|
-
props: {
|
|
118
|
-
segments: [{ type: 'free', points: [] }],
|
|
119
|
-
},
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
const result = up(recordEmpty)
|
|
123
|
-
expect(result.props.isPen).toBe(false)
|
|
124
|
-
})
|
|
125
|
-
|
|
126
|
-
it('should require both points to have non-standard pressure', () => {
|
|
127
|
-
const recordMixed = {
|
|
128
|
-
props: {
|
|
129
|
-
segments: [
|
|
130
|
-
{
|
|
131
|
-
type: 'free',
|
|
132
|
-
points: [
|
|
133
|
-
{ x: 0, y: 0, z: 0.3 }, // Non-standard
|
|
134
|
-
{ x: 10, y: 10, z: 0.5 }, // Standard
|
|
135
|
-
],
|
|
136
|
-
},
|
|
137
|
-
],
|
|
138
|
-
},
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
const result = up(recordMixed)
|
|
142
|
-
expect(result.props.isPen).toBe(false)
|
|
143
|
-
})
|
|
144
|
-
})
|
|
145
|
-
|
|
146
|
-
describe('AddScale migration', () => {
|
|
147
|
-
const { up, down } = getTestMigration(drawShapeVersions.AddScale)
|
|
148
|
-
|
|
149
|
-
it('should add scale property with default value 1', () => {
|
|
150
|
-
const oldRecord = {
|
|
151
|
-
props: {
|
|
152
|
-
color: 'blue',
|
|
153
|
-
segments: [{ type: 'free', points: [{ x: 0, y: 0 }] }],
|
|
154
|
-
isPen: false,
|
|
155
|
-
},
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
const result = up(oldRecord)
|
|
159
|
-
expect(result.props.scale).toBe(1)
|
|
160
|
-
})
|
|
161
|
-
|
|
162
|
-
it('should remove scale property on down migration', () => {
|
|
163
|
-
const newRecord = {
|
|
164
|
-
props: {
|
|
165
|
-
color: 'blue',
|
|
166
|
-
segments: [{ type: 'free', points: [{ x: 0, y: 0 }] }],
|
|
167
|
-
isPen: false,
|
|
168
|
-
scale: 1.5,
|
|
169
|
-
},
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
const result = down(newRecord)
|
|
173
|
-
expect(result.props.scale).toBeUndefined()
|
|
174
|
-
expect(result.props.color).toBe('blue') // Other props preserved
|
|
175
|
-
})
|
|
176
|
-
})
|
|
177
|
-
})
|
|
@@ -1,286 +0,0 @@
|
|
|
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
|
-
})
|
|
@@ -1,71 +0,0 @@
|
|
|
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
|
-
})
|