@tldraw/tlschema 4.1.0-canary.9c36de6e611c → 4.1.0-canary.a152954244d2
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/TLStore.js +3 -10
- package/dist-cjs/TLStore.js.map +2 -2
- package/dist-cjs/assets/TLBaseAsset.js.map +2 -2
- package/dist-cjs/assets/TLBookmarkAsset.js.map +2 -2
- package/dist-cjs/assets/TLImageAsset.js.map +2 -2
- package/dist-cjs/assets/TLVideoAsset.js.map +2 -2
- package/dist-cjs/bindings/TLArrowBinding.js.map +2 -2
- package/dist-cjs/bindings/TLBaseBinding.js.map +2 -2
- package/dist-cjs/createPresenceStateDerivation.js.map +2 -2
- package/dist-cjs/createTLSchema.js.map +2 -2
- package/dist-cjs/index.d.ts +4416 -223
- package/dist-cjs/index.js +1 -1
- package/dist-cjs/index.js.map +2 -2
- package/dist-cjs/misc/TLColor.js.map +2 -2
- package/dist-cjs/misc/TLCursor.js.map +2 -2
- package/dist-cjs/misc/TLHandle.js.map +2 -2
- package/dist-cjs/misc/TLOpacity.js.map +2 -2
- package/dist-cjs/misc/TLRichText.js.map +2 -2
- package/dist-cjs/misc/TLScribble.js.map +2 -2
- package/dist-cjs/misc/geometry-types.js.map +2 -2
- package/dist-cjs/misc/id-validator.js.map +2 -2
- package/dist-cjs/records/TLAsset.js.map +2 -2
- package/dist-cjs/records/TLBinding.js.map +2 -2
- package/dist-cjs/records/TLCamera.js.map +2 -2
- package/dist-cjs/records/TLDocument.js.map +2 -2
- package/dist-cjs/records/TLInstance.js.map +2 -2
- package/dist-cjs/records/TLPage.js.map +2 -2
- package/dist-cjs/records/TLPageState.js.map +2 -2
- package/dist-cjs/records/TLPointer.js.map +2 -2
- package/dist-cjs/records/TLPresence.js.map +2 -2
- package/dist-cjs/records/TLRecord.js.map +1 -1
- package/dist-cjs/records/TLShape.js.map +2 -2
- package/dist-cjs/recordsWithProps.js.map +2 -2
- package/dist-cjs/shapes/ShapeWithCrop.js.map +1 -1
- package/dist-cjs/shapes/TLArrowShape.js.map +2 -2
- package/dist-cjs/shapes/TLBaseShape.js.map +2 -2
- package/dist-cjs/shapes/TLBookmarkShape.js.map +2 -2
- package/dist-cjs/shapes/TLDrawShape.js.map +2 -2
- package/dist-cjs/shapes/TLEmbedShape.js +0 -10
- package/dist-cjs/shapes/TLEmbedShape.js.map +2 -2
- package/dist-cjs/shapes/TLFrameShape.js.map +2 -2
- package/dist-cjs/shapes/TLGeoShape.js.map +2 -2
- package/dist-cjs/shapes/TLGroupShape.js.map +2 -2
- package/dist-cjs/shapes/TLHighlightShape.js.map +2 -2
- package/dist-cjs/shapes/TLImageShape.js.map +2 -2
- package/dist-cjs/shapes/TLLineShape.js.map +2 -2
- package/dist-cjs/shapes/TLNoteShape.js.map +2 -2
- package/dist-cjs/shapes/TLTextShape.js.map +2 -2
- package/dist-cjs/shapes/TLVideoShape.js.map +2 -2
- package/dist-cjs/store-migrations.js.map +2 -2
- package/dist-cjs/styles/TLColorStyle.js.map +2 -2
- package/dist-cjs/styles/TLDashStyle.js.map +2 -2
- package/dist-cjs/styles/TLFillStyle.js.map +2 -2
- package/dist-cjs/styles/TLFontStyle.js.map +2 -2
- package/dist-cjs/styles/TLHorizontalAlignStyle.js.map +2 -2
- package/dist-cjs/styles/TLSizeStyle.js.map +2 -2
- package/dist-cjs/styles/TLTextAlignStyle.js.map +2 -2
- package/dist-cjs/styles/TLVerticalAlignStyle.js.map +2 -2
- package/dist-cjs/translations/translations.js +1 -1
- package/dist-cjs/translations/translations.js.map +2 -2
- package/dist-cjs/util-types.js.map +1 -1
- package/dist-esm/TLStore.mjs +3 -10
- package/dist-esm/TLStore.mjs.map +2 -2
- package/dist-esm/assets/TLBaseAsset.mjs.map +2 -2
- package/dist-esm/assets/TLBookmarkAsset.mjs.map +2 -2
- package/dist-esm/assets/TLImageAsset.mjs.map +2 -2
- package/dist-esm/assets/TLVideoAsset.mjs.map +2 -2
- package/dist-esm/bindings/TLArrowBinding.mjs.map +2 -2
- package/dist-esm/bindings/TLBaseBinding.mjs.map +2 -2
- package/dist-esm/createPresenceStateDerivation.mjs.map +2 -2
- package/dist-esm/createTLSchema.mjs.map +2 -2
- package/dist-esm/index.d.mts +4416 -223
- package/dist-esm/index.mjs +1 -1
- package/dist-esm/index.mjs.map +2 -2
- package/dist-esm/misc/TLColor.mjs.map +2 -2
- package/dist-esm/misc/TLCursor.mjs.map +2 -2
- package/dist-esm/misc/TLHandle.mjs.map +2 -2
- package/dist-esm/misc/TLOpacity.mjs.map +2 -2
- package/dist-esm/misc/TLRichText.mjs.map +2 -2
- package/dist-esm/misc/TLScribble.mjs.map +2 -2
- package/dist-esm/misc/geometry-types.mjs.map +2 -2
- package/dist-esm/misc/id-validator.mjs.map +2 -2
- package/dist-esm/records/TLAsset.mjs.map +2 -2
- package/dist-esm/records/TLBinding.mjs.map +2 -2
- package/dist-esm/records/TLCamera.mjs.map +2 -2
- package/dist-esm/records/TLDocument.mjs.map +2 -2
- package/dist-esm/records/TLInstance.mjs.map +2 -2
- package/dist-esm/records/TLPage.mjs.map +2 -2
- package/dist-esm/records/TLPageState.mjs.map +2 -2
- package/dist-esm/records/TLPointer.mjs.map +2 -2
- package/dist-esm/records/TLPresence.mjs.map +2 -2
- package/dist-esm/records/TLShape.mjs.map +2 -2
- package/dist-esm/recordsWithProps.mjs.map +2 -2
- package/dist-esm/shapes/TLArrowShape.mjs.map +2 -2
- package/dist-esm/shapes/TLBaseShape.mjs.map +2 -2
- package/dist-esm/shapes/TLBookmarkShape.mjs.map +2 -2
- package/dist-esm/shapes/TLDrawShape.mjs.map +2 -2
- package/dist-esm/shapes/TLEmbedShape.mjs +0 -10
- package/dist-esm/shapes/TLEmbedShape.mjs.map +2 -2
- package/dist-esm/shapes/TLFrameShape.mjs.map +2 -2
- package/dist-esm/shapes/TLGeoShape.mjs.map +2 -2
- package/dist-esm/shapes/TLGroupShape.mjs.map +2 -2
- package/dist-esm/shapes/TLHighlightShape.mjs.map +2 -2
- package/dist-esm/shapes/TLImageShape.mjs.map +2 -2
- package/dist-esm/shapes/TLLineShape.mjs.map +2 -2
- package/dist-esm/shapes/TLNoteShape.mjs.map +2 -2
- package/dist-esm/shapes/TLTextShape.mjs.map +2 -2
- package/dist-esm/shapes/TLVideoShape.mjs.map +2 -2
- package/dist-esm/store-migrations.mjs.map +2 -2
- package/dist-esm/styles/TLColorStyle.mjs.map +2 -2
- package/dist-esm/styles/TLDashStyle.mjs.map +2 -2
- package/dist-esm/styles/TLFillStyle.mjs.map +2 -2
- package/dist-esm/styles/TLFontStyle.mjs.map +2 -2
- package/dist-esm/styles/TLHorizontalAlignStyle.mjs.map +2 -2
- package/dist-esm/styles/TLSizeStyle.mjs.map +2 -2
- package/dist-esm/styles/TLTextAlignStyle.mjs.map +2 -2
- package/dist-esm/styles/TLVerticalAlignStyle.mjs.map +2 -2
- package/dist-esm/translations/translations.mjs +1 -1
- package/dist-esm/translations/translations.mjs.map +2 -2
- package/package.json +5 -5
- package/src/TLStore.test.ts +644 -0
- package/src/TLStore.ts +205 -20
- package/src/assets/TLBaseAsset.ts +90 -7
- package/src/assets/TLBookmarkAsset.test.ts +96 -0
- package/src/assets/TLBookmarkAsset.ts +52 -2
- package/src/assets/TLImageAsset.test.ts +213 -0
- package/src/assets/TLImageAsset.ts +60 -2
- package/src/assets/TLVideoAsset.test.ts +105 -0
- package/src/assets/TLVideoAsset.ts +93 -4
- package/src/bindings/TLArrowBinding.test.ts +55 -0
- package/src/bindings/TLArrowBinding.ts +132 -10
- package/src/bindings/TLBaseBinding.ts +140 -3
- package/src/createPresenceStateDerivation.test.ts +158 -0
- package/src/createPresenceStateDerivation.ts +71 -2
- package/src/createTLSchema.test.ts +181 -0
- package/src/createTLSchema.ts +164 -7
- package/src/index.ts +32 -0
- package/src/misc/TLColor.ts +50 -6
- package/src/misc/TLCursor.ts +110 -8
- package/src/misc/TLHandle.ts +86 -6
- package/src/misc/TLOpacity.ts +51 -2
- package/src/misc/TLRichText.ts +56 -3
- package/src/misc/TLScribble.ts +105 -5
- package/src/misc/geometry-types.ts +30 -2
- package/src/misc/id-validator.test.ts +50 -0
- package/src/misc/id-validator.ts +20 -1
- package/src/records/TLAsset.test.ts +234 -0
- package/src/records/TLAsset.ts +165 -8
- package/src/records/TLBinding.test.ts +22 -0
- package/src/records/TLBinding.ts +277 -11
- package/src/records/TLCamera.test.ts +19 -0
- package/src/records/TLCamera.ts +118 -7
- package/src/records/TLDocument.test.ts +35 -0
- package/src/records/TLDocument.ts +148 -8
- package/src/records/TLInstance.test.ts +201 -0
- package/src/records/TLInstance.ts +117 -9
- package/src/records/TLPage.test.ts +110 -0
- package/src/records/TLPage.ts +106 -8
- package/src/records/TLPageState.test.ts +228 -0
- package/src/records/TLPageState.ts +88 -7
- package/src/records/TLPointer.test.ts +63 -0
- package/src/records/TLPointer.ts +105 -7
- package/src/records/TLPresence.test.ts +190 -0
- package/src/records/TLPresence.ts +99 -5
- package/src/records/TLRecord.test.ts +70 -0
- package/src/records/TLRecord.ts +43 -1
- package/src/records/TLShape.test.ts +232 -0
- package/src/records/TLShape.ts +289 -12
- package/src/recordsWithProps.test.ts +188 -0
- package/src/recordsWithProps.ts +131 -2
- package/src/shapes/ShapeWithCrop.test.ts +18 -0
- package/src/shapes/ShapeWithCrop.ts +64 -2
- package/src/shapes/TLArrowShape.test.ts +505 -0
- package/src/shapes/TLArrowShape.ts +188 -10
- package/src/shapes/TLBaseShape.test.ts +142 -0
- package/src/shapes/TLBaseShape.ts +103 -4
- package/src/shapes/TLBookmarkShape.test.ts +122 -0
- package/src/shapes/TLBookmarkShape.ts +58 -4
- package/src/shapes/TLDrawShape.test.ts +177 -0
- package/src/shapes/TLDrawShape.ts +97 -6
- package/src/shapes/TLEmbedShape.test.ts +286 -0
- package/src/shapes/TLEmbedShape.ts +57 -14
- package/src/shapes/TLFrameShape.test.ts +71 -0
- package/src/shapes/TLFrameShape.ts +59 -4
- package/src/shapes/TLGeoShape.test.ts +247 -0
- package/src/shapes/TLGeoShape.ts +103 -7
- package/src/shapes/TLGroupShape.test.ts +59 -0
- package/src/shapes/TLGroupShape.ts +52 -4
- package/src/shapes/TLHighlightShape.test.ts +325 -0
- package/src/shapes/TLHighlightShape.ts +79 -4
- package/src/shapes/TLImageShape.test.ts +534 -0
- package/src/shapes/TLImageShape.ts +105 -5
- package/src/shapes/TLLineShape.test.ts +269 -0
- package/src/shapes/TLLineShape.ts +128 -8
- package/src/shapes/TLNoteShape.test.ts +1568 -0
- package/src/shapes/TLNoteShape.ts +97 -4
- package/src/shapes/TLTextShape.test.ts +407 -0
- package/src/shapes/TLTextShape.ts +94 -4
- package/src/shapes/TLVideoShape.test.ts +112 -0
- package/src/shapes/TLVideoShape.ts +99 -4
- package/src/store-migrations.test.ts +88 -0
- package/src/store-migrations.ts +47 -1
- package/src/styles/TLColorStyle.test.ts +439 -0
- package/src/styles/TLColorStyle.ts +228 -10
- package/src/styles/TLDashStyle.ts +54 -2
- package/src/styles/TLFillStyle.ts +54 -2
- package/src/styles/TLFontStyle.ts +72 -3
- package/src/styles/TLHorizontalAlignStyle.ts +55 -2
- package/src/styles/TLSizeStyle.ts +54 -2
- package/src/styles/TLTextAlignStyle.ts +52 -2
- package/src/styles/TLVerticalAlignStyle.ts +52 -2
- package/src/translations/translations.test.ts +378 -35
- package/src/translations/translations.ts +157 -10
- package/src/util-types.ts +51 -1
|
@@ -0,0 +1,1568 @@
|
|
|
1
|
+
import { T } from '@tldraw/validate'
|
|
2
|
+
import { describe, expect, it, test } from 'vitest'
|
|
3
|
+
import { getTestMigration } from '../__tests__/migrationTestUtils'
|
|
4
|
+
import { TLRichText, toRichText } from '../misc/TLRichText'
|
|
5
|
+
import { TLShapeId } from '../records/TLShape'
|
|
6
|
+
import { DefaultColorStyle, DefaultLabelColorStyle } from '../styles/TLColorStyle'
|
|
7
|
+
import { DefaultFontStyle } from '../styles/TLFontStyle'
|
|
8
|
+
import { DefaultHorizontalAlignStyle } from '../styles/TLHorizontalAlignStyle'
|
|
9
|
+
import { DefaultSizeStyle } from '../styles/TLSizeStyle'
|
|
10
|
+
import { DefaultVerticalAlignStyle } from '../styles/TLVerticalAlignStyle'
|
|
11
|
+
import {
|
|
12
|
+
TLNoteShape,
|
|
13
|
+
TLNoteShapeProps,
|
|
14
|
+
noteShapeMigrations,
|
|
15
|
+
noteShapeProps,
|
|
16
|
+
noteShapeVersions,
|
|
17
|
+
} from './TLNoteShape'
|
|
18
|
+
|
|
19
|
+
describe('TLNoteShape', () => {
|
|
20
|
+
describe('TLNoteShapeProps interface', () => {
|
|
21
|
+
it('should represent valid note shape properties', () => {
|
|
22
|
+
const validProps: TLNoteShapeProps = {
|
|
23
|
+
color: 'yellow',
|
|
24
|
+
labelColor: 'black',
|
|
25
|
+
size: 'm',
|
|
26
|
+
font: 'draw',
|
|
27
|
+
fontSizeAdjustment: 0,
|
|
28
|
+
align: 'middle',
|
|
29
|
+
verticalAlign: 'middle',
|
|
30
|
+
growY: 0,
|
|
31
|
+
url: '',
|
|
32
|
+
richText: toRichText('Hello World'),
|
|
33
|
+
scale: 1,
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
expect(validProps.color).toBe('yellow')
|
|
37
|
+
expect(validProps.labelColor).toBe('black')
|
|
38
|
+
expect(validProps.size).toBe('m')
|
|
39
|
+
expect(validProps.font).toBe('draw')
|
|
40
|
+
expect(validProps.fontSizeAdjustment).toBe(0)
|
|
41
|
+
expect(validProps.align).toBe('middle')
|
|
42
|
+
expect(validProps.verticalAlign).toBe('middle')
|
|
43
|
+
expect(validProps.growY).toBe(0)
|
|
44
|
+
expect(validProps.url).toBe('')
|
|
45
|
+
expect(validProps.richText).toBeDefined()
|
|
46
|
+
expect(validProps.scale).toBe(1)
|
|
47
|
+
})
|
|
48
|
+
|
|
49
|
+
it('should support different color combinations', () => {
|
|
50
|
+
const colorCombinations = [
|
|
51
|
+
{ color: 'black' as const, labelColor: 'white' as const },
|
|
52
|
+
{ color: 'red' as const, labelColor: 'black' as const },
|
|
53
|
+
{ color: 'blue' as const, labelColor: 'yellow' as const },
|
|
54
|
+
{ color: 'green' as const, labelColor: 'red' as const },
|
|
55
|
+
{ color: 'light-blue' as const, labelColor: 'black' as const },
|
|
56
|
+
]
|
|
57
|
+
|
|
58
|
+
colorCombinations.forEach(({ color, labelColor }) => {
|
|
59
|
+
const props: Partial<TLNoteShapeProps> = {
|
|
60
|
+
color,
|
|
61
|
+
labelColor,
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
expect(props.color).toBe(color)
|
|
65
|
+
expect(props.labelColor).toBe(labelColor)
|
|
66
|
+
})
|
|
67
|
+
})
|
|
68
|
+
|
|
69
|
+
it('should support different size variations', () => {
|
|
70
|
+
const sizes = ['s', 'm', 'l', 'xl'] as const
|
|
71
|
+
|
|
72
|
+
sizes.forEach((size) => {
|
|
73
|
+
const props: Partial<TLNoteShapeProps> = { size }
|
|
74
|
+
expect(props.size).toBe(size)
|
|
75
|
+
})
|
|
76
|
+
})
|
|
77
|
+
|
|
78
|
+
it('should support different font styles', () => {
|
|
79
|
+
const fonts = ['draw', 'sans', 'serif', 'mono'] as const
|
|
80
|
+
|
|
81
|
+
fonts.forEach((font) => {
|
|
82
|
+
const props: Partial<TLNoteShapeProps> = { font }
|
|
83
|
+
expect(props.font).toBe(font)
|
|
84
|
+
})
|
|
85
|
+
})
|
|
86
|
+
|
|
87
|
+
it('should support different alignment combinations', () => {
|
|
88
|
+
const alignmentCombinations: Array<
|
|
89
|
+
[TLNoteShapeProps['align'], TLNoteShapeProps['verticalAlign']]
|
|
90
|
+
> = [
|
|
91
|
+
['start', 'start'],
|
|
92
|
+
['middle', 'middle'],
|
|
93
|
+
['end', 'end'],
|
|
94
|
+
['start-legacy', 'start'],
|
|
95
|
+
['middle-legacy', 'middle'],
|
|
96
|
+
['end-legacy', 'end'],
|
|
97
|
+
]
|
|
98
|
+
|
|
99
|
+
alignmentCombinations.forEach(([align, verticalAlign]) => {
|
|
100
|
+
const props: Partial<TLNoteShapeProps> = {
|
|
101
|
+
align,
|
|
102
|
+
verticalAlign,
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
expect(props.align).toBe(align)
|
|
106
|
+
expect(props.verticalAlign).toBe(verticalAlign)
|
|
107
|
+
})
|
|
108
|
+
})
|
|
109
|
+
|
|
110
|
+
it('should support font size adjustments', () => {
|
|
111
|
+
const fontAdjustments = [-2, -1, 0, 1, 2, 5, 10]
|
|
112
|
+
|
|
113
|
+
fontAdjustments.forEach((adjustment) => {
|
|
114
|
+
const props: Partial<TLNoteShapeProps> = {
|
|
115
|
+
fontSizeAdjustment: adjustment,
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
expect(props.fontSizeAdjustment).toBe(adjustment)
|
|
119
|
+
})
|
|
120
|
+
})
|
|
121
|
+
|
|
122
|
+
it('should support growY values for height expansion', () => {
|
|
123
|
+
const growYValues = [0, 10, 25, 50, 100, 200]
|
|
124
|
+
|
|
125
|
+
growYValues.forEach((growY) => {
|
|
126
|
+
const props: Partial<TLNoteShapeProps> = { growY }
|
|
127
|
+
expect(props.growY).toBe(growY)
|
|
128
|
+
})
|
|
129
|
+
})
|
|
130
|
+
|
|
131
|
+
it('should support scale variations', () => {
|
|
132
|
+
const scaleValues = [0.5, 0.8, 1, 1.2, 1.5, 2, 3]
|
|
133
|
+
|
|
134
|
+
scaleValues.forEach((scale) => {
|
|
135
|
+
const props: Partial<TLNoteShapeProps> = { scale }
|
|
136
|
+
expect(props.scale).toBe(scale)
|
|
137
|
+
})
|
|
138
|
+
})
|
|
139
|
+
|
|
140
|
+
it('should support rich text content', () => {
|
|
141
|
+
const richTexts = [
|
|
142
|
+
toRichText(''),
|
|
143
|
+
toRichText('Simple note'),
|
|
144
|
+
toRichText('**Bold** note'),
|
|
145
|
+
toRichText('*Italic* text'),
|
|
146
|
+
toRichText('Multiple\nLines\nNote'),
|
|
147
|
+
toRichText('Complex **bold** and *italic* formatting'),
|
|
148
|
+
]
|
|
149
|
+
|
|
150
|
+
richTexts.forEach((richText) => {
|
|
151
|
+
const props: Partial<TLNoteShapeProps> = { richText }
|
|
152
|
+
expect(props.richText).toBe(richText)
|
|
153
|
+
})
|
|
154
|
+
})
|
|
155
|
+
|
|
156
|
+
it('should support URL links', () => {
|
|
157
|
+
const urlVariations = [
|
|
158
|
+
'',
|
|
159
|
+
'https://tldraw.com',
|
|
160
|
+
'http://example.com',
|
|
161
|
+
'https://subdomain.example.com/path',
|
|
162
|
+
'https://example.com/path?query=value#anchor',
|
|
163
|
+
]
|
|
164
|
+
|
|
165
|
+
urlVariations.forEach((url) => {
|
|
166
|
+
const props: Partial<TLNoteShapeProps> = { url }
|
|
167
|
+
expect(props.url).toBe(url)
|
|
168
|
+
})
|
|
169
|
+
})
|
|
170
|
+
})
|
|
171
|
+
|
|
172
|
+
describe('TLNoteShape type', () => {
|
|
173
|
+
it('should represent complete note shape records', () => {
|
|
174
|
+
const validNoteShape: TLNoteShape = {
|
|
175
|
+
id: 'shape:note123' as TLShapeId,
|
|
176
|
+
typeName: 'shape',
|
|
177
|
+
type: 'note',
|
|
178
|
+
x: 100,
|
|
179
|
+
y: 200,
|
|
180
|
+
rotation: 0,
|
|
181
|
+
index: 'a1' as any,
|
|
182
|
+
parentId: 'page:main' as any,
|
|
183
|
+
isLocked: false,
|
|
184
|
+
opacity: 1,
|
|
185
|
+
props: {
|
|
186
|
+
color: 'yellow',
|
|
187
|
+
labelColor: 'black',
|
|
188
|
+
size: 's',
|
|
189
|
+
font: 'sans',
|
|
190
|
+
fontSizeAdjustment: 2,
|
|
191
|
+
align: 'start',
|
|
192
|
+
verticalAlign: 'start',
|
|
193
|
+
growY: 50,
|
|
194
|
+
url: 'https://example.com',
|
|
195
|
+
richText: toRichText('Important **note**!'),
|
|
196
|
+
scale: 1,
|
|
197
|
+
},
|
|
198
|
+
meta: {},
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
expect(validNoteShape.type).toBe('note')
|
|
202
|
+
expect(validNoteShape.typeName).toBe('shape')
|
|
203
|
+
expect(validNoteShape.props.color).toBe('yellow')
|
|
204
|
+
expect(validNoteShape.props.labelColor).toBe('black')
|
|
205
|
+
expect(validNoteShape.props.size).toBe('s')
|
|
206
|
+
expect(validNoteShape.props.font).toBe('sans')
|
|
207
|
+
})
|
|
208
|
+
|
|
209
|
+
it('should support different note configurations', () => {
|
|
210
|
+
const configurations = [
|
|
211
|
+
{
|
|
212
|
+
color: 'light-blue' as const,
|
|
213
|
+
labelColor: 'black' as const,
|
|
214
|
+
size: 's' as const,
|
|
215
|
+
font: 'draw' as const,
|
|
216
|
+
fontSizeAdjustment: 0,
|
|
217
|
+
align: 'start' as const,
|
|
218
|
+
verticalAlign: 'start' as const,
|
|
219
|
+
growY: 0,
|
|
220
|
+
scale: 1,
|
|
221
|
+
},
|
|
222
|
+
{
|
|
223
|
+
color: 'red' as const,
|
|
224
|
+
labelColor: 'white' as const,
|
|
225
|
+
size: 'l' as const,
|
|
226
|
+
font: 'serif' as const,
|
|
227
|
+
fontSizeAdjustment: 5,
|
|
228
|
+
align: 'middle' as const,
|
|
229
|
+
verticalAlign: 'middle' as const,
|
|
230
|
+
growY: 25,
|
|
231
|
+
scale: 1.5,
|
|
232
|
+
},
|
|
233
|
+
{
|
|
234
|
+
color: 'green' as const,
|
|
235
|
+
labelColor: 'black' as const,
|
|
236
|
+
size: 'xl' as const,
|
|
237
|
+
font: 'mono' as const,
|
|
238
|
+
fontSizeAdjustment: -1,
|
|
239
|
+
align: 'end' as const,
|
|
240
|
+
verticalAlign: 'end' as const,
|
|
241
|
+
growY: 100,
|
|
242
|
+
scale: 0.8,
|
|
243
|
+
},
|
|
244
|
+
]
|
|
245
|
+
|
|
246
|
+
configurations.forEach((config, index) => {
|
|
247
|
+
const shape: TLNoteShape = {
|
|
248
|
+
id: `shape:note${index}` as TLShapeId,
|
|
249
|
+
typeName: 'shape',
|
|
250
|
+
type: 'note',
|
|
251
|
+
x: index * 100,
|
|
252
|
+
y: index * 50,
|
|
253
|
+
rotation: 0,
|
|
254
|
+
index: `a${index}` as any,
|
|
255
|
+
parentId: 'page:main' as any,
|
|
256
|
+
isLocked: false,
|
|
257
|
+
opacity: 1,
|
|
258
|
+
props: {
|
|
259
|
+
color: config.color,
|
|
260
|
+
labelColor: config.labelColor,
|
|
261
|
+
size: config.size,
|
|
262
|
+
font: config.font,
|
|
263
|
+
fontSizeAdjustment: config.fontSizeAdjustment,
|
|
264
|
+
align: config.align,
|
|
265
|
+
verticalAlign: config.verticalAlign,
|
|
266
|
+
growY: config.growY,
|
|
267
|
+
url: '',
|
|
268
|
+
richText: toRichText(`Note ${index + 1}`),
|
|
269
|
+
scale: config.scale,
|
|
270
|
+
},
|
|
271
|
+
meta: {},
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
expect(shape.props.color).toBe(config.color)
|
|
275
|
+
expect(shape.props.labelColor).toBe(config.labelColor)
|
|
276
|
+
expect(shape.props.size).toBe(config.size)
|
|
277
|
+
expect(shape.props.font).toBe(config.font)
|
|
278
|
+
expect(shape.props.fontSizeAdjustment).toBe(config.fontSizeAdjustment)
|
|
279
|
+
})
|
|
280
|
+
})
|
|
281
|
+
|
|
282
|
+
it('should support notes with different text content', () => {
|
|
283
|
+
const textVariations = [
|
|
284
|
+
'',
|
|
285
|
+
'Quick note',
|
|
286
|
+
'Multi\nLine\nNote',
|
|
287
|
+
'Note with **formatting**',
|
|
288
|
+
'Long note with multiple paragraphs and detailed content',
|
|
289
|
+
]
|
|
290
|
+
|
|
291
|
+
textVariations.forEach((text, index) => {
|
|
292
|
+
const shape: TLNoteShape = {
|
|
293
|
+
id: `shape:text_note${index}` as TLShapeId,
|
|
294
|
+
typeName: 'shape',
|
|
295
|
+
type: 'note',
|
|
296
|
+
x: 0,
|
|
297
|
+
y: 0,
|
|
298
|
+
rotation: 0,
|
|
299
|
+
index: `a${index}` as any,
|
|
300
|
+
parentId: 'page:main' as any,
|
|
301
|
+
isLocked: false,
|
|
302
|
+
opacity: 1,
|
|
303
|
+
props: {
|
|
304
|
+
color: 'yellow',
|
|
305
|
+
labelColor: 'black',
|
|
306
|
+
size: 'm',
|
|
307
|
+
font: 'draw',
|
|
308
|
+
fontSizeAdjustment: 0,
|
|
309
|
+
align: 'middle',
|
|
310
|
+
verticalAlign: 'middle',
|
|
311
|
+
growY: index * 20, // Different growY for text expansion
|
|
312
|
+
url: '',
|
|
313
|
+
richText: toRichText(text),
|
|
314
|
+
scale: 1,
|
|
315
|
+
},
|
|
316
|
+
meta: {},
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
expect(shape.props.richText).toBeDefined()
|
|
320
|
+
expect(shape.props.growY).toBe(index * 20)
|
|
321
|
+
})
|
|
322
|
+
})
|
|
323
|
+
|
|
324
|
+
it('should support locked and transparent notes', () => {
|
|
325
|
+
const shape: TLNoteShape = {
|
|
326
|
+
id: 'shape:special_note' as TLShapeId,
|
|
327
|
+
typeName: 'shape',
|
|
328
|
+
type: 'note',
|
|
329
|
+
x: 50,
|
|
330
|
+
y: 75,
|
|
331
|
+
rotation: 1.57, // 90 degrees
|
|
332
|
+
index: 'b1' as any,
|
|
333
|
+
parentId: 'page:test' as any,
|
|
334
|
+
isLocked: true,
|
|
335
|
+
opacity: 0.5,
|
|
336
|
+
props: {
|
|
337
|
+
color: 'red',
|
|
338
|
+
labelColor: 'white',
|
|
339
|
+
size: 'l',
|
|
340
|
+
font: 'serif',
|
|
341
|
+
fontSizeAdjustment: 3,
|
|
342
|
+
align: 'end',
|
|
343
|
+
verticalAlign: 'start',
|
|
344
|
+
growY: 30,
|
|
345
|
+
url: 'https://important-note.com',
|
|
346
|
+
richText: toRichText('Locked note'),
|
|
347
|
+
scale: 0.9,
|
|
348
|
+
},
|
|
349
|
+
meta: { priority: 'high' },
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
expect(shape.isLocked).toBe(true)
|
|
353
|
+
expect(shape.opacity).toBe(0.5)
|
|
354
|
+
expect(shape.rotation).toBe(1.57)
|
|
355
|
+
expect(shape.meta).toEqual({ priority: 'high' })
|
|
356
|
+
})
|
|
357
|
+
})
|
|
358
|
+
|
|
359
|
+
describe('noteShapeProps validation schema', () => {
|
|
360
|
+
it('should validate all note shape properties', () => {
|
|
361
|
+
const validProps = {
|
|
362
|
+
color: 'blue',
|
|
363
|
+
labelColor: 'red',
|
|
364
|
+
size: 'l',
|
|
365
|
+
font: 'sans',
|
|
366
|
+
fontSizeAdjustment: 3,
|
|
367
|
+
align: 'start',
|
|
368
|
+
verticalAlign: 'end',
|
|
369
|
+
growY: 15,
|
|
370
|
+
url: 'https://example.com',
|
|
371
|
+
richText: toRichText('Test note'),
|
|
372
|
+
scale: 1.2,
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
// Validate each property individually
|
|
376
|
+
expect(() => noteShapeProps.color.validate(validProps.color)).not.toThrow()
|
|
377
|
+
expect(() => noteShapeProps.labelColor.validate(validProps.labelColor)).not.toThrow()
|
|
378
|
+
expect(() => noteShapeProps.size.validate(validProps.size)).not.toThrow()
|
|
379
|
+
expect(() => noteShapeProps.font.validate(validProps.font)).not.toThrow()
|
|
380
|
+
expect(() =>
|
|
381
|
+
noteShapeProps.fontSizeAdjustment.validate(validProps.fontSizeAdjustment)
|
|
382
|
+
).not.toThrow()
|
|
383
|
+
expect(() => noteShapeProps.align.validate(validProps.align)).not.toThrow()
|
|
384
|
+
expect(() => noteShapeProps.verticalAlign.validate(validProps.verticalAlign)).not.toThrow()
|
|
385
|
+
expect(() => noteShapeProps.growY.validate(validProps.growY)).not.toThrow()
|
|
386
|
+
expect(() => noteShapeProps.url.validate(validProps.url)).not.toThrow()
|
|
387
|
+
expect(() => noteShapeProps.richText.validate(validProps.richText)).not.toThrow()
|
|
388
|
+
expect(() => noteShapeProps.scale.validate(validProps.scale)).not.toThrow()
|
|
389
|
+
})
|
|
390
|
+
|
|
391
|
+
it('should validate using comprehensive object validator', () => {
|
|
392
|
+
const fullValidator = T.object(noteShapeProps)
|
|
393
|
+
|
|
394
|
+
const validPropsObject = {
|
|
395
|
+
color: 'green' as const,
|
|
396
|
+
labelColor: 'black' as const,
|
|
397
|
+
size: 's' as const,
|
|
398
|
+
font: 'mono' as const,
|
|
399
|
+
fontSizeAdjustment: 2,
|
|
400
|
+
align: 'end' as const,
|
|
401
|
+
verticalAlign: 'start' as const,
|
|
402
|
+
growY: 25,
|
|
403
|
+
url: 'https://test.com',
|
|
404
|
+
richText: toRichText('Note test') as TLRichText,
|
|
405
|
+
scale: 0.8,
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
expect(() => fullValidator.validate(validPropsObject)).not.toThrow()
|
|
409
|
+
const result = fullValidator.validate(validPropsObject)
|
|
410
|
+
expect(result).toEqual(validPropsObject)
|
|
411
|
+
})
|
|
412
|
+
|
|
413
|
+
it('should validate fontSizeAdjustment as positiveNumber', () => {
|
|
414
|
+
// Valid positive numbers (including zero)
|
|
415
|
+
const validAdjustments = [0, 1, 2, 5, 10, 0.5]
|
|
416
|
+
|
|
417
|
+
validAdjustments.forEach((adjustment) => {
|
|
418
|
+
expect(() => noteShapeProps.fontSizeAdjustment.validate(adjustment)).not.toThrow()
|
|
419
|
+
})
|
|
420
|
+
|
|
421
|
+
// Invalid adjustments (negative numbers and non-numbers)
|
|
422
|
+
const invalidAdjustments = [-1, -0.1, -5, 'not-number', null, undefined, {}, [], true, false]
|
|
423
|
+
|
|
424
|
+
invalidAdjustments.forEach((adjustment) => {
|
|
425
|
+
expect(() => noteShapeProps.fontSizeAdjustment.validate(adjustment)).toThrow()
|
|
426
|
+
})
|
|
427
|
+
})
|
|
428
|
+
|
|
429
|
+
it('should validate growY as positiveNumber', () => {
|
|
430
|
+
// Valid positive numbers (including zero)
|
|
431
|
+
const validGrowY = [0, 0.1, 1, 5, 50, 100]
|
|
432
|
+
|
|
433
|
+
validGrowY.forEach((growY) => {
|
|
434
|
+
expect(() => noteShapeProps.growY.validate(growY)).not.toThrow()
|
|
435
|
+
})
|
|
436
|
+
|
|
437
|
+
// Invalid growY values (negative numbers and non-numbers)
|
|
438
|
+
const invalidGrowY = [-1, -0.1, -100, 'not-number', null, undefined, {}, [], true, false]
|
|
439
|
+
|
|
440
|
+
invalidGrowY.forEach((growY) => {
|
|
441
|
+
expect(() => noteShapeProps.growY.validate(growY)).toThrow()
|
|
442
|
+
})
|
|
443
|
+
})
|
|
444
|
+
|
|
445
|
+
it('should validate scale as nonZeroNumber', () => {
|
|
446
|
+
// Valid non-zero positive numbers
|
|
447
|
+
const validScales = [0.1, 0.5, 1, 1.5, 2, 10]
|
|
448
|
+
|
|
449
|
+
validScales.forEach((scale) => {
|
|
450
|
+
expect(() => noteShapeProps.scale.validate(scale)).not.toThrow()
|
|
451
|
+
})
|
|
452
|
+
|
|
453
|
+
// Invalid scales (zero, negative numbers, and non-numbers)
|
|
454
|
+
const invalidScales = [0, -0.5, -1, -2, 'not-number', null, undefined, {}, [], true, false]
|
|
455
|
+
|
|
456
|
+
invalidScales.forEach((scale) => {
|
|
457
|
+
expect(() => noteShapeProps.scale.validate(scale)).toThrow()
|
|
458
|
+
})
|
|
459
|
+
})
|
|
460
|
+
|
|
461
|
+
it('should validate URLs using linkUrl validator', () => {
|
|
462
|
+
const validUrls = [
|
|
463
|
+
'',
|
|
464
|
+
'https://example.com',
|
|
465
|
+
'http://test.com',
|
|
466
|
+
'https://subdomain.example.com/path',
|
|
467
|
+
'https://example.com/path?query=value#anchor',
|
|
468
|
+
]
|
|
469
|
+
|
|
470
|
+
validUrls.forEach((url) => {
|
|
471
|
+
expect(() => noteShapeProps.url.validate(url)).not.toThrow()
|
|
472
|
+
})
|
|
473
|
+
|
|
474
|
+
// Invalid URLs should be handled by linkUrl validator
|
|
475
|
+
const invalidUrls = [
|
|
476
|
+
'not-a-url',
|
|
477
|
+
'ftp://example.com', // May be invalid depending on linkUrl implementation
|
|
478
|
+
null,
|
|
479
|
+
undefined,
|
|
480
|
+
123,
|
|
481
|
+
{},
|
|
482
|
+
[],
|
|
483
|
+
]
|
|
484
|
+
|
|
485
|
+
invalidUrls.forEach((url) => {
|
|
486
|
+
expect(() => noteShapeProps.url.validate(url)).toThrow()
|
|
487
|
+
})
|
|
488
|
+
})
|
|
489
|
+
|
|
490
|
+
it('should validate rich text property', () => {
|
|
491
|
+
const validRichTexts = [
|
|
492
|
+
toRichText(''),
|
|
493
|
+
toRichText('Simple text'),
|
|
494
|
+
toRichText('**Bold** text'),
|
|
495
|
+
toRichText('*Italic* and **bold**'),
|
|
496
|
+
toRichText('Multiple\nlines\nof\ntext'),
|
|
497
|
+
]
|
|
498
|
+
|
|
499
|
+
validRichTexts.forEach((richText) => {
|
|
500
|
+
expect(() => noteShapeProps.richText.validate(richText)).not.toThrow()
|
|
501
|
+
})
|
|
502
|
+
|
|
503
|
+
const invalidRichTexts = [
|
|
504
|
+
'plain string', // Not a TLRichText object
|
|
505
|
+
null,
|
|
506
|
+
undefined,
|
|
507
|
+
123,
|
|
508
|
+
{},
|
|
509
|
+
[],
|
|
510
|
+
{ invalid: 'structure' },
|
|
511
|
+
]
|
|
512
|
+
|
|
513
|
+
invalidRichTexts.forEach((richText) => {
|
|
514
|
+
expect(() => noteShapeProps.richText.validate(richText)).toThrow()
|
|
515
|
+
})
|
|
516
|
+
})
|
|
517
|
+
|
|
518
|
+
it('should use correct default style validators', () => {
|
|
519
|
+
// Verify that the props schema uses the expected style validators
|
|
520
|
+
expect(noteShapeProps.color).toBe(DefaultColorStyle)
|
|
521
|
+
expect(noteShapeProps.labelColor).toBe(DefaultLabelColorStyle)
|
|
522
|
+
expect(noteShapeProps.size).toBe(DefaultSizeStyle)
|
|
523
|
+
expect(noteShapeProps.font).toBe(DefaultFontStyle)
|
|
524
|
+
expect(noteShapeProps.align).toBe(DefaultHorizontalAlignStyle)
|
|
525
|
+
expect(noteShapeProps.verticalAlign).toBe(DefaultVerticalAlignStyle)
|
|
526
|
+
})
|
|
527
|
+
|
|
528
|
+
it('should validate fontSizeAdjustment with T.positiveNumber', () => {
|
|
529
|
+
expect(noteShapeProps.fontSizeAdjustment).toBe(T.positiveNumber)
|
|
530
|
+
})
|
|
531
|
+
|
|
532
|
+
it('should validate growY with T.positiveNumber', () => {
|
|
533
|
+
expect(noteShapeProps.growY).toBe(T.positiveNumber)
|
|
534
|
+
})
|
|
535
|
+
|
|
536
|
+
it('should validate scale with T.nonZeroNumber', () => {
|
|
537
|
+
expect(noteShapeProps.scale).toBe(T.nonZeroNumber)
|
|
538
|
+
})
|
|
539
|
+
|
|
540
|
+
it('should validate url with T.linkUrl', () => {
|
|
541
|
+
expect(noteShapeProps.url).toBe(T.linkUrl)
|
|
542
|
+
})
|
|
543
|
+
})
|
|
544
|
+
|
|
545
|
+
describe('noteShapeVersions', () => {
|
|
546
|
+
it('should contain expected migration version IDs', () => {
|
|
547
|
+
expect(noteShapeVersions).toBeDefined()
|
|
548
|
+
expect(typeof noteShapeVersions).toBe('object')
|
|
549
|
+
})
|
|
550
|
+
|
|
551
|
+
it('should have all expected migration versions', () => {
|
|
552
|
+
const expectedVersions: Array<keyof typeof noteShapeVersions> = [
|
|
553
|
+
'AddUrlProp',
|
|
554
|
+
'RemoveJustify',
|
|
555
|
+
'MigrateLegacyAlign',
|
|
556
|
+
'AddVerticalAlign',
|
|
557
|
+
'MakeUrlsValid',
|
|
558
|
+
'AddFontSizeAdjustment',
|
|
559
|
+
'AddScale',
|
|
560
|
+
'AddLabelColor',
|
|
561
|
+
'AddRichText',
|
|
562
|
+
]
|
|
563
|
+
|
|
564
|
+
expectedVersions.forEach((version) => {
|
|
565
|
+
expect(noteShapeVersions[version]).toBeDefined()
|
|
566
|
+
expect(typeof noteShapeVersions[version]).toBe('string')
|
|
567
|
+
})
|
|
568
|
+
})
|
|
569
|
+
|
|
570
|
+
it('should have properly formatted migration IDs', () => {
|
|
571
|
+
Object.values(noteShapeVersions).forEach((versionId) => {
|
|
572
|
+
expect(versionId).toMatch(/^com\.tldraw\.shape\.note\//)
|
|
573
|
+
expect(versionId).toMatch(/\/\d+$/) // Should end with /number
|
|
574
|
+
})
|
|
575
|
+
})
|
|
576
|
+
|
|
577
|
+
it('should contain note in migration IDs', () => {
|
|
578
|
+
Object.values(noteShapeVersions).forEach((versionId) => {
|
|
579
|
+
expect(versionId).toContain('note')
|
|
580
|
+
})
|
|
581
|
+
})
|
|
582
|
+
|
|
583
|
+
it('should have unique version IDs', () => {
|
|
584
|
+
const versionIds = Object.values(noteShapeVersions)
|
|
585
|
+
const uniqueIds = new Set(versionIds)
|
|
586
|
+
expect(uniqueIds.size).toBe(versionIds.length)
|
|
587
|
+
})
|
|
588
|
+
})
|
|
589
|
+
|
|
590
|
+
describe('noteShapeMigrations', () => {
|
|
591
|
+
it('should be defined and have required structure', () => {
|
|
592
|
+
expect(noteShapeMigrations).toBeDefined()
|
|
593
|
+
expect(noteShapeMigrations.sequence).toBeDefined()
|
|
594
|
+
expect(Array.isArray(noteShapeMigrations.sequence)).toBe(true)
|
|
595
|
+
})
|
|
596
|
+
|
|
597
|
+
it('should have migrations for all version IDs', () => {
|
|
598
|
+
const migrationIds = noteShapeMigrations.sequence
|
|
599
|
+
.filter((migration) => 'id' in migration)
|
|
600
|
+
.map((migration) => ('id' in migration ? migration.id : null))
|
|
601
|
+
.filter(Boolean)
|
|
602
|
+
|
|
603
|
+
const versionIds = Object.values(noteShapeVersions)
|
|
604
|
+
|
|
605
|
+
versionIds.forEach((versionId) => {
|
|
606
|
+
expect(migrationIds).toContain(versionId)
|
|
607
|
+
})
|
|
608
|
+
})
|
|
609
|
+
|
|
610
|
+
it('should have correct number of migrations in sequence', () => {
|
|
611
|
+
// Should have at least as many migrations as version IDs
|
|
612
|
+
expect(noteShapeMigrations.sequence.length).toBeGreaterThanOrEqual(
|
|
613
|
+
Object.keys(noteShapeVersions).length
|
|
614
|
+
)
|
|
615
|
+
})
|
|
616
|
+
})
|
|
617
|
+
|
|
618
|
+
describe('noteShapeMigrations - AddUrlProp migration', () => {
|
|
619
|
+
const { up, down } = getTestMigration(noteShapeVersions.AddUrlProp)
|
|
620
|
+
|
|
621
|
+
describe('AddUrlProp up migration', () => {
|
|
622
|
+
it('should add url property with empty string default', () => {
|
|
623
|
+
const oldRecord = {
|
|
624
|
+
id: 'shape:note1',
|
|
625
|
+
typeName: 'shape',
|
|
626
|
+
type: 'note',
|
|
627
|
+
x: 100,
|
|
628
|
+
y: 200,
|
|
629
|
+
rotation: 0,
|
|
630
|
+
index: 'a1',
|
|
631
|
+
parentId: 'page:main',
|
|
632
|
+
isLocked: false,
|
|
633
|
+
opacity: 1,
|
|
634
|
+
props: {
|
|
635
|
+
color: 'yellow',
|
|
636
|
+
labelColor: 'black',
|
|
637
|
+
size: 'm',
|
|
638
|
+
},
|
|
639
|
+
meta: {},
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
const result = up(oldRecord)
|
|
643
|
+
expect(result.props.url).toBe('')
|
|
644
|
+
expect(result.props.color).toBe('yellow') // Preserve other props
|
|
645
|
+
})
|
|
646
|
+
|
|
647
|
+
it('should preserve all existing properties during migration', () => {
|
|
648
|
+
const oldRecord = {
|
|
649
|
+
id: 'shape:note2',
|
|
650
|
+
props: {
|
|
651
|
+
color: 'blue',
|
|
652
|
+
labelColor: 'red',
|
|
653
|
+
size: 'l',
|
|
654
|
+
font: 'sans',
|
|
655
|
+
fontSizeAdjustment: 2,
|
|
656
|
+
align: 'start',
|
|
657
|
+
verticalAlign: 'middle',
|
|
658
|
+
growY: 25,
|
|
659
|
+
scale: 1.5,
|
|
660
|
+
},
|
|
661
|
+
}
|
|
662
|
+
|
|
663
|
+
const result = up(oldRecord)
|
|
664
|
+
expect(result.props.url).toBe('')
|
|
665
|
+
expect(result.props.color).toBe('blue')
|
|
666
|
+
expect(result.props.labelColor).toBe('red')
|
|
667
|
+
expect(result.props.size).toBe('l')
|
|
668
|
+
expect(result.props.fontSizeAdjustment).toBe(2)
|
|
669
|
+
})
|
|
670
|
+
})
|
|
671
|
+
|
|
672
|
+
describe('AddUrlProp down migration', () => {
|
|
673
|
+
it('should be retired (no down migration)', () => {
|
|
674
|
+
expect(() => {
|
|
675
|
+
down({})
|
|
676
|
+
}).toThrow('Migration com.tldraw.shape.note/1 does not have a down function')
|
|
677
|
+
})
|
|
678
|
+
})
|
|
679
|
+
})
|
|
680
|
+
|
|
681
|
+
describe('noteShapeMigrations - RemoveJustify migration', () => {
|
|
682
|
+
const { up, down } = getTestMigration(noteShapeVersions.RemoveJustify)
|
|
683
|
+
|
|
684
|
+
describe('RemoveJustify up migration', () => {
|
|
685
|
+
it('should convert justify alignment to start', () => {
|
|
686
|
+
const oldRecord = {
|
|
687
|
+
id: 'shape:note1',
|
|
688
|
+
props: {
|
|
689
|
+
color: 'yellow',
|
|
690
|
+
align: 'justify',
|
|
691
|
+
labelColor: 'black',
|
|
692
|
+
},
|
|
693
|
+
}
|
|
694
|
+
|
|
695
|
+
const result = up(oldRecord)
|
|
696
|
+
expect(result.props.align).toBe('start')
|
|
697
|
+
expect(result.props.color).toBe('yellow') // Preserve other props
|
|
698
|
+
})
|
|
699
|
+
|
|
700
|
+
it('should preserve non-justify alignments', () => {
|
|
701
|
+
const alignments = ['start', 'middle', 'end']
|
|
702
|
+
|
|
703
|
+
alignments.forEach((align) => {
|
|
704
|
+
const oldRecord = {
|
|
705
|
+
id: 'shape:note1',
|
|
706
|
+
props: {
|
|
707
|
+
color: 'red',
|
|
708
|
+
align,
|
|
709
|
+
labelColor: 'black',
|
|
710
|
+
},
|
|
711
|
+
}
|
|
712
|
+
|
|
713
|
+
const result = up(oldRecord)
|
|
714
|
+
expect(result.props.align).toBe(align)
|
|
715
|
+
})
|
|
716
|
+
})
|
|
717
|
+
|
|
718
|
+
it('should preserve all other properties during migration', () => {
|
|
719
|
+
const oldRecord = {
|
|
720
|
+
id: 'shape:note1',
|
|
721
|
+
props: {
|
|
722
|
+
color: 'green',
|
|
723
|
+
align: 'justify',
|
|
724
|
+
labelColor: 'white',
|
|
725
|
+
size: 'l',
|
|
726
|
+
font: 'serif',
|
|
727
|
+
fontSizeAdjustment: 1,
|
|
728
|
+
verticalAlign: 'start',
|
|
729
|
+
growY: 30,
|
|
730
|
+
scale: 0.8,
|
|
731
|
+
},
|
|
732
|
+
}
|
|
733
|
+
|
|
734
|
+
const result = up(oldRecord)
|
|
735
|
+
expect(result.props.align).toBe('start')
|
|
736
|
+
expect(result.props.color).toBe('green')
|
|
737
|
+
expect(result.props.labelColor).toBe('white')
|
|
738
|
+
expect(result.props.size).toBe('l')
|
|
739
|
+
expect(result.props.font).toBe('serif')
|
|
740
|
+
})
|
|
741
|
+
})
|
|
742
|
+
|
|
743
|
+
describe('RemoveJustify down migration', () => {
|
|
744
|
+
it('should be retired (no down migration)', () => {
|
|
745
|
+
expect(() => {
|
|
746
|
+
down({})
|
|
747
|
+
}).toThrow('Migration com.tldraw.shape.note/2 does not have a down function')
|
|
748
|
+
})
|
|
749
|
+
})
|
|
750
|
+
})
|
|
751
|
+
|
|
752
|
+
describe('noteShapeMigrations - MigrateLegacyAlign migration', () => {
|
|
753
|
+
const { up, down } = getTestMigration(noteShapeVersions.MigrateLegacyAlign)
|
|
754
|
+
|
|
755
|
+
describe('MigrateLegacyAlign up migration', () => {
|
|
756
|
+
it('should convert start to start-legacy', () => {
|
|
757
|
+
const oldRecord = {
|
|
758
|
+
id: 'shape:note1',
|
|
759
|
+
props: {
|
|
760
|
+
color: 'yellow',
|
|
761
|
+
align: 'start',
|
|
762
|
+
verticalAlign: 'middle',
|
|
763
|
+
},
|
|
764
|
+
}
|
|
765
|
+
|
|
766
|
+
const result = up(oldRecord)
|
|
767
|
+
expect(result.props.align).toBe('start-legacy')
|
|
768
|
+
expect(result.props.verticalAlign).toBe('middle')
|
|
769
|
+
})
|
|
770
|
+
|
|
771
|
+
it('should convert end to end-legacy', () => {
|
|
772
|
+
const oldRecord = {
|
|
773
|
+
id: 'shape:note1',
|
|
774
|
+
props: {
|
|
775
|
+
color: 'blue',
|
|
776
|
+
align: 'end',
|
|
777
|
+
verticalAlign: 'start',
|
|
778
|
+
},
|
|
779
|
+
}
|
|
780
|
+
|
|
781
|
+
const result = up(oldRecord)
|
|
782
|
+
expect(result.props.align).toBe('end-legacy')
|
|
783
|
+
expect(result.props.verticalAlign).toBe('start')
|
|
784
|
+
})
|
|
785
|
+
|
|
786
|
+
it('should convert middle to middle-legacy', () => {
|
|
787
|
+
const oldRecord = {
|
|
788
|
+
id: 'shape:note1',
|
|
789
|
+
props: {
|
|
790
|
+
color: 'red',
|
|
791
|
+
align: 'middle',
|
|
792
|
+
verticalAlign: 'end',
|
|
793
|
+
},
|
|
794
|
+
}
|
|
795
|
+
|
|
796
|
+
const result = up(oldRecord)
|
|
797
|
+
expect(result.props.align).toBe('middle-legacy')
|
|
798
|
+
expect(result.props.verticalAlign).toBe('end')
|
|
799
|
+
})
|
|
800
|
+
|
|
801
|
+
it('should handle other alignment values as middle-legacy', () => {
|
|
802
|
+
const oldRecord = {
|
|
803
|
+
id: 'shape:note1',
|
|
804
|
+
props: {
|
|
805
|
+
color: 'green',
|
|
806
|
+
align: 'unknown-align',
|
|
807
|
+
verticalAlign: 'middle',
|
|
808
|
+
},
|
|
809
|
+
}
|
|
810
|
+
|
|
811
|
+
const result = up(oldRecord)
|
|
812
|
+
expect(result.props.align).toBe('middle-legacy')
|
|
813
|
+
})
|
|
814
|
+
|
|
815
|
+
it('should preserve all other properties during migration', () => {
|
|
816
|
+
const oldRecord = {
|
|
817
|
+
id: 'shape:note1',
|
|
818
|
+
props: {
|
|
819
|
+
color: 'purple',
|
|
820
|
+
align: 'start',
|
|
821
|
+
verticalAlign: 'middle',
|
|
822
|
+
labelColor: 'white',
|
|
823
|
+
size: 's',
|
|
824
|
+
font: 'mono',
|
|
825
|
+
fontSizeAdjustment: 3,
|
|
826
|
+
growY: 15,
|
|
827
|
+
url: 'https://example.com',
|
|
828
|
+
scale: 1.2,
|
|
829
|
+
},
|
|
830
|
+
}
|
|
831
|
+
|
|
832
|
+
const result = up(oldRecord)
|
|
833
|
+
expect(result.props.align).toBe('start-legacy')
|
|
834
|
+
expect(result.props.color).toBe('purple')
|
|
835
|
+
expect(result.props.labelColor).toBe('white')
|
|
836
|
+
expect(result.props.fontSizeAdjustment).toBe(3)
|
|
837
|
+
})
|
|
838
|
+
})
|
|
839
|
+
|
|
840
|
+
describe('MigrateLegacyAlign down migration', () => {
|
|
841
|
+
it('should be retired (no down migration)', () => {
|
|
842
|
+
expect(() => {
|
|
843
|
+
down({})
|
|
844
|
+
}).toThrow('Migration com.tldraw.shape.note/3 does not have a down function')
|
|
845
|
+
})
|
|
846
|
+
})
|
|
847
|
+
})
|
|
848
|
+
|
|
849
|
+
describe('noteShapeMigrations - AddVerticalAlign migration', () => {
|
|
850
|
+
const { up, down } = getTestMigration(noteShapeVersions.AddVerticalAlign)
|
|
851
|
+
|
|
852
|
+
describe('AddVerticalAlign up migration', () => {
|
|
853
|
+
it('should add verticalAlign property with default value "middle"', () => {
|
|
854
|
+
const oldRecord = {
|
|
855
|
+
id: 'shape:note1',
|
|
856
|
+
props: {
|
|
857
|
+
color: 'yellow',
|
|
858
|
+
align: 'start-legacy',
|
|
859
|
+
labelColor: 'black',
|
|
860
|
+
},
|
|
861
|
+
}
|
|
862
|
+
|
|
863
|
+
const result = up(oldRecord)
|
|
864
|
+
expect(result.props.verticalAlign).toBe('middle')
|
|
865
|
+
expect(result.props.align).toBe('start-legacy') // Preserve other props
|
|
866
|
+
})
|
|
867
|
+
|
|
868
|
+
it('should preserve all existing properties during migration', () => {
|
|
869
|
+
const oldRecord = {
|
|
870
|
+
id: 'shape:note1',
|
|
871
|
+
props: {
|
|
872
|
+
color: 'red',
|
|
873
|
+
align: 'middle-legacy',
|
|
874
|
+
labelColor: 'white',
|
|
875
|
+
size: 'xl',
|
|
876
|
+
font: 'draw',
|
|
877
|
+
fontSizeAdjustment: 0,
|
|
878
|
+
growY: 50,
|
|
879
|
+
url: 'https://test.com',
|
|
880
|
+
scale: 0.9,
|
|
881
|
+
},
|
|
882
|
+
}
|
|
883
|
+
|
|
884
|
+
const result = up(oldRecord)
|
|
885
|
+
expect(result.props.verticalAlign).toBe('middle')
|
|
886
|
+
expect(result.props.color).toBe('red')
|
|
887
|
+
expect(result.props.align).toBe('middle-legacy')
|
|
888
|
+
expect(result.props.size).toBe('xl')
|
|
889
|
+
})
|
|
890
|
+
})
|
|
891
|
+
|
|
892
|
+
describe('AddVerticalAlign down migration', () => {
|
|
893
|
+
it('should be retired (no down migration)', () => {
|
|
894
|
+
expect(() => {
|
|
895
|
+
down({})
|
|
896
|
+
}).toThrow('Migration com.tldraw.shape.note/4 does not have a down function')
|
|
897
|
+
})
|
|
898
|
+
})
|
|
899
|
+
})
|
|
900
|
+
|
|
901
|
+
describe('noteShapeMigrations - MakeUrlsValid migration', () => {
|
|
902
|
+
const { up, down } = getTestMigration(noteShapeVersions.MakeUrlsValid)
|
|
903
|
+
|
|
904
|
+
describe('MakeUrlsValid up migration', () => {
|
|
905
|
+
it('should clear invalid URLs', () => {
|
|
906
|
+
const oldRecord = {
|
|
907
|
+
id: 'shape:note1',
|
|
908
|
+
props: {
|
|
909
|
+
color: 'yellow',
|
|
910
|
+
url: 'invalid-url',
|
|
911
|
+
align: 'start-legacy',
|
|
912
|
+
verticalAlign: 'middle',
|
|
913
|
+
},
|
|
914
|
+
}
|
|
915
|
+
|
|
916
|
+
const result = up(oldRecord)
|
|
917
|
+
expect(result.props.url).toBe('')
|
|
918
|
+
expect(result.props.color).toBe('yellow') // Preserve other props
|
|
919
|
+
})
|
|
920
|
+
|
|
921
|
+
it('should preserve valid URLs', () => {
|
|
922
|
+
const validUrls = [
|
|
923
|
+
'',
|
|
924
|
+
'https://example.com',
|
|
925
|
+
'http://test.com',
|
|
926
|
+
'https://subdomain.example.com/path',
|
|
927
|
+
]
|
|
928
|
+
|
|
929
|
+
validUrls.forEach((url) => {
|
|
930
|
+
const oldRecord = {
|
|
931
|
+
id: 'shape:note1',
|
|
932
|
+
props: {
|
|
933
|
+
color: 'blue',
|
|
934
|
+
url,
|
|
935
|
+
align: 'middle-legacy',
|
|
936
|
+
verticalAlign: 'start',
|
|
937
|
+
},
|
|
938
|
+
}
|
|
939
|
+
|
|
940
|
+
const result = up(oldRecord)
|
|
941
|
+
expect(result.props.url).toBe(url)
|
|
942
|
+
})
|
|
943
|
+
})
|
|
944
|
+
|
|
945
|
+
it('should preserve all other properties during migration', () => {
|
|
946
|
+
const oldRecord = {
|
|
947
|
+
id: 'shape:note1',
|
|
948
|
+
props: {
|
|
949
|
+
color: 'green',
|
|
950
|
+
url: 'not-valid',
|
|
951
|
+
align: 'end-legacy',
|
|
952
|
+
verticalAlign: 'end',
|
|
953
|
+
labelColor: 'red',
|
|
954
|
+
size: 'm',
|
|
955
|
+
font: 'sans',
|
|
956
|
+
fontSizeAdjustment: 2,
|
|
957
|
+
growY: 40,
|
|
958
|
+
scale: 1.1,
|
|
959
|
+
},
|
|
960
|
+
}
|
|
961
|
+
|
|
962
|
+
const result = up(oldRecord)
|
|
963
|
+
expect(result.props.url).toBe('')
|
|
964
|
+
expect(result.props.color).toBe('green')
|
|
965
|
+
expect(result.props.align).toBe('end-legacy')
|
|
966
|
+
expect(result.props.verticalAlign).toBe('end')
|
|
967
|
+
})
|
|
968
|
+
})
|
|
969
|
+
|
|
970
|
+
describe('MakeUrlsValid down migration', () => {
|
|
971
|
+
it('should be a no-op migration', () => {
|
|
972
|
+
const newRecord = {
|
|
973
|
+
id: 'shape:note1',
|
|
974
|
+
props: {
|
|
975
|
+
color: 'yellow',
|
|
976
|
+
url: 'https://example.com',
|
|
977
|
+
align: 'start-legacy',
|
|
978
|
+
verticalAlign: 'middle',
|
|
979
|
+
},
|
|
980
|
+
}
|
|
981
|
+
|
|
982
|
+
const result = down(newRecord)
|
|
983
|
+
expect(result).toEqual(newRecord)
|
|
984
|
+
})
|
|
985
|
+
})
|
|
986
|
+
})
|
|
987
|
+
|
|
988
|
+
describe('noteShapeMigrations - AddFontSizeAdjustment migration', () => {
|
|
989
|
+
const { up, down } = getTestMigration(noteShapeVersions.AddFontSizeAdjustment)
|
|
990
|
+
|
|
991
|
+
describe('AddFontSizeAdjustment up migration', () => {
|
|
992
|
+
it('should add fontSizeAdjustment property with default value 0', () => {
|
|
993
|
+
const oldRecord = {
|
|
994
|
+
id: 'shape:note1',
|
|
995
|
+
props: {
|
|
996
|
+
color: 'yellow',
|
|
997
|
+
url: 'https://example.com',
|
|
998
|
+
align: 'start-legacy',
|
|
999
|
+
verticalAlign: 'middle',
|
|
1000
|
+
},
|
|
1001
|
+
}
|
|
1002
|
+
|
|
1003
|
+
const result = up(oldRecord)
|
|
1004
|
+
expect(result.props.fontSizeAdjustment).toBe(0)
|
|
1005
|
+
expect(result.props.color).toBe('yellow') // Preserve other props
|
|
1006
|
+
})
|
|
1007
|
+
|
|
1008
|
+
it('should preserve all existing properties during migration', () => {
|
|
1009
|
+
const oldRecord = {
|
|
1010
|
+
id: 'shape:note1',
|
|
1011
|
+
props: {
|
|
1012
|
+
color: 'red',
|
|
1013
|
+
url: '',
|
|
1014
|
+
align: 'middle-legacy',
|
|
1015
|
+
verticalAlign: 'start',
|
|
1016
|
+
labelColor: 'white',
|
|
1017
|
+
size: 'l',
|
|
1018
|
+
font: 'serif',
|
|
1019
|
+
growY: 35,
|
|
1020
|
+
scale: 1.3,
|
|
1021
|
+
},
|
|
1022
|
+
}
|
|
1023
|
+
|
|
1024
|
+
const result = up(oldRecord)
|
|
1025
|
+
expect(result.props.fontSizeAdjustment).toBe(0)
|
|
1026
|
+
expect(result.props.color).toBe('red')
|
|
1027
|
+
expect(result.props.align).toBe('middle-legacy')
|
|
1028
|
+
expect(result.props.size).toBe('l')
|
|
1029
|
+
})
|
|
1030
|
+
})
|
|
1031
|
+
|
|
1032
|
+
describe('AddFontSizeAdjustment down migration', () => {
|
|
1033
|
+
it('should remove fontSizeAdjustment property', () => {
|
|
1034
|
+
const newRecord = {
|
|
1035
|
+
id: 'shape:note1',
|
|
1036
|
+
props: {
|
|
1037
|
+
color: 'yellow',
|
|
1038
|
+
url: 'https://example.com',
|
|
1039
|
+
align: 'start-legacy',
|
|
1040
|
+
verticalAlign: 'middle',
|
|
1041
|
+
fontSizeAdjustment: 2,
|
|
1042
|
+
},
|
|
1043
|
+
}
|
|
1044
|
+
|
|
1045
|
+
const result = down(newRecord)
|
|
1046
|
+
expect(result.props.fontSizeAdjustment).toBeUndefined()
|
|
1047
|
+
expect(result.props.color).toBe('yellow') // Preserve other props
|
|
1048
|
+
})
|
|
1049
|
+
})
|
|
1050
|
+
})
|
|
1051
|
+
|
|
1052
|
+
describe('noteShapeMigrations - AddScale migration', () => {
|
|
1053
|
+
const { up, down } = getTestMigration(noteShapeVersions.AddScale)
|
|
1054
|
+
|
|
1055
|
+
describe('AddScale up migration', () => {
|
|
1056
|
+
it('should add scale property with default value 1', () => {
|
|
1057
|
+
const oldRecord = {
|
|
1058
|
+
id: 'shape:note1',
|
|
1059
|
+
props: {
|
|
1060
|
+
color: 'yellow',
|
|
1061
|
+
url: 'https://example.com',
|
|
1062
|
+
align: 'start-legacy',
|
|
1063
|
+
verticalAlign: 'middle',
|
|
1064
|
+
fontSizeAdjustment: 1,
|
|
1065
|
+
},
|
|
1066
|
+
}
|
|
1067
|
+
|
|
1068
|
+
const result = up(oldRecord)
|
|
1069
|
+
expect(result.props.scale).toBe(1)
|
|
1070
|
+
expect(result.props.color).toBe('yellow') // Preserve other props
|
|
1071
|
+
})
|
|
1072
|
+
|
|
1073
|
+
it('should preserve all existing properties during migration', () => {
|
|
1074
|
+
const oldRecord = {
|
|
1075
|
+
id: 'shape:note1',
|
|
1076
|
+
props: {
|
|
1077
|
+
color: 'blue',
|
|
1078
|
+
url: '',
|
|
1079
|
+
align: 'end-legacy',
|
|
1080
|
+
verticalAlign: 'end',
|
|
1081
|
+
labelColor: 'black',
|
|
1082
|
+
size: 's',
|
|
1083
|
+
font: 'mono',
|
|
1084
|
+
fontSizeAdjustment: 3,
|
|
1085
|
+
growY: 60,
|
|
1086
|
+
},
|
|
1087
|
+
}
|
|
1088
|
+
|
|
1089
|
+
const result = up(oldRecord)
|
|
1090
|
+
expect(result.props.scale).toBe(1)
|
|
1091
|
+
expect(result.props.color).toBe('blue')
|
|
1092
|
+
expect(result.props.fontSizeAdjustment).toBe(3)
|
|
1093
|
+
expect(result.props.growY).toBe(60)
|
|
1094
|
+
})
|
|
1095
|
+
})
|
|
1096
|
+
|
|
1097
|
+
describe('AddScale down migration', () => {
|
|
1098
|
+
it('should remove scale property', () => {
|
|
1099
|
+
const newRecord = {
|
|
1100
|
+
id: 'shape:note1',
|
|
1101
|
+
props: {
|
|
1102
|
+
color: 'yellow',
|
|
1103
|
+
url: 'https://example.com',
|
|
1104
|
+
align: 'start-legacy',
|
|
1105
|
+
verticalAlign: 'middle',
|
|
1106
|
+
fontSizeAdjustment: 1,
|
|
1107
|
+
scale: 1.5,
|
|
1108
|
+
},
|
|
1109
|
+
}
|
|
1110
|
+
|
|
1111
|
+
const result = down(newRecord)
|
|
1112
|
+
expect(result.props.scale).toBeUndefined()
|
|
1113
|
+
expect(result.props.color).toBe('yellow') // Preserve other props
|
|
1114
|
+
})
|
|
1115
|
+
})
|
|
1116
|
+
})
|
|
1117
|
+
|
|
1118
|
+
describe('noteShapeMigrations - AddLabelColor migration', () => {
|
|
1119
|
+
const { up, down } = getTestMigration(noteShapeVersions.AddLabelColor)
|
|
1120
|
+
|
|
1121
|
+
describe('AddLabelColor up migration', () => {
|
|
1122
|
+
it('should add labelColor property with default value "black"', () => {
|
|
1123
|
+
const oldRecord = {
|
|
1124
|
+
id: 'shape:note1',
|
|
1125
|
+
props: {
|
|
1126
|
+
color: 'yellow',
|
|
1127
|
+
url: 'https://example.com',
|
|
1128
|
+
align: 'start-legacy',
|
|
1129
|
+
verticalAlign: 'middle',
|
|
1130
|
+
fontSizeAdjustment: 1,
|
|
1131
|
+
scale: 1,
|
|
1132
|
+
},
|
|
1133
|
+
}
|
|
1134
|
+
|
|
1135
|
+
const result = up(oldRecord)
|
|
1136
|
+
expect(result.props.labelColor).toBe('black')
|
|
1137
|
+
expect(result.props.color).toBe('yellow') // Preserve other props
|
|
1138
|
+
})
|
|
1139
|
+
|
|
1140
|
+
it('should preserve all existing properties during migration', () => {
|
|
1141
|
+
const oldRecord = {
|
|
1142
|
+
id: 'shape:note1',
|
|
1143
|
+
props: {
|
|
1144
|
+
color: 'red',
|
|
1145
|
+
url: '',
|
|
1146
|
+
align: 'middle-legacy',
|
|
1147
|
+
verticalAlign: 'start',
|
|
1148
|
+
size: 'xl',
|
|
1149
|
+
font: 'draw',
|
|
1150
|
+
fontSizeAdjustment: 2,
|
|
1151
|
+
growY: 45,
|
|
1152
|
+
scale: 0.7,
|
|
1153
|
+
},
|
|
1154
|
+
}
|
|
1155
|
+
|
|
1156
|
+
const result = up(oldRecord)
|
|
1157
|
+
expect(result.props.labelColor).toBe('black')
|
|
1158
|
+
expect(result.props.color).toBe('red')
|
|
1159
|
+
expect(result.props.size).toBe('xl')
|
|
1160
|
+
expect(result.props.fontSizeAdjustment).toBe(2)
|
|
1161
|
+
})
|
|
1162
|
+
})
|
|
1163
|
+
|
|
1164
|
+
describe('AddLabelColor down migration', () => {
|
|
1165
|
+
it('should remove labelColor property', () => {
|
|
1166
|
+
const newRecord = {
|
|
1167
|
+
id: 'shape:note1',
|
|
1168
|
+
props: {
|
|
1169
|
+
color: 'yellow',
|
|
1170
|
+
labelColor: 'white',
|
|
1171
|
+
url: 'https://example.com',
|
|
1172
|
+
align: 'start-legacy',
|
|
1173
|
+
verticalAlign: 'middle',
|
|
1174
|
+
fontSizeAdjustment: 1,
|
|
1175
|
+
scale: 1,
|
|
1176
|
+
},
|
|
1177
|
+
}
|
|
1178
|
+
|
|
1179
|
+
const result = down(newRecord)
|
|
1180
|
+
expect(result.props.labelColor).toBeUndefined()
|
|
1181
|
+
expect(result.props.color).toBe('yellow') // Preserve other props
|
|
1182
|
+
})
|
|
1183
|
+
})
|
|
1184
|
+
})
|
|
1185
|
+
|
|
1186
|
+
describe('noteShapeMigrations - AddRichText migration', () => {
|
|
1187
|
+
const { up } = getTestMigration(noteShapeVersions.AddRichText)
|
|
1188
|
+
|
|
1189
|
+
describe('AddRichText up migration', () => {
|
|
1190
|
+
it('should convert text property to richText', () => {
|
|
1191
|
+
const oldRecord = {
|
|
1192
|
+
id: 'shape:note1',
|
|
1193
|
+
props: {
|
|
1194
|
+
color: 'yellow',
|
|
1195
|
+
labelColor: 'black',
|
|
1196
|
+
text: 'Simple note content',
|
|
1197
|
+
url: 'https://example.com',
|
|
1198
|
+
align: 'start-legacy',
|
|
1199
|
+
verticalAlign: 'middle',
|
|
1200
|
+
fontSizeAdjustment: 1,
|
|
1201
|
+
scale: 1,
|
|
1202
|
+
},
|
|
1203
|
+
}
|
|
1204
|
+
|
|
1205
|
+
const result = up(oldRecord)
|
|
1206
|
+
expect(result.props.richText).toBeDefined()
|
|
1207
|
+
expect(result.props.text).toBeUndefined()
|
|
1208
|
+
expect(result.props.color).toBe('yellow') // Preserve other props
|
|
1209
|
+
})
|
|
1210
|
+
|
|
1211
|
+
it('should handle empty text', () => {
|
|
1212
|
+
const oldRecord = {
|
|
1213
|
+
id: 'shape:note1',
|
|
1214
|
+
props: {
|
|
1215
|
+
color: 'blue',
|
|
1216
|
+
labelColor: 'white',
|
|
1217
|
+
text: '',
|
|
1218
|
+
url: '',
|
|
1219
|
+
align: 'middle-legacy',
|
|
1220
|
+
verticalAlign: 'start',
|
|
1221
|
+
fontSizeAdjustment: 0,
|
|
1222
|
+
scale: 1,
|
|
1223
|
+
},
|
|
1224
|
+
}
|
|
1225
|
+
|
|
1226
|
+
const result = up(oldRecord)
|
|
1227
|
+
expect(result.props.richText).toBeDefined()
|
|
1228
|
+
expect(result.props.text).toBeUndefined()
|
|
1229
|
+
})
|
|
1230
|
+
|
|
1231
|
+
it('should preserve all other properties during migration', () => {
|
|
1232
|
+
const oldRecord = {
|
|
1233
|
+
id: 'shape:note1',
|
|
1234
|
+
props: {
|
|
1235
|
+
color: 'green',
|
|
1236
|
+
labelColor: 'red',
|
|
1237
|
+
text: 'Multi-line\nnote content',
|
|
1238
|
+
url: 'https://test.com',
|
|
1239
|
+
align: 'end-legacy',
|
|
1240
|
+
verticalAlign: 'end',
|
|
1241
|
+
size: 'l',
|
|
1242
|
+
font: 'sans',
|
|
1243
|
+
fontSizeAdjustment: 3,
|
|
1244
|
+
growY: 70,
|
|
1245
|
+
scale: 1.4,
|
|
1246
|
+
},
|
|
1247
|
+
}
|
|
1248
|
+
|
|
1249
|
+
const result = up(oldRecord)
|
|
1250
|
+
expect(result.props.richText).toBeDefined()
|
|
1251
|
+
expect(result.props.text).toBeUndefined()
|
|
1252
|
+
expect(result.props.color).toBe('green')
|
|
1253
|
+
expect(result.props.labelColor).toBe('red')
|
|
1254
|
+
expect(result.props.size).toBe('l')
|
|
1255
|
+
expect(result.props.fontSizeAdjustment).toBe(3)
|
|
1256
|
+
})
|
|
1257
|
+
})
|
|
1258
|
+
|
|
1259
|
+
// Note: The down migration is explicitly not defined (forced client update)
|
|
1260
|
+
// so we don't test it
|
|
1261
|
+
})
|
|
1262
|
+
|
|
1263
|
+
describe('integration tests', () => {
|
|
1264
|
+
it('should work with complete note shape record validation', () => {
|
|
1265
|
+
const completeValidator = T.object({
|
|
1266
|
+
id: T.string,
|
|
1267
|
+
typeName: T.literal('shape'),
|
|
1268
|
+
type: T.literal('note'),
|
|
1269
|
+
x: T.number,
|
|
1270
|
+
y: T.number,
|
|
1271
|
+
rotation: T.number,
|
|
1272
|
+
index: T.string,
|
|
1273
|
+
parentId: T.string,
|
|
1274
|
+
isLocked: T.boolean,
|
|
1275
|
+
opacity: T.number,
|
|
1276
|
+
props: T.object(noteShapeProps),
|
|
1277
|
+
meta: T.jsonValue,
|
|
1278
|
+
})
|
|
1279
|
+
|
|
1280
|
+
const validNoteShape = {
|
|
1281
|
+
id: 'shape:note123',
|
|
1282
|
+
typeName: 'shape' as const,
|
|
1283
|
+
type: 'note' as const,
|
|
1284
|
+
x: 100,
|
|
1285
|
+
y: 200,
|
|
1286
|
+
rotation: 0.5,
|
|
1287
|
+
index: 'a1',
|
|
1288
|
+
parentId: 'page:main',
|
|
1289
|
+
isLocked: false,
|
|
1290
|
+
opacity: 0.8,
|
|
1291
|
+
props: {
|
|
1292
|
+
color: 'light-blue' as const,
|
|
1293
|
+
labelColor: 'black' as const,
|
|
1294
|
+
size: 'l' as const,
|
|
1295
|
+
font: 'sans' as const,
|
|
1296
|
+
fontSizeAdjustment: 2,
|
|
1297
|
+
align: 'start' as const,
|
|
1298
|
+
verticalAlign: 'end' as const,
|
|
1299
|
+
growY: 30,
|
|
1300
|
+
url: 'https://example.com',
|
|
1301
|
+
richText: toRichText('Important **note**') as TLRichText,
|
|
1302
|
+
scale: 1.1,
|
|
1303
|
+
},
|
|
1304
|
+
meta: { priority: 'high' },
|
|
1305
|
+
}
|
|
1306
|
+
|
|
1307
|
+
expect(() => completeValidator.validate(validNoteShape)).not.toThrow()
|
|
1308
|
+
})
|
|
1309
|
+
|
|
1310
|
+
it('should be compatible with TLBaseShape structure', () => {
|
|
1311
|
+
const noteShape: TLNoteShape = {
|
|
1312
|
+
id: 'shape:note_test' as TLShapeId,
|
|
1313
|
+
typeName: 'shape',
|
|
1314
|
+
type: 'note',
|
|
1315
|
+
x: 50,
|
|
1316
|
+
y: 75,
|
|
1317
|
+
rotation: 1.57,
|
|
1318
|
+
index: 'b1' as any,
|
|
1319
|
+
parentId: 'page:test' as any,
|
|
1320
|
+
isLocked: true,
|
|
1321
|
+
opacity: 0.5,
|
|
1322
|
+
props: {
|
|
1323
|
+
color: 'red',
|
|
1324
|
+
labelColor: 'white',
|
|
1325
|
+
size: 's',
|
|
1326
|
+
font: 'mono',
|
|
1327
|
+
fontSizeAdjustment: 1,
|
|
1328
|
+
align: 'middle',
|
|
1329
|
+
verticalAlign: 'middle',
|
|
1330
|
+
growY: 15,
|
|
1331
|
+
url: '',
|
|
1332
|
+
richText: toRichText('📝'),
|
|
1333
|
+
scale: 0.9,
|
|
1334
|
+
},
|
|
1335
|
+
meta: { category: 'reminder' },
|
|
1336
|
+
}
|
|
1337
|
+
|
|
1338
|
+
// Should satisfy TLBaseShape structure
|
|
1339
|
+
expect(noteShape.typeName).toBe('shape')
|
|
1340
|
+
expect(noteShape.type).toBe('note')
|
|
1341
|
+
expect(typeof noteShape.id).toBe('string')
|
|
1342
|
+
expect(typeof noteShape.x).toBe('number')
|
|
1343
|
+
expect(typeof noteShape.y).toBe('number')
|
|
1344
|
+
expect(typeof noteShape.rotation).toBe('number')
|
|
1345
|
+
expect(noteShape.props).toBeDefined()
|
|
1346
|
+
expect(noteShape.meta).toBeDefined()
|
|
1347
|
+
})
|
|
1348
|
+
|
|
1349
|
+
test('should handle all migration versions in correct order', () => {
|
|
1350
|
+
const expectedOrder: Array<keyof typeof noteShapeVersions> = [
|
|
1351
|
+
'AddUrlProp',
|
|
1352
|
+
'RemoveJustify',
|
|
1353
|
+
'MigrateLegacyAlign',
|
|
1354
|
+
'AddVerticalAlign',
|
|
1355
|
+
'MakeUrlsValid',
|
|
1356
|
+
'AddFontSizeAdjustment',
|
|
1357
|
+
'AddScale',
|
|
1358
|
+
'AddLabelColor',
|
|
1359
|
+
'AddRichText',
|
|
1360
|
+
]
|
|
1361
|
+
|
|
1362
|
+
const migrationIds = noteShapeMigrations.sequence
|
|
1363
|
+
.filter((migration) => 'id' in migration)
|
|
1364
|
+
.map((migration) => ('id' in migration ? migration.id : ''))
|
|
1365
|
+
.filter(Boolean)
|
|
1366
|
+
|
|
1367
|
+
expectedOrder.forEach((expectedVersion) => {
|
|
1368
|
+
const versionId = noteShapeVersions[expectedVersion]
|
|
1369
|
+
expect(migrationIds).toContain(versionId)
|
|
1370
|
+
})
|
|
1371
|
+
})
|
|
1372
|
+
})
|
|
1373
|
+
|
|
1374
|
+
describe('edge cases and error handling', () => {
|
|
1375
|
+
it('should handle empty or malformed props gracefully during validation', () => {
|
|
1376
|
+
const fullValidator = T.object(noteShapeProps)
|
|
1377
|
+
|
|
1378
|
+
// Missing required properties should throw
|
|
1379
|
+
expect(() => fullValidator.validate({})).toThrow()
|
|
1380
|
+
|
|
1381
|
+
// Partial props should throw for missing required fields
|
|
1382
|
+
expect(() =>
|
|
1383
|
+
fullValidator.validate({
|
|
1384
|
+
color: 'yellow',
|
|
1385
|
+
labelColor: 'black',
|
|
1386
|
+
// Missing other required properties
|
|
1387
|
+
})
|
|
1388
|
+
).toThrow()
|
|
1389
|
+
|
|
1390
|
+
// Extra unexpected properties should throw
|
|
1391
|
+
expect(() =>
|
|
1392
|
+
fullValidator.validate({
|
|
1393
|
+
color: 'yellow',
|
|
1394
|
+
labelColor: 'black',
|
|
1395
|
+
size: 'm',
|
|
1396
|
+
font: 'draw',
|
|
1397
|
+
fontSizeAdjustment: 0,
|
|
1398
|
+
align: 'middle',
|
|
1399
|
+
verticalAlign: 'middle',
|
|
1400
|
+
growY: 0,
|
|
1401
|
+
url: '',
|
|
1402
|
+
richText: toRichText(''),
|
|
1403
|
+
scale: 1,
|
|
1404
|
+
unexpectedProperty: 'extra', // This should cause validation to fail
|
|
1405
|
+
})
|
|
1406
|
+
).toThrow()
|
|
1407
|
+
})
|
|
1408
|
+
|
|
1409
|
+
it('should handle boundary values for numeric properties', () => {
|
|
1410
|
+
// Test extreme but valid values
|
|
1411
|
+
const extremeProps = {
|
|
1412
|
+
color: 'yellow' as const,
|
|
1413
|
+
labelColor: 'black' as const,
|
|
1414
|
+
size: 'm' as const,
|
|
1415
|
+
font: 'draw' as const,
|
|
1416
|
+
fontSizeAdjustment: 0, // Minimum positive number
|
|
1417
|
+
align: 'middle' as const,
|
|
1418
|
+
verticalAlign: 'middle' as const,
|
|
1419
|
+
growY: 0, // Minimum positive number
|
|
1420
|
+
url: '',
|
|
1421
|
+
richText: toRichText('') as TLRichText,
|
|
1422
|
+
scale: 0.0001, // Very small but not zero
|
|
1423
|
+
}
|
|
1424
|
+
|
|
1425
|
+
const fullValidator = T.object(noteShapeProps)
|
|
1426
|
+
expect(() => fullValidator.validate(extremeProps)).not.toThrow()
|
|
1427
|
+
})
|
|
1428
|
+
|
|
1429
|
+
it('should handle zero and negative values validation correctly', () => {
|
|
1430
|
+
// Zero should be invalid for scale (nonZeroNumber)
|
|
1431
|
+
expect(() => noteShapeProps.scale.validate(0)).toThrow()
|
|
1432
|
+
|
|
1433
|
+
// Negative values should be invalid for fontSizeAdjustment, growY, and scale
|
|
1434
|
+
expect(() => noteShapeProps.fontSizeAdjustment.validate(-1)).toThrow()
|
|
1435
|
+
expect(() => noteShapeProps.growY.validate(-1)).toThrow()
|
|
1436
|
+
expect(() => noteShapeProps.scale.validate(-1)).toThrow()
|
|
1437
|
+
|
|
1438
|
+
// Zero should be valid for fontSizeAdjustment and growY (positiveNumber includes zero)
|
|
1439
|
+
expect(() => noteShapeProps.fontSizeAdjustment.validate(0)).not.toThrow()
|
|
1440
|
+
expect(() => noteShapeProps.growY.validate(0)).not.toThrow()
|
|
1441
|
+
})
|
|
1442
|
+
|
|
1443
|
+
it('should handle complex rich text content', () => {
|
|
1444
|
+
const complexRichTexts = [
|
|
1445
|
+
toRichText(''),
|
|
1446
|
+
toRichText('Simple note'),
|
|
1447
|
+
toRichText('**Bold** and *italic* and ***both***'),
|
|
1448
|
+
toRichText('Line 1\nLine 2\nLine 3'),
|
|
1449
|
+
toRichText('Special chars: !@#$%^&*()'),
|
|
1450
|
+
toRichText('Unicode: 📝 ✅ ❗'),
|
|
1451
|
+
]
|
|
1452
|
+
|
|
1453
|
+
complexRichTexts.forEach((richText) => {
|
|
1454
|
+
expect(() => noteShapeProps.richText.validate(richText)).not.toThrow()
|
|
1455
|
+
})
|
|
1456
|
+
})
|
|
1457
|
+
|
|
1458
|
+
it('should handle different font size adjustments', () => {
|
|
1459
|
+
const fontAdjustments = [0, 0.5, 1, 2, 5, 10, 20]
|
|
1460
|
+
|
|
1461
|
+
fontAdjustments.forEach((adjustment) => {
|
|
1462
|
+
expect(() => noteShapeProps.fontSizeAdjustment.validate(adjustment)).not.toThrow()
|
|
1463
|
+
})
|
|
1464
|
+
|
|
1465
|
+
// Negative adjustments should be invalid
|
|
1466
|
+
const negativeAdjustments = [-1, -0.5, -10]
|
|
1467
|
+
negativeAdjustments.forEach((adjustment) => {
|
|
1468
|
+
expect(() => noteShapeProps.fontSizeAdjustment.validate(adjustment)).toThrow()
|
|
1469
|
+
})
|
|
1470
|
+
})
|
|
1471
|
+
|
|
1472
|
+
it('should validate different URL formats correctly', () => {
|
|
1473
|
+
const urlTestCases = [
|
|
1474
|
+
{ url: '', shouldPass: true },
|
|
1475
|
+
{ url: 'https://example.com', shouldPass: true },
|
|
1476
|
+
{ url: 'http://test.com', shouldPass: true },
|
|
1477
|
+
{ url: 'https://subdomain.example.com/path?query=value', shouldPass: true },
|
|
1478
|
+
]
|
|
1479
|
+
|
|
1480
|
+
urlTestCases.forEach(({ url, shouldPass }) => {
|
|
1481
|
+
if (shouldPass) {
|
|
1482
|
+
expect(() => noteShapeProps.url.validate(url)).not.toThrow()
|
|
1483
|
+
} else {
|
|
1484
|
+
expect(() => noteShapeProps.url.validate(url)).toThrow()
|
|
1485
|
+
}
|
|
1486
|
+
})
|
|
1487
|
+
})
|
|
1488
|
+
|
|
1489
|
+
it('should handle all style property combinations', () => {
|
|
1490
|
+
const styleVariations = [
|
|
1491
|
+
{
|
|
1492
|
+
color: 'black' as const,
|
|
1493
|
+
labelColor: 'white' as const,
|
|
1494
|
+
size: 's' as const,
|
|
1495
|
+
font: 'draw' as const,
|
|
1496
|
+
align: 'start' as const,
|
|
1497
|
+
verticalAlign: 'start' as const,
|
|
1498
|
+
},
|
|
1499
|
+
{
|
|
1500
|
+
color: 'red' as const,
|
|
1501
|
+
labelColor: 'black' as const,
|
|
1502
|
+
size: 'm' as const,
|
|
1503
|
+
font: 'sans' as const,
|
|
1504
|
+
align: 'middle' as const,
|
|
1505
|
+
verticalAlign: 'middle' as const,
|
|
1506
|
+
},
|
|
1507
|
+
{
|
|
1508
|
+
color: 'blue' as const,
|
|
1509
|
+
labelColor: 'yellow' as const,
|
|
1510
|
+
size: 'l' as const,
|
|
1511
|
+
font: 'serif' as const,
|
|
1512
|
+
align: 'end' as const,
|
|
1513
|
+
verticalAlign: 'end' as const,
|
|
1514
|
+
},
|
|
1515
|
+
{
|
|
1516
|
+
color: 'green' as const,
|
|
1517
|
+
labelColor: 'red' as const,
|
|
1518
|
+
size: 'xl' as const,
|
|
1519
|
+
font: 'mono' as const,
|
|
1520
|
+
align: 'start-legacy' as const,
|
|
1521
|
+
verticalAlign: 'start' as const,
|
|
1522
|
+
},
|
|
1523
|
+
]
|
|
1524
|
+
|
|
1525
|
+
styleVariations.forEach((styling, index) => {
|
|
1526
|
+
const props: TLNoteShapeProps = {
|
|
1527
|
+
color: styling.color,
|
|
1528
|
+
labelColor: styling.labelColor,
|
|
1529
|
+
size: styling.size,
|
|
1530
|
+
font: styling.font,
|
|
1531
|
+
fontSizeAdjustment: index,
|
|
1532
|
+
align: styling.align,
|
|
1533
|
+
verticalAlign: styling.verticalAlign,
|
|
1534
|
+
growY: index * 10,
|
|
1535
|
+
url: '',
|
|
1536
|
+
richText: toRichText(`Note ${index + 1}`),
|
|
1537
|
+
scale: 1,
|
|
1538
|
+
}
|
|
1539
|
+
|
|
1540
|
+
const fullValidator = T.object(noteShapeProps)
|
|
1541
|
+
expect(() => fullValidator.validate(props)).not.toThrow()
|
|
1542
|
+
})
|
|
1543
|
+
})
|
|
1544
|
+
|
|
1545
|
+
it('should handle note shapes with different growth and scaling', () => {
|
|
1546
|
+
const dimensionVariations = [
|
|
1547
|
+
{ fontSizeAdjustment: 0, growY: 0, scale: 0.5 },
|
|
1548
|
+
{ fontSizeAdjustment: 1, growY: 10, scale: 1 },
|
|
1549
|
+
{ fontSizeAdjustment: 3, growY: 50, scale: 1.5 },
|
|
1550
|
+
{ fontSizeAdjustment: 5, growY: 100, scale: 2 },
|
|
1551
|
+
]
|
|
1552
|
+
|
|
1553
|
+
dimensionVariations.forEach((dims) => {
|
|
1554
|
+
const props: Partial<TLNoteShapeProps> = {
|
|
1555
|
+
fontSizeAdjustment: dims.fontSizeAdjustment,
|
|
1556
|
+
growY: dims.growY,
|
|
1557
|
+
scale: dims.scale,
|
|
1558
|
+
}
|
|
1559
|
+
|
|
1560
|
+
expect(() =>
|
|
1561
|
+
noteShapeProps.fontSizeAdjustment.validate(props.fontSizeAdjustment)
|
|
1562
|
+
).not.toThrow()
|
|
1563
|
+
expect(() => noteShapeProps.growY.validate(props.growY)).not.toThrow()
|
|
1564
|
+
expect(() => noteShapeProps.scale.validate(props.scale)).not.toThrow()
|
|
1565
|
+
})
|
|
1566
|
+
})
|
|
1567
|
+
})
|
|
1568
|
+
})
|