@tldraw/tlschema 4.1.0-canary.e653ec63c99b → 4.1.0-canary.e87046ba1a0c
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,644 @@
|
|
|
1
|
+
import { Store } from '@tldraw/store'
|
|
2
|
+
import { annotateError, IndexKey, structuredClone } from '@tldraw/utils'
|
|
3
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
|
4
|
+
import { createTLSchema } from './createTLSchema'
|
|
5
|
+
import { CameraRecordType } from './records/TLCamera'
|
|
6
|
+
import { TLDOCUMENT_ID } from './records/TLDocument'
|
|
7
|
+
import { TLINSTANCE_ID } from './records/TLInstance'
|
|
8
|
+
import { PageRecordType, TLPageId } from './records/TLPage'
|
|
9
|
+
import { InstancePageStateRecordType } from './records/TLPageState'
|
|
10
|
+
import { TLPOINTER_ID } from './records/TLPointer'
|
|
11
|
+
import { TLRecord } from './records/TLRecord'
|
|
12
|
+
import { TLShapeId } from './records/TLShape'
|
|
13
|
+
import {
|
|
14
|
+
createIntegrityChecker,
|
|
15
|
+
onValidationFailure,
|
|
16
|
+
redactRecordForErrorReporting,
|
|
17
|
+
TLAssetStore,
|
|
18
|
+
TLStoreProps,
|
|
19
|
+
} from './TLStore'
|
|
20
|
+
|
|
21
|
+
// Mock dependencies
|
|
22
|
+
vi.mock('@tldraw/utils', async () => {
|
|
23
|
+
const actual = await vi.importActual('@tldraw/utils')
|
|
24
|
+
return {
|
|
25
|
+
...actual,
|
|
26
|
+
annotateError: vi.fn(),
|
|
27
|
+
structuredClone: vi.fn((obj) => JSON.parse(JSON.stringify(obj))),
|
|
28
|
+
}
|
|
29
|
+
})
|
|
30
|
+
|
|
31
|
+
describe('TLStore utility functions', () => {
|
|
32
|
+
beforeEach(() => {
|
|
33
|
+
vi.clearAllMocks()
|
|
34
|
+
})
|
|
35
|
+
|
|
36
|
+
describe('redactRecordForErrorReporting', () => {
|
|
37
|
+
it('should redact src field from asset record', () => {
|
|
38
|
+
const assetRecord = {
|
|
39
|
+
id: 'asset:test',
|
|
40
|
+
typeName: 'asset',
|
|
41
|
+
type: 'image',
|
|
42
|
+
src: 'https://secret.com/image.png',
|
|
43
|
+
props: {
|
|
44
|
+
src: 'https://secret.com/props-image.png',
|
|
45
|
+
width: 100,
|
|
46
|
+
height: 100,
|
|
47
|
+
},
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
redactRecordForErrorReporting(assetRecord)
|
|
51
|
+
|
|
52
|
+
expect(assetRecord.src).toBe('<redacted>')
|
|
53
|
+
expect(assetRecord.props.src).toBe('<redacted>')
|
|
54
|
+
expect(assetRecord.props.width).toBe(100) // Other props should remain unchanged
|
|
55
|
+
expect(assetRecord.props.height).toBe(100)
|
|
56
|
+
})
|
|
57
|
+
|
|
58
|
+
it('should redact only props.src if top-level src does not exist', () => {
|
|
59
|
+
const assetRecord = {
|
|
60
|
+
id: 'asset:test',
|
|
61
|
+
typeName: 'asset',
|
|
62
|
+
type: 'video',
|
|
63
|
+
props: {
|
|
64
|
+
src: 'https://secret.com/video.mp4',
|
|
65
|
+
width: 200,
|
|
66
|
+
height: 150,
|
|
67
|
+
},
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
redactRecordForErrorReporting(assetRecord)
|
|
71
|
+
|
|
72
|
+
expect(assetRecord.props.src).toBe('<redacted>')
|
|
73
|
+
expect(assetRecord.props.width).toBe(200)
|
|
74
|
+
expect(assetRecord.props.height).toBe(150)
|
|
75
|
+
})
|
|
76
|
+
|
|
77
|
+
it('should not modify non-asset records', () => {
|
|
78
|
+
const shapeRecord = {
|
|
79
|
+
id: 'shape:test',
|
|
80
|
+
typeName: 'shape',
|
|
81
|
+
type: 'geo',
|
|
82
|
+
x: 100,
|
|
83
|
+
y: 200,
|
|
84
|
+
props: {
|
|
85
|
+
color: 'red',
|
|
86
|
+
size: 'medium',
|
|
87
|
+
},
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const originalRecord = JSON.parse(JSON.stringify(shapeRecord))
|
|
91
|
+
redactRecordForErrorReporting(shapeRecord)
|
|
92
|
+
|
|
93
|
+
expect(shapeRecord).toEqual(originalRecord)
|
|
94
|
+
})
|
|
95
|
+
|
|
96
|
+
it('should handle asset records without src fields gracefully', () => {
|
|
97
|
+
const assetRecord = {
|
|
98
|
+
id: 'asset:test',
|
|
99
|
+
typeName: 'asset',
|
|
100
|
+
type: 'bookmark',
|
|
101
|
+
props: {
|
|
102
|
+
title: 'Test Bookmark',
|
|
103
|
+
description: 'A test bookmark',
|
|
104
|
+
},
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const originalRecord = JSON.parse(JSON.stringify(assetRecord))
|
|
108
|
+
redactRecordForErrorReporting(assetRecord)
|
|
109
|
+
|
|
110
|
+
expect(assetRecord).toEqual(originalRecord)
|
|
111
|
+
})
|
|
112
|
+
|
|
113
|
+
it('should handle asset records with only top-level src', () => {
|
|
114
|
+
const assetRecord = {
|
|
115
|
+
id: 'asset:test',
|
|
116
|
+
typeName: 'asset',
|
|
117
|
+
type: 'image',
|
|
118
|
+
src: 'https://secret.com/image.png',
|
|
119
|
+
props: {
|
|
120
|
+
width: 100,
|
|
121
|
+
height: 100,
|
|
122
|
+
},
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
redactRecordForErrorReporting(assetRecord)
|
|
126
|
+
|
|
127
|
+
expect(assetRecord.src).toBe('<redacted>')
|
|
128
|
+
expect(assetRecord.props.width).toBe(100)
|
|
129
|
+
expect(assetRecord.props.height).toBe(100)
|
|
130
|
+
})
|
|
131
|
+
})
|
|
132
|
+
})
|
|
133
|
+
|
|
134
|
+
describe('onValidationFailure', () => {
|
|
135
|
+
beforeEach(() => {
|
|
136
|
+
vi.clearAllMocks()
|
|
137
|
+
})
|
|
138
|
+
|
|
139
|
+
it('should annotate error with correct tags and extras', () => {
|
|
140
|
+
const mockError = new Error('Test validation error')
|
|
141
|
+
const record = {
|
|
142
|
+
id: 'shape:test',
|
|
143
|
+
typeName: 'shape',
|
|
144
|
+
type: 'geo',
|
|
145
|
+
x: 100,
|
|
146
|
+
y: 200,
|
|
147
|
+
} as any
|
|
148
|
+
|
|
149
|
+
const recordBefore = {
|
|
150
|
+
id: 'shape:test',
|
|
151
|
+
typeName: 'shape',
|
|
152
|
+
type: 'geo',
|
|
153
|
+
x: 50,
|
|
154
|
+
y: 150,
|
|
155
|
+
} as any
|
|
156
|
+
|
|
157
|
+
const validationFailure = {
|
|
158
|
+
error: mockError,
|
|
159
|
+
phase: 'updateRecord' as const,
|
|
160
|
+
record,
|
|
161
|
+
recordBefore,
|
|
162
|
+
store: {} as any, // Required by StoreValidationFailure interface
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
expect(() => onValidationFailure(validationFailure)).toThrow(mockError)
|
|
166
|
+
|
|
167
|
+
expect(annotateError).toHaveBeenCalledWith(
|
|
168
|
+
mockError,
|
|
169
|
+
expect.objectContaining({
|
|
170
|
+
tags: expect.objectContaining({
|
|
171
|
+
origin: 'store.validateRecord',
|
|
172
|
+
storePhase: 'updateRecord',
|
|
173
|
+
isExistingValidationIssue: false,
|
|
174
|
+
}),
|
|
175
|
+
})
|
|
176
|
+
)
|
|
177
|
+
})
|
|
178
|
+
|
|
179
|
+
it('should mark initialize phase as existing validation issue', () => {
|
|
180
|
+
const mockError = new Error('Initialize error')
|
|
181
|
+
const record = { id: 'test:1', typeName: 'test' } as any
|
|
182
|
+
|
|
183
|
+
const validationFailure = {
|
|
184
|
+
error: mockError,
|
|
185
|
+
phase: 'initialize' as const,
|
|
186
|
+
record,
|
|
187
|
+
recordBefore: null,
|
|
188
|
+
store: {} as any,
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
expect(() => onValidationFailure(validationFailure)).toThrow(mockError)
|
|
192
|
+
|
|
193
|
+
expect(annotateError).toHaveBeenCalledWith(
|
|
194
|
+
mockError,
|
|
195
|
+
expect.objectContaining({
|
|
196
|
+
tags: expect.objectContaining({
|
|
197
|
+
isExistingValidationIssue: true,
|
|
198
|
+
}),
|
|
199
|
+
})
|
|
200
|
+
)
|
|
201
|
+
})
|
|
202
|
+
|
|
203
|
+
it('should handle missing recordBefore', () => {
|
|
204
|
+
const mockError = new Error('No record before')
|
|
205
|
+
const record = { id: 'test:new', typeName: 'test' } as any
|
|
206
|
+
|
|
207
|
+
const validationFailure = {
|
|
208
|
+
error: mockError,
|
|
209
|
+
phase: 'createRecord' as const,
|
|
210
|
+
record,
|
|
211
|
+
recordBefore: null,
|
|
212
|
+
store: {} as any,
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
expect(() => onValidationFailure(validationFailure)).toThrow(mockError)
|
|
216
|
+
|
|
217
|
+
expect(annotateError).toHaveBeenCalledWith(
|
|
218
|
+
mockError,
|
|
219
|
+
expect.objectContaining({
|
|
220
|
+
tags: expect.objectContaining({
|
|
221
|
+
origin: 'store.validateRecord',
|
|
222
|
+
storePhase: 'createRecord',
|
|
223
|
+
isExistingValidationIssue: false,
|
|
224
|
+
}),
|
|
225
|
+
})
|
|
226
|
+
)
|
|
227
|
+
})
|
|
228
|
+
|
|
229
|
+
it('should redact asset records in error reporting', () => {
|
|
230
|
+
const mockError = new Error('Asset validation error')
|
|
231
|
+
const assetRecord = {
|
|
232
|
+
id: 'asset:image',
|
|
233
|
+
typeName: 'asset',
|
|
234
|
+
type: 'image',
|
|
235
|
+
src: 'https://secret.com/image.png',
|
|
236
|
+
props: {
|
|
237
|
+
src: 'https://secret.com/props-image.png',
|
|
238
|
+
width: 100,
|
|
239
|
+
height: 100,
|
|
240
|
+
},
|
|
241
|
+
} as any
|
|
242
|
+
|
|
243
|
+
const validationFailure = {
|
|
244
|
+
error: mockError,
|
|
245
|
+
phase: 'createRecord' as const,
|
|
246
|
+
record: assetRecord,
|
|
247
|
+
recordBefore: null,
|
|
248
|
+
store: {} as any,
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
expect(() => onValidationFailure(validationFailure)).toThrow(mockError)
|
|
252
|
+
|
|
253
|
+
// The function should call annotateError, and redaction happens internally
|
|
254
|
+
expect(annotateError).toHaveBeenCalledWith(
|
|
255
|
+
mockError,
|
|
256
|
+
expect.objectContaining({
|
|
257
|
+
tags: expect.objectContaining({
|
|
258
|
+
origin: 'store.validateRecord',
|
|
259
|
+
storePhase: 'createRecord',
|
|
260
|
+
isExistingValidationIssue: false,
|
|
261
|
+
}),
|
|
262
|
+
})
|
|
263
|
+
)
|
|
264
|
+
})
|
|
265
|
+
|
|
266
|
+
it('should handle different validation phases correctly', () => {
|
|
267
|
+
const phases = ['initialize', 'createRecord', 'updateRecord', 'tests'] as const
|
|
268
|
+
const mockError = new Error('Phase test')
|
|
269
|
+
const record = { id: 'test:phase', typeName: 'test' } as any
|
|
270
|
+
|
|
271
|
+
phases.forEach((phase) => {
|
|
272
|
+
const validationFailure = {
|
|
273
|
+
error: mockError,
|
|
274
|
+
phase,
|
|
275
|
+
record,
|
|
276
|
+
recordBefore: null,
|
|
277
|
+
store: {} as any,
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
expect(() => onValidationFailure(validationFailure)).toThrow(mockError)
|
|
281
|
+
|
|
282
|
+
expect(annotateError).toHaveBeenCalledWith(
|
|
283
|
+
mockError,
|
|
284
|
+
expect.objectContaining({
|
|
285
|
+
tags: expect.objectContaining({
|
|
286
|
+
storePhase: phase,
|
|
287
|
+
isExistingValidationIssue: phase === 'initialize',
|
|
288
|
+
}),
|
|
289
|
+
})
|
|
290
|
+
)
|
|
291
|
+
|
|
292
|
+
vi.clearAllMocks()
|
|
293
|
+
})
|
|
294
|
+
})
|
|
295
|
+
|
|
296
|
+
it('should use structuredClone for records', () => {
|
|
297
|
+
const mockError = new Error('Clone test')
|
|
298
|
+
const record = { id: 'test:clone', typeName: 'test', nested: { prop: 'value' } } as any
|
|
299
|
+
const recordBefore = { id: 'test:clone', typeName: 'test', nested: { prop: 'old' } } as any
|
|
300
|
+
|
|
301
|
+
const validationFailure = {
|
|
302
|
+
error: mockError,
|
|
303
|
+
phase: 'updateRecord' as const,
|
|
304
|
+
record,
|
|
305
|
+
recordBefore,
|
|
306
|
+
store: {} as any,
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
expect(() => onValidationFailure(validationFailure)).toThrow(mockError)
|
|
310
|
+
|
|
311
|
+
expect(structuredClone).toHaveBeenCalledWith(record)
|
|
312
|
+
expect(structuredClone).toHaveBeenCalledWith(recordBefore)
|
|
313
|
+
})
|
|
314
|
+
})
|
|
315
|
+
|
|
316
|
+
describe('createIntegrityChecker', () => {
|
|
317
|
+
let store: Store<TLRecord, TLStoreProps>
|
|
318
|
+
let mockAssetStore: Required<TLAssetStore>
|
|
319
|
+
|
|
320
|
+
beforeEach(() => {
|
|
321
|
+
mockAssetStore = {
|
|
322
|
+
upload: vi.fn().mockResolvedValue({ src: 'uploaded-url' }),
|
|
323
|
+
resolve: vi.fn().mockResolvedValue('resolved-url'),
|
|
324
|
+
remove: vi.fn().mockResolvedValue(undefined),
|
|
325
|
+
} as Required<TLAssetStore>
|
|
326
|
+
|
|
327
|
+
const schema = createTLSchema()
|
|
328
|
+
store = new Store({
|
|
329
|
+
schema,
|
|
330
|
+
props: {
|
|
331
|
+
defaultName: 'Test Store',
|
|
332
|
+
assets: mockAssetStore,
|
|
333
|
+
onMount: vi.fn(),
|
|
334
|
+
},
|
|
335
|
+
})
|
|
336
|
+
})
|
|
337
|
+
|
|
338
|
+
afterEach(() => {
|
|
339
|
+
vi.clearAllMocks()
|
|
340
|
+
})
|
|
341
|
+
|
|
342
|
+
describe('document and pointer records', () => {
|
|
343
|
+
it('should create missing document record', () => {
|
|
344
|
+
// Remove document record if it exists
|
|
345
|
+
if (store.has(TLDOCUMENT_ID)) {
|
|
346
|
+
store.remove([TLDOCUMENT_ID])
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
const checker = createIntegrityChecker(store)
|
|
350
|
+
checker()
|
|
351
|
+
|
|
352
|
+
expect(store.has(TLDOCUMENT_ID)).toBe(true)
|
|
353
|
+
const document = store.get(TLDOCUMENT_ID)
|
|
354
|
+
expect(document).toBeDefined()
|
|
355
|
+
expect(document!.name).toBe('Test Store')
|
|
356
|
+
})
|
|
357
|
+
|
|
358
|
+
it('should create missing pointer record', () => {
|
|
359
|
+
// Remove pointer record if it exists
|
|
360
|
+
if (store.has(TLPOINTER_ID)) {
|
|
361
|
+
store.remove([TLPOINTER_ID])
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
const checker = createIntegrityChecker(store)
|
|
365
|
+
checker()
|
|
366
|
+
|
|
367
|
+
expect(store.has(TLPOINTER_ID)).toBe(true)
|
|
368
|
+
const pointer = store.get(TLPOINTER_ID)
|
|
369
|
+
expect(pointer).toBeDefined()
|
|
370
|
+
expect(pointer!.typeName).toBe('pointer')
|
|
371
|
+
})
|
|
372
|
+
})
|
|
373
|
+
|
|
374
|
+
describe('page management', () => {
|
|
375
|
+
it('should create default page when none exist', () => {
|
|
376
|
+
// Clear all pages
|
|
377
|
+
const pageIds = store.query.ids('page').get()
|
|
378
|
+
store.remove([...pageIds])
|
|
379
|
+
|
|
380
|
+
const checker = createIntegrityChecker(store)
|
|
381
|
+
checker()
|
|
382
|
+
|
|
383
|
+
const newPageIds = store.query.ids('page').get()
|
|
384
|
+
expect(newPageIds.size).toBe(1)
|
|
385
|
+
|
|
386
|
+
const page = store.get([...newPageIds][0]) as any
|
|
387
|
+
expect(page).toBeDefined()
|
|
388
|
+
expect(page!.name).toBe('Page 1')
|
|
389
|
+
expect(page!.index).toBe('a1')
|
|
390
|
+
})
|
|
391
|
+
|
|
392
|
+
it('should preserve existing pages', () => {
|
|
393
|
+
// First ensure we have at least one page
|
|
394
|
+
const checker = createIntegrityChecker(store)
|
|
395
|
+
checker()
|
|
396
|
+
|
|
397
|
+
const existingPageIds = store.query.ids('page').get()
|
|
398
|
+
expect(existingPageIds.size).toBeGreaterThan(0) // Should have at least one page
|
|
399
|
+
|
|
400
|
+
// Run checker again - should not change pages
|
|
401
|
+
checker()
|
|
402
|
+
const newPageIds = store.query.ids('page').get()
|
|
403
|
+
expect(newPageIds.size).toBe(existingPageIds.size)
|
|
404
|
+
})
|
|
405
|
+
})
|
|
406
|
+
|
|
407
|
+
describe('instance state management', () => {
|
|
408
|
+
it('should create missing instance state', () => {
|
|
409
|
+
// Remove instance if it exists
|
|
410
|
+
if (store.has(TLINSTANCE_ID)) {
|
|
411
|
+
store.remove([TLINSTANCE_ID])
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
// Ensure we have at least one page
|
|
415
|
+
const pageIds = store.query.ids('page').get()
|
|
416
|
+
if (pageIds.size === 0) {
|
|
417
|
+
store.put([
|
|
418
|
+
PageRecordType.create({
|
|
419
|
+
id: 'page:test' as TLPageId,
|
|
420
|
+
name: 'Test Page',
|
|
421
|
+
index: 'a1' as IndexKey,
|
|
422
|
+
meta: {},
|
|
423
|
+
}),
|
|
424
|
+
])
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
const checker = createIntegrityChecker(store)
|
|
428
|
+
checker()
|
|
429
|
+
|
|
430
|
+
expect(store.has(TLINSTANCE_ID)).toBe(true)
|
|
431
|
+
const instance = store.get(TLINSTANCE_ID)
|
|
432
|
+
expect(instance).toBeDefined()
|
|
433
|
+
expect(instance!.currentPageId).toBeDefined()
|
|
434
|
+
expect(instance!.exportBackground).toBe(true)
|
|
435
|
+
})
|
|
436
|
+
|
|
437
|
+
it('should update instance to reference valid page when current page is invalid', () => {
|
|
438
|
+
// Create a valid page
|
|
439
|
+
const validPageId = 'page:valid' as TLPageId
|
|
440
|
+
store.put([
|
|
441
|
+
PageRecordType.create({
|
|
442
|
+
id: validPageId,
|
|
443
|
+
name: 'Valid Page',
|
|
444
|
+
index: 'a1' as IndexKey,
|
|
445
|
+
meta: {},
|
|
446
|
+
}),
|
|
447
|
+
])
|
|
448
|
+
|
|
449
|
+
// Create instance with invalid page reference
|
|
450
|
+
const invalidPageId = 'page:invalid' as TLPageId
|
|
451
|
+
store.put([
|
|
452
|
+
store.schema.types.instance.create({
|
|
453
|
+
id: TLINSTANCE_ID,
|
|
454
|
+
currentPageId: invalidPageId,
|
|
455
|
+
exportBackground: true,
|
|
456
|
+
}),
|
|
457
|
+
])
|
|
458
|
+
|
|
459
|
+
const checker = createIntegrityChecker(store)
|
|
460
|
+
checker()
|
|
461
|
+
|
|
462
|
+
const instance = store.get(TLINSTANCE_ID)
|
|
463
|
+
expect(instance!.currentPageId).toBe(validPageId)
|
|
464
|
+
})
|
|
465
|
+
})
|
|
466
|
+
|
|
467
|
+
describe('page state and camera management', () => {
|
|
468
|
+
it('should create missing page states for existing pages', () => {
|
|
469
|
+
// Create a page
|
|
470
|
+
const pageId = 'page:test' as TLPageId
|
|
471
|
+
store.put([
|
|
472
|
+
PageRecordType.create({
|
|
473
|
+
id: pageId,
|
|
474
|
+
name: 'Test Page',
|
|
475
|
+
index: 'a1' as IndexKey,
|
|
476
|
+
meta: {},
|
|
477
|
+
}),
|
|
478
|
+
])
|
|
479
|
+
|
|
480
|
+
// Remove any existing page state
|
|
481
|
+
const pageStateId = InstancePageStateRecordType.createId(pageId)
|
|
482
|
+
if (store.has(pageStateId)) {
|
|
483
|
+
store.remove([pageStateId])
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
const checker = createIntegrityChecker(store)
|
|
487
|
+
checker()
|
|
488
|
+
|
|
489
|
+
expect(store.has(pageStateId)).toBe(true)
|
|
490
|
+
const pageState = store.get(pageStateId)
|
|
491
|
+
expect(pageState).toBeDefined()
|
|
492
|
+
expect(pageState!.pageId).toBe(pageId)
|
|
493
|
+
})
|
|
494
|
+
|
|
495
|
+
it('should create missing cameras for existing pages', () => {
|
|
496
|
+
// Create a page
|
|
497
|
+
const pageId = 'page:test' as TLPageId
|
|
498
|
+
store.put([
|
|
499
|
+
PageRecordType.create({
|
|
500
|
+
id: pageId,
|
|
501
|
+
name: 'Test Page',
|
|
502
|
+
index: 'a1' as IndexKey,
|
|
503
|
+
meta: {},
|
|
504
|
+
}),
|
|
505
|
+
])
|
|
506
|
+
|
|
507
|
+
// Remove any existing camera
|
|
508
|
+
const cameraId = CameraRecordType.createId(pageId)
|
|
509
|
+
if (store.has(cameraId)) {
|
|
510
|
+
store.remove([cameraId])
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
const checker = createIntegrityChecker(store)
|
|
514
|
+
checker()
|
|
515
|
+
|
|
516
|
+
expect(store.has(cameraId)).toBe(true)
|
|
517
|
+
const camera = store.get(cameraId)
|
|
518
|
+
expect(camera).toBeDefined()
|
|
519
|
+
expect(camera!.id).toBe(cameraId)
|
|
520
|
+
})
|
|
521
|
+
})
|
|
522
|
+
|
|
523
|
+
describe('page state cleanup and validation', () => {
|
|
524
|
+
it('should remove page states for non-existent pages', () => {
|
|
525
|
+
// Create page state for non-existent page
|
|
526
|
+
const nonExistentPageId = 'page:nonexistent' as TLPageId
|
|
527
|
+
const orphanPageStateId = InstancePageStateRecordType.createId(nonExistentPageId)
|
|
528
|
+
|
|
529
|
+
store.put([
|
|
530
|
+
InstancePageStateRecordType.create({
|
|
531
|
+
id: orphanPageStateId,
|
|
532
|
+
pageId: nonExistentPageId,
|
|
533
|
+
}),
|
|
534
|
+
])
|
|
535
|
+
|
|
536
|
+
const checker = createIntegrityChecker(store)
|
|
537
|
+
checker()
|
|
538
|
+
|
|
539
|
+
expect(store.has(orphanPageStateId)).toBe(false)
|
|
540
|
+
})
|
|
541
|
+
|
|
542
|
+
it.each([
|
|
543
|
+
['croppingShapeId', 'shape:nonexistent' as TLShapeId, null],
|
|
544
|
+
['focusedGroupId', 'shape:nonexistent' as TLShapeId, null],
|
|
545
|
+
['hoveredShapeId', 'shape:nonexistent' as TLShapeId, null],
|
|
546
|
+
])('should clear invalid %s from page states', (fieldName, invalidValue, expectedValue) => {
|
|
547
|
+
const pageId = 'page:test' as TLPageId
|
|
548
|
+
const pageStateId = InstancePageStateRecordType.createId(pageId)
|
|
549
|
+
|
|
550
|
+
store.put([
|
|
551
|
+
PageRecordType.create({
|
|
552
|
+
id: pageId,
|
|
553
|
+
name: 'Test Page',
|
|
554
|
+
index: 'a1' as IndexKey,
|
|
555
|
+
meta: {},
|
|
556
|
+
}),
|
|
557
|
+
InstancePageStateRecordType.create({
|
|
558
|
+
id: pageStateId,
|
|
559
|
+
pageId: pageId,
|
|
560
|
+
[fieldName]: invalidValue,
|
|
561
|
+
}),
|
|
562
|
+
])
|
|
563
|
+
|
|
564
|
+
const checker = createIntegrityChecker(store)
|
|
565
|
+
checker()
|
|
566
|
+
|
|
567
|
+
const pageState = store.get(pageStateId)
|
|
568
|
+
expect(pageState![fieldName as keyof typeof pageState]).toBe(expectedValue)
|
|
569
|
+
})
|
|
570
|
+
|
|
571
|
+
it.each([
|
|
572
|
+
['selectedShapeIds', ['shape:nonexistent1', 'shape:nonexistent2'] as TLShapeId[]],
|
|
573
|
+
['hintingShapeIds', ['shape:hint1', 'shape:hint2'] as TLShapeId[]],
|
|
574
|
+
['erasingShapeIds', ['shape:erase1', 'shape:erase2'] as TLShapeId[]],
|
|
575
|
+
])('should filter invalid %s from page states', (fieldName, invalidShapeIds) => {
|
|
576
|
+
const pageId = 'page:test' as TLPageId
|
|
577
|
+
const pageStateId = InstancePageStateRecordType.createId(pageId)
|
|
578
|
+
|
|
579
|
+
store.put([
|
|
580
|
+
PageRecordType.create({
|
|
581
|
+
id: pageId,
|
|
582
|
+
name: 'Test Page',
|
|
583
|
+
index: 'a1' as IndexKey,
|
|
584
|
+
meta: {},
|
|
585
|
+
}),
|
|
586
|
+
InstancePageStateRecordType.create({
|
|
587
|
+
id: pageStateId,
|
|
588
|
+
pageId: pageId,
|
|
589
|
+
[fieldName]: invalidShapeIds,
|
|
590
|
+
}),
|
|
591
|
+
])
|
|
592
|
+
|
|
593
|
+
const checker = createIntegrityChecker(store)
|
|
594
|
+
checker()
|
|
595
|
+
|
|
596
|
+
const pageState = store.get(pageStateId)
|
|
597
|
+
expect(pageState![fieldName as keyof typeof pageState]).toEqual([])
|
|
598
|
+
})
|
|
599
|
+
})
|
|
600
|
+
|
|
601
|
+
describe('recursive integrity checking', () => {
|
|
602
|
+
it('should recursively call itself when making corrections', () => {
|
|
603
|
+
// Start with empty store - this will trigger multiple corrections
|
|
604
|
+
store.clear()
|
|
605
|
+
|
|
606
|
+
const checker = createIntegrityChecker(store)
|
|
607
|
+
|
|
608
|
+
// This should not throw or hang - it should complete successfully
|
|
609
|
+
expect(() => checker()).not.toThrow()
|
|
610
|
+
|
|
611
|
+
// Verify final state is valid
|
|
612
|
+
expect(store.has(TLDOCUMENT_ID)).toBe(true)
|
|
613
|
+
expect(store.has(TLPOINTER_ID)).toBe(true)
|
|
614
|
+
expect(store.query.ids('page').get().size).toBe(1)
|
|
615
|
+
expect(store.has(TLINSTANCE_ID)).toBe(true)
|
|
616
|
+
})
|
|
617
|
+
|
|
618
|
+
it('should handle complex integrity violations in sequence', () => {
|
|
619
|
+
// Create a scenario with multiple integrity issues
|
|
620
|
+
store.clear()
|
|
621
|
+
|
|
622
|
+
// Add a page without required instance/document/pointer records
|
|
623
|
+
const pageId = 'page:orphan' as TLPageId
|
|
624
|
+
store.put([
|
|
625
|
+
PageRecordType.create({
|
|
626
|
+
id: pageId,
|
|
627
|
+
name: 'Orphan Page',
|
|
628
|
+
index: 'a1' as IndexKey,
|
|
629
|
+
meta: {},
|
|
630
|
+
}),
|
|
631
|
+
])
|
|
632
|
+
|
|
633
|
+
const checker = createIntegrityChecker(store)
|
|
634
|
+
checker()
|
|
635
|
+
|
|
636
|
+
// All required records should now exist
|
|
637
|
+
expect(store.has(TLDOCUMENT_ID)).toBe(true)
|
|
638
|
+
expect(store.has(TLPOINTER_ID)).toBe(true)
|
|
639
|
+
expect(store.has(TLINSTANCE_ID)).toBe(true)
|
|
640
|
+
expect(store.has(InstancePageStateRecordType.createId(pageId))).toBe(true)
|
|
641
|
+
expect(store.has(CameraRecordType.createId(pageId))).toBe(true)
|
|
642
|
+
})
|
|
643
|
+
})
|
|
644
|
+
})
|