@tldraw/tlschema 4.2.2 → 4.2.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist-cjs/bindings/TLBaseBinding.js.map +2 -2
- package/dist-cjs/createTLSchema.js.map +2 -2
- package/dist-cjs/index.d.ts +71 -242
- package/dist-cjs/index.js +1 -4
- package/dist-cjs/index.js.map +2 -2
- package/dist-cjs/misc/TLOpacity.js +5 -1
- package/dist-cjs/misc/TLOpacity.js.map +2 -2
- package/dist-cjs/misc/TLRichText.js +1 -5
- package/dist-cjs/misc/TLRichText.js.map +2 -2
- package/dist-cjs/records/TLAsset.js.map +1 -1
- package/dist-cjs/records/TLBinding.js.map +2 -2
- package/dist-cjs/records/TLShape.js.map +2 -2
- package/dist-cjs/shapes/ShapeWithCrop.js.map +1 -1
- package/dist-cjs/shapes/TLArrowShape.js +13 -26
- package/dist-cjs/shapes/TLArrowShape.js.map +2 -2
- package/dist-cjs/shapes/TLBaseShape.js.map +2 -2
- package/dist-cjs/shapes/TLDrawShape.js +4 -37
- package/dist-cjs/shapes/TLDrawShape.js.map +2 -2
- package/dist-cjs/shapes/TLEmbedShape.js +0 -17
- package/dist-cjs/shapes/TLEmbedShape.js.map +2 -2
- package/dist-cjs/shapes/TLGeoShape.js +1 -12
- package/dist-cjs/shapes/TLGeoShape.js.map +2 -2
- package/dist-cjs/shapes/TLHighlightShape.js +2 -29
- package/dist-cjs/shapes/TLHighlightShape.js.map +2 -2
- package/dist-cjs/shapes/TLNoteShape.js +1 -12
- package/dist-cjs/shapes/TLNoteShape.js.map +2 -2
- package/dist-cjs/shapes/TLTextShape.js +1 -12
- package/dist-cjs/shapes/TLTextShape.js.map +2 -2
- package/dist-cjs/store-migrations.js +15 -15
- package/dist-cjs/store-migrations.js.map +2 -2
- package/dist-esm/bindings/TLBaseBinding.mjs.map +2 -2
- package/dist-esm/createTLSchema.mjs.map +2 -2
- package/dist-esm/index.d.mts +71 -242
- package/dist-esm/index.mjs +1 -5
- package/dist-esm/index.mjs.map +2 -2
- package/dist-esm/misc/TLOpacity.mjs +5 -1
- package/dist-esm/misc/TLOpacity.mjs.map +2 -2
- package/dist-esm/misc/TLRichText.mjs +1 -5
- package/dist-esm/misc/TLRichText.mjs.map +2 -2
- package/dist-esm/records/TLAsset.mjs.map +1 -1
- package/dist-esm/records/TLBinding.mjs.map +2 -2
- package/dist-esm/records/TLShape.mjs.map +2 -2
- package/dist-esm/shapes/TLArrowShape.mjs +13 -26
- package/dist-esm/shapes/TLArrowShape.mjs.map +2 -2
- package/dist-esm/shapes/TLBaseShape.mjs.map +2 -2
- package/dist-esm/shapes/TLDrawShape.mjs +4 -37
- package/dist-esm/shapes/TLDrawShape.mjs.map +2 -2
- package/dist-esm/shapes/TLEmbedShape.mjs +0 -17
- package/dist-esm/shapes/TLEmbedShape.mjs.map +2 -2
- package/dist-esm/shapes/TLGeoShape.mjs +1 -12
- package/dist-esm/shapes/TLGeoShape.mjs.map +2 -2
- package/dist-esm/shapes/TLHighlightShape.mjs +2 -29
- package/dist-esm/shapes/TLHighlightShape.mjs.map +2 -2
- package/dist-esm/shapes/TLNoteShape.mjs +1 -12
- package/dist-esm/shapes/TLNoteShape.mjs.map +2 -2
- package/dist-esm/shapes/TLTextShape.mjs +1 -12
- package/dist-esm/shapes/TLTextShape.mjs.map +2 -2
- package/dist-esm/store-migrations.mjs +15 -15
- package/dist-esm/store-migrations.mjs.map +2 -2
- package/package.json +8 -8
- package/src/__tests__/migrationTestUtils.ts +3 -9
- package/src/assets/TLBookmarkAsset.test.ts +96 -0
- package/src/assets/TLImageAsset.test.ts +213 -0
- package/src/assets/TLVideoAsset.test.ts +105 -0
- package/src/bindings/TLArrowBinding.test.ts +55 -0
- package/src/bindings/TLBaseBinding.ts +14 -25
- package/src/createTLSchema.ts +2 -8
- package/src/index.ts +0 -9
- package/src/migrations.test.ts +1 -149
- package/src/misc/TLOpacity.ts +5 -1
- package/src/misc/TLRichText.ts +1 -6
- package/src/misc/id-validator.test.ts +50 -0
- package/src/records/TLAsset.test.ts +234 -0
- package/src/records/TLAsset.ts +2 -2
- package/src/records/TLBinding.test.ts +22 -0
- package/src/records/TLBinding.ts +23 -65
- package/src/records/TLCamera.test.ts +19 -0
- package/src/records/TLDocument.test.ts +35 -0
- package/src/records/TLInstance.test.ts +201 -0
- package/src/records/TLPage.test.ts +110 -0
- package/src/records/TLPageState.test.ts +228 -0
- package/src/records/TLPointer.test.ts +63 -0
- package/src/records/TLPresence.test.ts +190 -0
- package/src/records/TLRecord.test.ts +70 -0
- package/src/records/TLShape.test.ts +232 -0
- package/src/records/TLShape.ts +5 -100
- package/src/shapes/ShapeWithCrop.test.ts +18 -0
- package/src/shapes/ShapeWithCrop.ts +2 -2
- package/src/shapes/TLArrowShape.test.ts +505 -0
- package/src/shapes/TLArrowShape.ts +14 -28
- package/src/shapes/TLBaseShape.test.ts +142 -0
- package/src/shapes/TLBaseShape.ts +10 -34
- package/src/shapes/TLBookmarkShape.test.ts +122 -0
- package/src/shapes/TLDrawShape.test.ts +177 -0
- package/src/shapes/TLDrawShape.ts +12 -59
- package/src/shapes/TLEmbedShape.test.ts +286 -0
- package/src/shapes/TLEmbedShape.ts +0 -17
- package/src/shapes/TLFrameShape.test.ts +71 -0
- package/src/shapes/TLGeoShape.test.ts +247 -0
- package/src/shapes/TLGeoShape.ts +1 -14
- package/src/shapes/TLGroupShape.test.ts +59 -0
- package/src/shapes/TLHighlightShape.test.ts +325 -0
- package/src/shapes/TLHighlightShape.ts +0 -37
- package/src/shapes/TLImageShape.test.ts +534 -0
- package/src/shapes/TLLineShape.test.ts +269 -0
- package/src/shapes/TLNoteShape.test.ts +1568 -0
- package/src/shapes/TLNoteShape.ts +1 -15
- package/src/shapes/TLTextShape.test.ts +407 -0
- package/src/shapes/TLTextShape.ts +2 -16
- package/src/shapes/TLVideoShape.test.ts +112 -0
- package/src/store-migrations.ts +16 -17
- package/src/styles/TLColorStyle.test.ts +439 -0
- package/dist-cjs/misc/b64Vecs.js +0 -224
- package/dist-cjs/misc/b64Vecs.js.map +0 -7
- package/dist-esm/misc/b64Vecs.mjs +0 -204
- package/dist-esm/misc/b64Vecs.mjs.map +0 -7
- package/src/misc/b64Vecs.ts +0 -308
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest'
|
|
2
|
+
import { cameraMigrations, cameraVersions } from './TLCamera'
|
|
3
|
+
|
|
4
|
+
describe('cameraMigrations', () => {
|
|
5
|
+
it('should apply AddMeta migration correctly', () => {
|
|
6
|
+
const addMetaMigration = cameraMigrations.sequence.find((m) => m.id === cameraVersions.AddMeta)!
|
|
7
|
+
|
|
8
|
+
const oldRecord: any = {
|
|
9
|
+
typeName: 'camera',
|
|
10
|
+
id: 'camera:test',
|
|
11
|
+
x: 100,
|
|
12
|
+
y: 200,
|
|
13
|
+
z: 0.5,
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
addMetaMigration.up(oldRecord)
|
|
17
|
+
expect(oldRecord.meta).toEqual({})
|
|
18
|
+
})
|
|
19
|
+
})
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest'
|
|
2
|
+
import { documentMigrations, documentVersions, TLDOCUMENT_ID } from './TLDocument'
|
|
3
|
+
|
|
4
|
+
describe('documentMigrations', () => {
|
|
5
|
+
it('should apply AddName migration correctly', () => {
|
|
6
|
+
const addNameMigration = documentMigrations.sequence.find(
|
|
7
|
+
(m) => m.id === documentVersions.AddName
|
|
8
|
+
)!
|
|
9
|
+
|
|
10
|
+
const oldRecord: any = {
|
|
11
|
+
typeName: 'document',
|
|
12
|
+
id: TLDOCUMENT_ID,
|
|
13
|
+
gridSize: 10,
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
addNameMigration.up(oldRecord)
|
|
17
|
+
expect(oldRecord.name).toBe('')
|
|
18
|
+
})
|
|
19
|
+
|
|
20
|
+
it('should apply AddMeta migration correctly', () => {
|
|
21
|
+
const addMetaMigration = documentMigrations.sequence.find(
|
|
22
|
+
(m) => m.id === documentVersions.AddMeta
|
|
23
|
+
)!
|
|
24
|
+
|
|
25
|
+
const oldRecord: any = {
|
|
26
|
+
typeName: 'document',
|
|
27
|
+
id: TLDOCUMENT_ID,
|
|
28
|
+
gridSize: 10,
|
|
29
|
+
name: 'Test',
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
addMetaMigration.up(oldRecord)
|
|
33
|
+
expect(oldRecord.meta).toEqual({})
|
|
34
|
+
})
|
|
35
|
+
})
|
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest'
|
|
2
|
+
import { StyleProp } from '../styles/StyleProp'
|
|
3
|
+
import {
|
|
4
|
+
createInstanceRecordType,
|
|
5
|
+
instanceIdValidator,
|
|
6
|
+
instanceMigrations,
|
|
7
|
+
instanceVersions,
|
|
8
|
+
pluckPreservingValues,
|
|
9
|
+
shouldKeyBePreservedBetweenSessions,
|
|
10
|
+
TLInstance,
|
|
11
|
+
TLINSTANCE_ID,
|
|
12
|
+
} from './TLInstance'
|
|
13
|
+
|
|
14
|
+
// Mock style prop for testing
|
|
15
|
+
const mockColorStyle = {
|
|
16
|
+
type: 'color',
|
|
17
|
+
defaultValue: 'black',
|
|
18
|
+
getDefaultValue: () => 'black',
|
|
19
|
+
} as unknown as StyleProp<string>
|
|
20
|
+
|
|
21
|
+
const mockStylesMap = new Map([['color', mockColorStyle]])
|
|
22
|
+
createInstanceRecordType(mockStylesMap)
|
|
23
|
+
|
|
24
|
+
describe('shouldKeyBePreservedBetweenSessions', () => {
|
|
25
|
+
it('should preserve user preferences', () => {
|
|
26
|
+
const userPreferences = [
|
|
27
|
+
'isFocusMode',
|
|
28
|
+
'isDebugMode',
|
|
29
|
+
'isToolLocked',
|
|
30
|
+
'exportBackground',
|
|
31
|
+
'isGridMode',
|
|
32
|
+
'isReadonly',
|
|
33
|
+
]
|
|
34
|
+
|
|
35
|
+
userPreferences.forEach((key) => {
|
|
36
|
+
expect(
|
|
37
|
+
shouldKeyBePreservedBetweenSessions[key as keyof typeof shouldKeyBePreservedBetweenSessions]
|
|
38
|
+
).toBe(true)
|
|
39
|
+
})
|
|
40
|
+
})
|
|
41
|
+
|
|
42
|
+
it('should not preserve temporary state', () => {
|
|
43
|
+
const temporaryState = [
|
|
44
|
+
'currentPageId',
|
|
45
|
+
'opacityForNextShape',
|
|
46
|
+
'stylesForNextShape',
|
|
47
|
+
'followingUserId',
|
|
48
|
+
'brush',
|
|
49
|
+
'cursor',
|
|
50
|
+
'scribbles',
|
|
51
|
+
'zoomBrush',
|
|
52
|
+
'chatMessage',
|
|
53
|
+
'isChatting',
|
|
54
|
+
'isPenMode',
|
|
55
|
+
'isHoveringCanvas',
|
|
56
|
+
'openMenus',
|
|
57
|
+
'isChangingStyle',
|
|
58
|
+
'duplicateProps',
|
|
59
|
+
]
|
|
60
|
+
|
|
61
|
+
temporaryState.forEach((key) => {
|
|
62
|
+
expect(
|
|
63
|
+
shouldKeyBePreservedBetweenSessions[key as keyof typeof shouldKeyBePreservedBetweenSessions]
|
|
64
|
+
).toBe(false)
|
|
65
|
+
})
|
|
66
|
+
})
|
|
67
|
+
})
|
|
68
|
+
|
|
69
|
+
describe('pluckPreservingValues', () => {
|
|
70
|
+
it('should return null for null or undefined input', () => {
|
|
71
|
+
expect(pluckPreservingValues(null)).toBe(null)
|
|
72
|
+
expect(pluckPreservingValues(undefined)).toBe(null)
|
|
73
|
+
})
|
|
74
|
+
|
|
75
|
+
it('should filter properties according to preservation rules', () => {
|
|
76
|
+
const fullInstance: TLInstance = {
|
|
77
|
+
id: TLINSTANCE_ID,
|
|
78
|
+
typeName: 'instance',
|
|
79
|
+
currentPageId: 'page:page1' as any,
|
|
80
|
+
opacityForNextShape: 0.5,
|
|
81
|
+
stylesForNextShape: {},
|
|
82
|
+
followingUserId: null,
|
|
83
|
+
highlightedUserIds: [],
|
|
84
|
+
brush: null,
|
|
85
|
+
cursor: { type: 'default', rotation: 0 },
|
|
86
|
+
scribbles: [],
|
|
87
|
+
isFocusMode: true,
|
|
88
|
+
isDebugMode: false,
|
|
89
|
+
isToolLocked: true,
|
|
90
|
+
exportBackground: true,
|
|
91
|
+
screenBounds: { x: 0, y: 0, w: 1920, h: 1080 },
|
|
92
|
+
insets: [false, false, false, false],
|
|
93
|
+
zoomBrush: null,
|
|
94
|
+
chatMessage: '',
|
|
95
|
+
isChatting: false,
|
|
96
|
+
isPenMode: false,
|
|
97
|
+
isGridMode: true,
|
|
98
|
+
isFocused: true,
|
|
99
|
+
devicePixelRatio: 2,
|
|
100
|
+
isCoarsePointer: false,
|
|
101
|
+
isHoveringCanvas: null,
|
|
102
|
+
openMenus: [],
|
|
103
|
+
isChangingStyle: false,
|
|
104
|
+
isReadonly: false,
|
|
105
|
+
meta: {},
|
|
106
|
+
duplicateProps: null,
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
const preserved = pluckPreservingValues(fullInstance)
|
|
110
|
+
|
|
111
|
+
// Should preserve user preferences
|
|
112
|
+
expect(preserved?.isFocusMode).toBe(true)
|
|
113
|
+
expect(preserved?.isDebugMode).toBe(false)
|
|
114
|
+
expect(preserved?.isToolLocked).toBe(true)
|
|
115
|
+
expect(preserved?.exportBackground).toBe(true)
|
|
116
|
+
expect(preserved?.isGridMode).toBe(true)
|
|
117
|
+
|
|
118
|
+
// Should not preserve temporary state
|
|
119
|
+
expect(preserved?.currentPageId).toBeUndefined()
|
|
120
|
+
expect(preserved?.opacityForNextShape).toBeUndefined()
|
|
121
|
+
expect(preserved?.brush).toBeUndefined()
|
|
122
|
+
expect(preserved?.cursor).toBeUndefined()
|
|
123
|
+
expect(preserved?.chatMessage).toBeUndefined()
|
|
124
|
+
expect(preserved?.openMenus).toBeUndefined()
|
|
125
|
+
})
|
|
126
|
+
})
|
|
127
|
+
|
|
128
|
+
describe('instanceIdValidator', () => {
|
|
129
|
+
it('should validate correct instance IDs and reject invalid ones', () => {
|
|
130
|
+
expect(() => instanceIdValidator.validate('instance:instance')).not.toThrow()
|
|
131
|
+
expect(() => instanceIdValidator.validate('instance:test')).not.toThrow()
|
|
132
|
+
expect(() => instanceIdValidator.validate(TLINSTANCE_ID)).not.toThrow()
|
|
133
|
+
|
|
134
|
+
expect(() => instanceIdValidator.validate('invalid')).toThrow()
|
|
135
|
+
expect(() => instanceIdValidator.validate('page:instance')).toThrow()
|
|
136
|
+
expect(() => instanceIdValidator.validate('')).toThrow()
|
|
137
|
+
})
|
|
138
|
+
})
|
|
139
|
+
|
|
140
|
+
describe('createInstanceRecordType', () => {
|
|
141
|
+
it('should create a valid record type with correct configuration', () => {
|
|
142
|
+
const recordType = createInstanceRecordType(mockStylesMap)
|
|
143
|
+
expect(recordType.typeName).toBe('instance')
|
|
144
|
+
expect(recordType.scope).toBe('session')
|
|
145
|
+
})
|
|
146
|
+
})
|
|
147
|
+
|
|
148
|
+
describe('instanceMigrations', () => {
|
|
149
|
+
it('should have correct migration configuration', () => {
|
|
150
|
+
expect(instanceMigrations.sequenceId).toBe('com.tldraw.instance')
|
|
151
|
+
expect(Array.isArray(instanceMigrations.sequence)).toBe(true)
|
|
152
|
+
expect(instanceMigrations.sequence.length).toBe(25)
|
|
153
|
+
})
|
|
154
|
+
|
|
155
|
+
it('should migrate HoistOpacity correctly', () => {
|
|
156
|
+
const migration = instanceMigrations.sequence.find(
|
|
157
|
+
(m) => m.id === instanceVersions.HoistOpacity
|
|
158
|
+
)!
|
|
159
|
+
const oldRecord: any = {
|
|
160
|
+
id: TLINSTANCE_ID,
|
|
161
|
+
typeName: 'instance',
|
|
162
|
+
propsForNextShape: {
|
|
163
|
+
opacity: '0.5',
|
|
164
|
+
color: 'red',
|
|
165
|
+
},
|
|
166
|
+
}
|
|
167
|
+
const result = migration.up(oldRecord)
|
|
168
|
+
|
|
169
|
+
expect((result as any).opacityForNextShape).toBe(0.5)
|
|
170
|
+
expect((result as any).propsForNextShape.opacity).toBeUndefined()
|
|
171
|
+
expect((result as any).propsForNextShape.color).toBe('red')
|
|
172
|
+
})
|
|
173
|
+
|
|
174
|
+
it('should migrate RemoveDialog correctly', () => {
|
|
175
|
+
const migration = instanceMigrations.sequence.find(
|
|
176
|
+
(m) => m.id === instanceVersions.RemoveDialog
|
|
177
|
+
)!
|
|
178
|
+
const oldRecord: any = {
|
|
179
|
+
id: TLINSTANCE_ID,
|
|
180
|
+
typeName: 'instance',
|
|
181
|
+
dialog: 'some-dialog',
|
|
182
|
+
otherProp: 'keep-me',
|
|
183
|
+
}
|
|
184
|
+
const result = migration.up(oldRecord)
|
|
185
|
+
|
|
186
|
+
expect((result as any).dialog).toBeUndefined()
|
|
187
|
+
expect((result as any).otherProp).toBe('keep-me')
|
|
188
|
+
})
|
|
189
|
+
|
|
190
|
+
it('should have bidirectional migrations where applicable', () => {
|
|
191
|
+
const addInsetMigration = instanceMigrations.sequence.find(
|
|
192
|
+
(m) => m.id === instanceVersions.AddInset
|
|
193
|
+
)!
|
|
194
|
+
expect(addInsetMigration.down).toBeDefined()
|
|
195
|
+
|
|
196
|
+
const removeCameraMigration = instanceMigrations.sequence.find(
|
|
197
|
+
(m) => m.id === instanceVersions.RemoveCanMoveCamera
|
|
198
|
+
)!
|
|
199
|
+
expect(removeCameraMigration.down).toBeDefined()
|
|
200
|
+
})
|
|
201
|
+
})
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest'
|
|
2
|
+
import {
|
|
3
|
+
isPageId,
|
|
4
|
+
pageIdValidator,
|
|
5
|
+
pageMigrations,
|
|
6
|
+
PageRecordType,
|
|
7
|
+
pageValidator,
|
|
8
|
+
pageVersions,
|
|
9
|
+
TLPageId,
|
|
10
|
+
} from './TLPage'
|
|
11
|
+
|
|
12
|
+
describe('pageIdValidator', () => {
|
|
13
|
+
it('should validate correct page IDs', () => {
|
|
14
|
+
expect(() => pageIdValidator.validate('page:main')).not.toThrow()
|
|
15
|
+
expect(() => pageIdValidator.validate('page:page1')).not.toThrow()
|
|
16
|
+
})
|
|
17
|
+
|
|
18
|
+
it('should reject invalid page IDs', () => {
|
|
19
|
+
expect(() => pageIdValidator.validate('invalid')).toThrow()
|
|
20
|
+
expect(() => pageIdValidator.validate('shape:page1')).toThrow()
|
|
21
|
+
expect(() => pageIdValidator.validate('')).toThrow()
|
|
22
|
+
})
|
|
23
|
+
})
|
|
24
|
+
|
|
25
|
+
describe('pageValidator', () => {
|
|
26
|
+
it('should validate valid page records', () => {
|
|
27
|
+
const validPage = {
|
|
28
|
+
typeName: 'page',
|
|
29
|
+
id: 'page:test' as TLPageId,
|
|
30
|
+
name: 'Test Page',
|
|
31
|
+
index: 'a1' as any,
|
|
32
|
+
meta: {},
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
expect(() => pageValidator.validate(validPage)).not.toThrow()
|
|
36
|
+
})
|
|
37
|
+
|
|
38
|
+
it('should reject pages with invalid typeName', () => {
|
|
39
|
+
const invalidPage = {
|
|
40
|
+
typeName: 'not-page',
|
|
41
|
+
id: 'page:test' as TLPageId,
|
|
42
|
+
name: 'Test',
|
|
43
|
+
index: 'a1' as any,
|
|
44
|
+
meta: {},
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
expect(() => pageValidator.validate(invalidPage)).toThrow()
|
|
48
|
+
})
|
|
49
|
+
|
|
50
|
+
it('should reject pages with missing required fields', () => {
|
|
51
|
+
const incompletePages = [
|
|
52
|
+
{
|
|
53
|
+
typeName: 'page',
|
|
54
|
+
id: 'page:test' as TLPageId,
|
|
55
|
+
// missing name, index, meta
|
|
56
|
+
},
|
|
57
|
+
{
|
|
58
|
+
typeName: 'page',
|
|
59
|
+
id: 'page:test' as TLPageId,
|
|
60
|
+
name: 'Test',
|
|
61
|
+
// missing index, meta
|
|
62
|
+
},
|
|
63
|
+
]
|
|
64
|
+
|
|
65
|
+
incompletePages.forEach((page) => {
|
|
66
|
+
expect(() => pageValidator.validate(page)).toThrow()
|
|
67
|
+
})
|
|
68
|
+
})
|
|
69
|
+
})
|
|
70
|
+
|
|
71
|
+
describe('pageMigrations', () => {
|
|
72
|
+
it('should apply AddMeta migration correctly', () => {
|
|
73
|
+
const addMetaMigration = pageMigrations.sequence.find((m) => m.id === pageVersions.AddMeta)!
|
|
74
|
+
|
|
75
|
+
const oldRecord: any = {
|
|
76
|
+
typeName: 'page',
|
|
77
|
+
id: 'page:test',
|
|
78
|
+
name: 'Test Page',
|
|
79
|
+
index: 'a1',
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
addMetaMigration.up(oldRecord)
|
|
83
|
+
expect(oldRecord.meta).toEqual({})
|
|
84
|
+
})
|
|
85
|
+
})
|
|
86
|
+
|
|
87
|
+
describe('PageRecordType', () => {
|
|
88
|
+
it('should create page records with defaults', () => {
|
|
89
|
+
const page = PageRecordType.create({
|
|
90
|
+
id: 'page:test' as TLPageId,
|
|
91
|
+
name: 'Test Page',
|
|
92
|
+
index: 'a1' as any,
|
|
93
|
+
})
|
|
94
|
+
|
|
95
|
+
expect(page.meta).toEqual({})
|
|
96
|
+
})
|
|
97
|
+
})
|
|
98
|
+
|
|
99
|
+
describe('isPageId', () => {
|
|
100
|
+
it('should return true for valid page IDs', () => {
|
|
101
|
+
expect(isPageId('page:main')).toBe(true)
|
|
102
|
+
expect(isPageId('page:page1')).toBe(true)
|
|
103
|
+
})
|
|
104
|
+
|
|
105
|
+
it('should return false for invalid page IDs', () => {
|
|
106
|
+
expect(isPageId('shape:main')).toBe(false)
|
|
107
|
+
expect(isPageId('invalid')).toBe(false)
|
|
108
|
+
expect(isPageId('')).toBe(false)
|
|
109
|
+
})
|
|
110
|
+
})
|
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest'
|
|
2
|
+
import {
|
|
3
|
+
instancePageStateMigrations,
|
|
4
|
+
InstancePageStateRecordType,
|
|
5
|
+
instancePageStateValidator,
|
|
6
|
+
instancePageStateVersions,
|
|
7
|
+
TLInstancePageStateId,
|
|
8
|
+
} from './TLPageState'
|
|
9
|
+
|
|
10
|
+
describe('instancePageStateValidator', () => {
|
|
11
|
+
it('should reject invalid typeName', () => {
|
|
12
|
+
const invalidPageState = {
|
|
13
|
+
typeName: 'not-instance-page-state',
|
|
14
|
+
id: 'instance_page_state:test' as TLInstancePageStateId,
|
|
15
|
+
pageId: 'page:test' as any,
|
|
16
|
+
selectedShapeIds: [],
|
|
17
|
+
hintingShapeIds: [],
|
|
18
|
+
erasingShapeIds: [],
|
|
19
|
+
hoveredShapeId: null,
|
|
20
|
+
editingShapeId: null,
|
|
21
|
+
croppingShapeId: null,
|
|
22
|
+
focusedGroupId: null,
|
|
23
|
+
meta: {},
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
expect(() => instancePageStateValidator.validate(invalidPageState)).toThrow()
|
|
27
|
+
})
|
|
28
|
+
|
|
29
|
+
it('should reject invalid id format', () => {
|
|
30
|
+
const invalidPageState = {
|
|
31
|
+
typeName: 'instance_page_state',
|
|
32
|
+
id: 'not-valid-id' as TLInstancePageStateId,
|
|
33
|
+
pageId: 'page:test' as any,
|
|
34
|
+
selectedShapeIds: [],
|
|
35
|
+
hintingShapeIds: [],
|
|
36
|
+
erasingShapeIds: [],
|
|
37
|
+
hoveredShapeId: null,
|
|
38
|
+
editingShapeId: null,
|
|
39
|
+
croppingShapeId: null,
|
|
40
|
+
focusedGroupId: null,
|
|
41
|
+
meta: {},
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
expect(() => instancePageStateValidator.validate(invalidPageState)).toThrow()
|
|
45
|
+
})
|
|
46
|
+
|
|
47
|
+
it('should reject missing required fields', () => {
|
|
48
|
+
const incompletePageState = {
|
|
49
|
+
typeName: 'instance_page_state',
|
|
50
|
+
id: 'instance_page_state:test' as TLInstancePageStateId,
|
|
51
|
+
pageId: 'page:test' as any,
|
|
52
|
+
// missing required array fields
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
expect(() => instancePageStateValidator.validate(incompletePageState)).toThrow()
|
|
56
|
+
})
|
|
57
|
+
})
|
|
58
|
+
|
|
59
|
+
describe('instancePageStateMigrations', () => {
|
|
60
|
+
it('should migrate AddCroppingId correctly', () => {
|
|
61
|
+
const migration = instancePageStateMigrations.sequence.find(
|
|
62
|
+
(m) => m.id === instancePageStateVersions.AddCroppingId
|
|
63
|
+
)!
|
|
64
|
+
const oldRecord: any = { id: 'instance_page_state:test', typeName: 'instance_page_state' }
|
|
65
|
+
migration.up(oldRecord)
|
|
66
|
+
|
|
67
|
+
expect(oldRecord.croppingShapeId).toBe(null)
|
|
68
|
+
})
|
|
69
|
+
|
|
70
|
+
it('should migrate RemoveInstanceIdAndCameraId correctly', () => {
|
|
71
|
+
const migration = instancePageStateMigrations.sequence.find(
|
|
72
|
+
(m) => m.id === instancePageStateVersions.RemoveInstanceIdAndCameraId
|
|
73
|
+
)!
|
|
74
|
+
const oldRecord: any = {
|
|
75
|
+
id: 'instance_page_state:test',
|
|
76
|
+
typeName: 'instance_page_state',
|
|
77
|
+
instanceId: 'instance:removed',
|
|
78
|
+
cameraId: 'camera:removed',
|
|
79
|
+
otherProp: 'keep-me',
|
|
80
|
+
}
|
|
81
|
+
migration.up(oldRecord)
|
|
82
|
+
|
|
83
|
+
expect(oldRecord.instanceId).toBeUndefined()
|
|
84
|
+
expect(oldRecord.cameraId).toBeUndefined()
|
|
85
|
+
expect(oldRecord.otherProp).toBe('keep-me')
|
|
86
|
+
})
|
|
87
|
+
|
|
88
|
+
it('should migrate AddMeta correctly', () => {
|
|
89
|
+
const migration = instancePageStateMigrations.sequence.find(
|
|
90
|
+
(m) => m.id === instancePageStateVersions.AddMeta
|
|
91
|
+
)!
|
|
92
|
+
const oldRecord: any = { id: 'instance_page_state:test', typeName: 'instance_page_state' }
|
|
93
|
+
migration.up(oldRecord)
|
|
94
|
+
|
|
95
|
+
expect(oldRecord.meta).toEqual({})
|
|
96
|
+
})
|
|
97
|
+
|
|
98
|
+
it('should handle RenameProperties migration (noop)', () => {
|
|
99
|
+
const migration = instancePageStateMigrations.sequence.find(
|
|
100
|
+
(m) => m.id === instancePageStateVersions.RenameProperties
|
|
101
|
+
)!
|
|
102
|
+
const oldRecord: any = {
|
|
103
|
+
id: 'instance_page_state:test',
|
|
104
|
+
typeName: 'instance_page_state',
|
|
105
|
+
selectedIds: ['shape:1'],
|
|
106
|
+
}
|
|
107
|
+
const originalRecord = { ...oldRecord }
|
|
108
|
+
migration.up(oldRecord)
|
|
109
|
+
|
|
110
|
+
// Should be a noop
|
|
111
|
+
expect(oldRecord).toEqual(originalRecord)
|
|
112
|
+
})
|
|
113
|
+
|
|
114
|
+
it('should migrate RenamePropertiesAgain correctly', () => {
|
|
115
|
+
const migration = instancePageStateMigrations.sequence.find(
|
|
116
|
+
(m) => m.id === instancePageStateVersions.RenamePropertiesAgain
|
|
117
|
+
)!
|
|
118
|
+
const oldRecord: any = {
|
|
119
|
+
id: 'instance_page_state:test',
|
|
120
|
+
typeName: 'instance_page_state',
|
|
121
|
+
selectedIds: ['shape:1', 'shape:2'],
|
|
122
|
+
hintingIds: ['shape:3'],
|
|
123
|
+
erasingIds: ['shape:4'],
|
|
124
|
+
hoveredId: 'shape:5',
|
|
125
|
+
editingId: 'shape:6',
|
|
126
|
+
croppingId: 'shape:7',
|
|
127
|
+
focusLayerId: 'shape:8',
|
|
128
|
+
}
|
|
129
|
+
migration.up(oldRecord)
|
|
130
|
+
|
|
131
|
+
expect(oldRecord.selectedShapeIds).toEqual(['shape:1', 'shape:2'])
|
|
132
|
+
expect(oldRecord.hintingShapeIds).toEqual(['shape:3'])
|
|
133
|
+
expect(oldRecord.erasingShapeIds).toEqual(['shape:4'])
|
|
134
|
+
expect(oldRecord.hoveredShapeId).toBe('shape:5')
|
|
135
|
+
expect(oldRecord.editingShapeId).toBe('shape:6')
|
|
136
|
+
expect(oldRecord.croppingShapeId).toBe('shape:7')
|
|
137
|
+
expect(oldRecord.focusedGroupId).toBe('shape:8')
|
|
138
|
+
|
|
139
|
+
// Old properties should be removed
|
|
140
|
+
expect(oldRecord.selectedIds).toBeUndefined()
|
|
141
|
+
expect(oldRecord.hintingIds).toBeUndefined()
|
|
142
|
+
expect(oldRecord.erasingIds).toBeUndefined()
|
|
143
|
+
expect(oldRecord.hoveredId).toBeUndefined()
|
|
144
|
+
expect(oldRecord.editingId).toBeUndefined()
|
|
145
|
+
expect(oldRecord.croppingId).toBeUndefined()
|
|
146
|
+
expect(oldRecord.focusLayerId).toBeUndefined()
|
|
147
|
+
})
|
|
148
|
+
|
|
149
|
+
it('should handle down migration for RenamePropertiesAgain', () => {
|
|
150
|
+
const migration = instancePageStateMigrations.sequence.find(
|
|
151
|
+
(m) => m.id === instancePageStateVersions.RenamePropertiesAgain
|
|
152
|
+
)!
|
|
153
|
+
expect(migration.down).toBeDefined()
|
|
154
|
+
|
|
155
|
+
const record: any = {
|
|
156
|
+
id: 'instance_page_state:test',
|
|
157
|
+
typeName: 'instance_page_state',
|
|
158
|
+
selectedShapeIds: ['shape:1', 'shape:2'],
|
|
159
|
+
hintingShapeIds: ['shape:3'],
|
|
160
|
+
erasingShapeIds: ['shape:4'],
|
|
161
|
+
hoveredShapeId: 'shape:5',
|
|
162
|
+
editingShapeId: 'shape:6',
|
|
163
|
+
croppingShapeId: 'shape:7',
|
|
164
|
+
focusedGroupId: 'shape:8',
|
|
165
|
+
}
|
|
166
|
+
migration.down!(record)
|
|
167
|
+
|
|
168
|
+
expect(record.selectedIds).toEqual(['shape:1', 'shape:2'])
|
|
169
|
+
expect(record.hintingIds).toEqual(['shape:3'])
|
|
170
|
+
expect(record.erasingIds).toEqual(['shape:4'])
|
|
171
|
+
expect(record.hoveredId).toBe('shape:5')
|
|
172
|
+
expect(record.editingId).toBe('shape:6')
|
|
173
|
+
expect(record.croppingId).toBe('shape:7')
|
|
174
|
+
expect(record.focusLayerId).toBe('shape:8')
|
|
175
|
+
|
|
176
|
+
// New properties should be removed
|
|
177
|
+
expect(record.selectedShapeIds).toBeUndefined()
|
|
178
|
+
expect(record.hintingShapeIds).toBeUndefined()
|
|
179
|
+
expect(record.erasingShapeIds).toBeUndefined()
|
|
180
|
+
expect(record.hoveredShapeId).toBeUndefined()
|
|
181
|
+
expect(record.editingShapeId).toBeUndefined()
|
|
182
|
+
expect(record.croppingShapeId).toBeUndefined()
|
|
183
|
+
expect(record.focusedGroupId).toBeUndefined()
|
|
184
|
+
})
|
|
185
|
+
|
|
186
|
+
it('should handle croppingShapeId fallback in RenamePropertiesAgain', () => {
|
|
187
|
+
const migration = instancePageStateMigrations.sequence.find(
|
|
188
|
+
(m) => m.id === instancePageStateVersions.RenamePropertiesAgain
|
|
189
|
+
)!
|
|
190
|
+
|
|
191
|
+
// Test with existing croppingShapeId
|
|
192
|
+
const recordWithCroppingShapeId: any = {
|
|
193
|
+
croppingShapeId: 'shape:existing',
|
|
194
|
+
croppingId: 'shape:fallback',
|
|
195
|
+
}
|
|
196
|
+
migration.up(recordWithCroppingShapeId)
|
|
197
|
+
expect(recordWithCroppingShapeId.croppingShapeId).toBe('shape:existing')
|
|
198
|
+
|
|
199
|
+
// Test with only croppingId
|
|
200
|
+
const recordWithCroppingId: any = {
|
|
201
|
+
croppingId: 'shape:fallback',
|
|
202
|
+
}
|
|
203
|
+
migration.up(recordWithCroppingId)
|
|
204
|
+
expect(recordWithCroppingId.croppingShapeId).toBe('shape:fallback')
|
|
205
|
+
|
|
206
|
+
// Test with neither
|
|
207
|
+
const recordWithNeither: any = {}
|
|
208
|
+
migration.up(recordWithNeither)
|
|
209
|
+
expect(recordWithNeither.croppingShapeId).toBe(null)
|
|
210
|
+
})
|
|
211
|
+
})
|
|
212
|
+
|
|
213
|
+
describe('InstancePageStateRecordType', () => {
|
|
214
|
+
it('should have correct ephemeral keys configuration', () => {
|
|
215
|
+
// Non-ephemeral keys (persistent)
|
|
216
|
+
expect(InstancePageStateRecordType.ephemeralKeys?.pageId).toBe(false)
|
|
217
|
+
expect(InstancePageStateRecordType.ephemeralKeys?.selectedShapeIds).toBe(false)
|
|
218
|
+
expect(InstancePageStateRecordType.ephemeralKeys?.editingShapeId).toBe(false)
|
|
219
|
+
expect(InstancePageStateRecordType.ephemeralKeys?.croppingShapeId).toBe(false)
|
|
220
|
+
expect(InstancePageStateRecordType.ephemeralKeys?.meta).toBe(false)
|
|
221
|
+
|
|
222
|
+
// Ephemeral keys (temporary)
|
|
223
|
+
expect(InstancePageStateRecordType.ephemeralKeys?.hintingShapeIds).toBe(true)
|
|
224
|
+
expect(InstancePageStateRecordType.ephemeralKeys?.erasingShapeIds).toBe(true)
|
|
225
|
+
expect(InstancePageStateRecordType.ephemeralKeys?.hoveredShapeId).toBe(true)
|
|
226
|
+
expect(InstancePageStateRecordType.ephemeralKeys?.focusedGroupId).toBe(true)
|
|
227
|
+
})
|
|
228
|
+
})
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest'
|
|
2
|
+
import { pointerMigrations, pointerValidator, pointerVersions, TLPOINTER_ID } from './TLPointer'
|
|
3
|
+
|
|
4
|
+
describe('pointerValidator', () => {
|
|
5
|
+
it('should validate valid pointer records', () => {
|
|
6
|
+
const validPointer = {
|
|
7
|
+
typeName: 'pointer',
|
|
8
|
+
id: TLPOINTER_ID,
|
|
9
|
+
x: 100,
|
|
10
|
+
y: 200,
|
|
11
|
+
lastActivityTimestamp: Date.now(),
|
|
12
|
+
meta: {},
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
expect(() => pointerValidator.validate(validPointer)).not.toThrow()
|
|
16
|
+
})
|
|
17
|
+
|
|
18
|
+
it('should reject pointers with invalid typeName', () => {
|
|
19
|
+
const invalidPointer = {
|
|
20
|
+
typeName: 'not-pointer',
|
|
21
|
+
id: TLPOINTER_ID,
|
|
22
|
+
x: 0,
|
|
23
|
+
y: 0,
|
|
24
|
+
lastActivityTimestamp: 0,
|
|
25
|
+
meta: {},
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
expect(() => pointerValidator.validate(invalidPointer)).toThrow()
|
|
29
|
+
})
|
|
30
|
+
|
|
31
|
+
it('should reject pointers with missing required fields', () => {
|
|
32
|
+
const incompletePointer = {
|
|
33
|
+
typeName: 'pointer',
|
|
34
|
+
id: TLPOINTER_ID,
|
|
35
|
+
x: 0,
|
|
36
|
+
// missing y, lastActivityTimestamp, meta
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
expect(() => pointerValidator.validate(incompletePointer)).toThrow()
|
|
40
|
+
})
|
|
41
|
+
})
|
|
42
|
+
|
|
43
|
+
describe('pointerMigrations', () => {
|
|
44
|
+
it('should apply AddMeta migration correctly', () => {
|
|
45
|
+
const addMetaMigration = pointerMigrations.sequence.find(
|
|
46
|
+
(m) => m.id === pointerVersions.AddMeta
|
|
47
|
+
)!
|
|
48
|
+
|
|
49
|
+
const oldRecord: any = {
|
|
50
|
+
typeName: 'pointer',
|
|
51
|
+
id: TLPOINTER_ID,
|
|
52
|
+
x: 100,
|
|
53
|
+
y: 200,
|
|
54
|
+
lastActivityTimestamp: 123456,
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
addMetaMigration.up(oldRecord)
|
|
58
|
+
expect(oldRecord.meta).toEqual({})
|
|
59
|
+
expect(oldRecord.x).toBe(100)
|
|
60
|
+
expect(oldRecord.y).toBe(200)
|
|
61
|
+
expect(oldRecord.lastActivityTimestamp).toBe(123456)
|
|
62
|
+
})
|
|
63
|
+
})
|