@tldraw/tlschema 4.2.2 → 4.2.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (117) hide show
  1. package/dist-cjs/bindings/TLBaseBinding.js.map +2 -2
  2. package/dist-cjs/createTLSchema.js.map +2 -2
  3. package/dist-cjs/index.d.ts +71 -242
  4. package/dist-cjs/index.js +1 -4
  5. package/dist-cjs/index.js.map +2 -2
  6. package/dist-cjs/misc/TLOpacity.js +5 -1
  7. package/dist-cjs/misc/TLOpacity.js.map +2 -2
  8. package/dist-cjs/misc/TLRichText.js +1 -5
  9. package/dist-cjs/misc/TLRichText.js.map +2 -2
  10. package/dist-cjs/records/TLAsset.js.map +1 -1
  11. package/dist-cjs/records/TLBinding.js.map +2 -2
  12. package/dist-cjs/records/TLShape.js.map +2 -2
  13. package/dist-cjs/shapes/ShapeWithCrop.js.map +1 -1
  14. package/dist-cjs/shapes/TLArrowShape.js +13 -26
  15. package/dist-cjs/shapes/TLArrowShape.js.map +2 -2
  16. package/dist-cjs/shapes/TLBaseShape.js.map +2 -2
  17. package/dist-cjs/shapes/TLDrawShape.js +4 -37
  18. package/dist-cjs/shapes/TLDrawShape.js.map +2 -2
  19. package/dist-cjs/shapes/TLEmbedShape.js +0 -17
  20. package/dist-cjs/shapes/TLEmbedShape.js.map +2 -2
  21. package/dist-cjs/shapes/TLGeoShape.js +1 -12
  22. package/dist-cjs/shapes/TLGeoShape.js.map +2 -2
  23. package/dist-cjs/shapes/TLHighlightShape.js +2 -29
  24. package/dist-cjs/shapes/TLHighlightShape.js.map +2 -2
  25. package/dist-cjs/shapes/TLNoteShape.js +1 -12
  26. package/dist-cjs/shapes/TLNoteShape.js.map +2 -2
  27. package/dist-cjs/shapes/TLTextShape.js +1 -12
  28. package/dist-cjs/shapes/TLTextShape.js.map +2 -2
  29. package/dist-cjs/store-migrations.js +15 -15
  30. package/dist-cjs/store-migrations.js.map +2 -2
  31. package/dist-esm/bindings/TLBaseBinding.mjs.map +2 -2
  32. package/dist-esm/createTLSchema.mjs.map +2 -2
  33. package/dist-esm/index.d.mts +71 -242
  34. package/dist-esm/index.mjs +1 -5
  35. package/dist-esm/index.mjs.map +2 -2
  36. package/dist-esm/misc/TLOpacity.mjs +5 -1
  37. package/dist-esm/misc/TLOpacity.mjs.map +2 -2
  38. package/dist-esm/misc/TLRichText.mjs +1 -5
  39. package/dist-esm/misc/TLRichText.mjs.map +2 -2
  40. package/dist-esm/records/TLAsset.mjs.map +1 -1
  41. package/dist-esm/records/TLBinding.mjs.map +2 -2
  42. package/dist-esm/records/TLShape.mjs.map +2 -2
  43. package/dist-esm/shapes/TLArrowShape.mjs +13 -26
  44. package/dist-esm/shapes/TLArrowShape.mjs.map +2 -2
  45. package/dist-esm/shapes/TLBaseShape.mjs.map +2 -2
  46. package/dist-esm/shapes/TLDrawShape.mjs +4 -37
  47. package/dist-esm/shapes/TLDrawShape.mjs.map +2 -2
  48. package/dist-esm/shapes/TLEmbedShape.mjs +0 -17
  49. package/dist-esm/shapes/TLEmbedShape.mjs.map +2 -2
  50. package/dist-esm/shapes/TLGeoShape.mjs +1 -12
  51. package/dist-esm/shapes/TLGeoShape.mjs.map +2 -2
  52. package/dist-esm/shapes/TLHighlightShape.mjs +2 -29
  53. package/dist-esm/shapes/TLHighlightShape.mjs.map +2 -2
  54. package/dist-esm/shapes/TLNoteShape.mjs +1 -12
  55. package/dist-esm/shapes/TLNoteShape.mjs.map +2 -2
  56. package/dist-esm/shapes/TLTextShape.mjs +1 -12
  57. package/dist-esm/shapes/TLTextShape.mjs.map +2 -2
  58. package/dist-esm/store-migrations.mjs +15 -15
  59. package/dist-esm/store-migrations.mjs.map +2 -2
  60. package/package.json +8 -8
  61. package/src/__tests__/migrationTestUtils.ts +3 -9
  62. package/src/assets/TLBookmarkAsset.test.ts +96 -0
  63. package/src/assets/TLImageAsset.test.ts +213 -0
  64. package/src/assets/TLVideoAsset.test.ts +105 -0
  65. package/src/bindings/TLArrowBinding.test.ts +55 -0
  66. package/src/bindings/TLBaseBinding.ts +14 -25
  67. package/src/createTLSchema.ts +2 -8
  68. package/src/index.ts +0 -9
  69. package/src/migrations.test.ts +1 -149
  70. package/src/misc/TLOpacity.ts +5 -1
  71. package/src/misc/TLRichText.ts +1 -6
  72. package/src/misc/id-validator.test.ts +50 -0
  73. package/src/records/TLAsset.test.ts +234 -0
  74. package/src/records/TLAsset.ts +2 -2
  75. package/src/records/TLBinding.test.ts +22 -0
  76. package/src/records/TLBinding.ts +23 -65
  77. package/src/records/TLCamera.test.ts +19 -0
  78. package/src/records/TLDocument.test.ts +35 -0
  79. package/src/records/TLInstance.test.ts +201 -0
  80. package/src/records/TLPage.test.ts +110 -0
  81. package/src/records/TLPageState.test.ts +228 -0
  82. package/src/records/TLPointer.test.ts +63 -0
  83. package/src/records/TLPresence.test.ts +190 -0
  84. package/src/records/TLRecord.test.ts +70 -0
  85. package/src/records/TLShape.test.ts +232 -0
  86. package/src/records/TLShape.ts +5 -100
  87. package/src/shapes/ShapeWithCrop.test.ts +18 -0
  88. package/src/shapes/ShapeWithCrop.ts +2 -2
  89. package/src/shapes/TLArrowShape.test.ts +505 -0
  90. package/src/shapes/TLArrowShape.ts +14 -28
  91. package/src/shapes/TLBaseShape.test.ts +142 -0
  92. package/src/shapes/TLBaseShape.ts +10 -34
  93. package/src/shapes/TLBookmarkShape.test.ts +122 -0
  94. package/src/shapes/TLDrawShape.test.ts +177 -0
  95. package/src/shapes/TLDrawShape.ts +12 -59
  96. package/src/shapes/TLEmbedShape.test.ts +286 -0
  97. package/src/shapes/TLEmbedShape.ts +0 -17
  98. package/src/shapes/TLFrameShape.test.ts +71 -0
  99. package/src/shapes/TLGeoShape.test.ts +247 -0
  100. package/src/shapes/TLGeoShape.ts +1 -14
  101. package/src/shapes/TLGroupShape.test.ts +59 -0
  102. package/src/shapes/TLHighlightShape.test.ts +325 -0
  103. package/src/shapes/TLHighlightShape.ts +0 -37
  104. package/src/shapes/TLImageShape.test.ts +534 -0
  105. package/src/shapes/TLLineShape.test.ts +269 -0
  106. package/src/shapes/TLNoteShape.test.ts +1568 -0
  107. package/src/shapes/TLNoteShape.ts +1 -15
  108. package/src/shapes/TLTextShape.test.ts +407 -0
  109. package/src/shapes/TLTextShape.ts +2 -16
  110. package/src/shapes/TLVideoShape.test.ts +112 -0
  111. package/src/store-migrations.ts +16 -17
  112. package/src/styles/TLColorStyle.test.ts +439 -0
  113. package/dist-cjs/misc/b64Vecs.js +0 -224
  114. package/dist-cjs/misc/b64Vecs.js.map +0 -7
  115. package/dist-esm/misc/b64Vecs.mjs +0 -204
  116. package/dist-esm/misc/b64Vecs.mjs.map +0 -7
  117. package/src/misc/b64Vecs.ts +0 -308
@@ -0,0 +1,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
+ })