@tldraw/editor 3.14.0-canary.d8a1c8c23469 → 3.14.0-canary.db789786fb06
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/index.d.ts +220 -117
- package/dist-cjs/index.js +11 -8
- package/dist-cjs/index.js.map +2 -2
- package/dist-cjs/lib/config/TLSessionStateSnapshot.js +1 -12
- package/dist-cjs/lib/config/TLSessionStateSnapshot.js.map +3 -3
- package/dist-cjs/lib/editor/Editor.js +132 -101
- package/dist-cjs/lib/editor/Editor.js.map +2 -2
- package/dist-cjs/lib/editor/bindings/BindingUtil.js.map +2 -2
- package/dist-cjs/lib/editor/derivations/bindingsIndex.js +22 -22
- package/dist-cjs/lib/editor/derivations/bindingsIndex.js.map +2 -2
- package/dist-cjs/lib/editor/derivations/notVisibleShapes.js +16 -20
- package/dist-cjs/lib/editor/derivations/notVisibleShapes.js.map +3 -3
- package/dist-cjs/lib/editor/derivations/parentsToChildren.js +16 -16
- package/dist-cjs/lib/editor/derivations/parentsToChildren.js.map +2 -2
- package/dist-cjs/lib/editor/managers/{ClickManager.js → ClickManager/ClickManager.js} +1 -1
- package/dist-cjs/lib/editor/managers/ClickManager/ClickManager.js.map +7 -0
- package/dist-cjs/lib/editor/managers/{EdgeScrollManager.js → EdgeScrollManager/EdgeScrollManager.js} +2 -2
- package/dist-cjs/lib/editor/managers/EdgeScrollManager/EdgeScrollManager.js.map +7 -0
- package/dist-cjs/lib/editor/managers/FocusManager/FocusManager.js.map +7 -0
- package/dist-cjs/lib/editor/managers/{FontManager.js → FontManager/FontManager.js} +4 -1
- package/dist-cjs/lib/editor/managers/FontManager/FontManager.js.map +7 -0
- package/dist-cjs/lib/editor/managers/{HistoryManager.js → HistoryManager/HistoryManager.js} +67 -7
- package/dist-cjs/lib/editor/managers/HistoryManager/HistoryManager.js.map +7 -0
- package/dist-cjs/lib/editor/managers/{ScribbleManager.js → ScribbleManager/ScribbleManager.js} +1 -1
- package/dist-cjs/lib/editor/managers/ScribbleManager/ScribbleManager.js.map +7 -0
- package/dist-cjs/lib/editor/managers/{TextManager.js → TextManager/TextManager.js} +73 -42
- package/dist-cjs/lib/editor/managers/TextManager/TextManager.js.map +7 -0
- package/dist-cjs/lib/editor/managers/{TickManager.js → TickManager/TickManager.js} +1 -1
- package/dist-cjs/lib/editor/managers/TickManager/TickManager.js.map +7 -0
- package/dist-cjs/lib/editor/managers/{UserPreferencesManager.js → UserPreferencesManager/UserPreferencesManager.js} +1 -1
- package/dist-cjs/lib/editor/managers/UserPreferencesManager/UserPreferencesManager.js.map +7 -0
- package/dist-cjs/lib/editor/shapes/ShapeUtil.js +8 -10
- package/dist-cjs/lib/editor/shapes/ShapeUtil.js.map +2 -2
- package/dist-cjs/lib/editor/shapes/group/GroupShapeUtil.js +6 -0
- package/dist-cjs/lib/editor/shapes/group/GroupShapeUtil.js.map +2 -2
- package/dist-cjs/lib/editor/tools/BaseBoxShapeTool/children/Pointing.js +10 -6
- package/dist-cjs/lib/editor/tools/BaseBoxShapeTool/children/Pointing.js.map +3 -3
- package/dist-cjs/lib/editor/tools/StateNode.js +3 -3
- package/dist-cjs/lib/editor/tools/StateNode.js.map +2 -2
- package/dist-cjs/lib/editor/types/emit-types.js.map +1 -1
- package/dist-cjs/lib/editor/types/external-content.js.map +1 -1
- package/dist-cjs/lib/exports/getSvgJsx.js.map +1 -1
- package/dist-cjs/lib/hooks/useCanvasEvents.js +1 -2
- package/dist-cjs/lib/hooks/useCanvasEvents.js.map +2 -2
- package/dist-cjs/lib/primitives/Box.js +33 -33
- package/dist-cjs/lib/primitives/Box.js.map +2 -2
- package/dist-cjs/lib/primitives/Vec.js +13 -8
- package/dist-cjs/lib/primitives/Vec.js.map +2 -2
- package/dist-cjs/lib/primitives/geometry/Arc2d.js +41 -21
- package/dist-cjs/lib/primitives/geometry/Arc2d.js.map +2 -2
- package/dist-cjs/lib/primitives/geometry/Circle2d.js +11 -11
- package/dist-cjs/lib/primitives/geometry/Circle2d.js.map +2 -2
- package/dist-cjs/lib/primitives/geometry/CubicBezier2d.js +13 -16
- package/dist-cjs/lib/primitives/geometry/CubicBezier2d.js.map +2 -2
- package/dist-cjs/lib/primitives/geometry/CubicSpline2d.js +4 -4
- package/dist-cjs/lib/primitives/geometry/CubicSpline2d.js.map +2 -2
- package/dist-cjs/lib/primitives/geometry/Edge2d.js +14 -17
- package/dist-cjs/lib/primitives/geometry/Edge2d.js.map +2 -2
- package/dist-cjs/lib/primitives/geometry/Ellipse2d.js +10 -10
- package/dist-cjs/lib/primitives/geometry/Ellipse2d.js.map +2 -2
- package/dist-cjs/lib/primitives/geometry/Geometry2d.js +6 -2
- package/dist-cjs/lib/primitives/geometry/Geometry2d.js.map +2 -2
- package/dist-cjs/lib/primitives/geometry/Point2d.js +6 -6
- package/dist-cjs/lib/primitives/geometry/Point2d.js.map +2 -2
- package/dist-cjs/lib/primitives/geometry/Polygon2d.js +3 -0
- package/dist-cjs/lib/primitives/geometry/Polygon2d.js.map +2 -2
- package/dist-cjs/lib/primitives/geometry/Polyline2d.js +8 -5
- package/dist-cjs/lib/primitives/geometry/Polyline2d.js.map +2 -2
- package/dist-cjs/lib/primitives/geometry/Rectangle2d.js +22 -11
- package/dist-cjs/lib/primitives/geometry/Rectangle2d.js.map +2 -2
- package/dist-cjs/lib/primitives/geometry/Stadium2d.js +22 -22
- package/dist-cjs/lib/primitives/geometry/Stadium2d.js.map +2 -2
- package/dist-cjs/lib/utils/reorderShapes.js +11 -10
- package/dist-cjs/lib/utils/reorderShapes.js.map +2 -2
- package/dist-cjs/lib/utils/reparenting.js +232 -0
- package/dist-cjs/lib/utils/reparenting.js.map +7 -0
- package/dist-cjs/lib/utils/richText.js +7 -2
- package/dist-cjs/lib/utils/richText.js.map +2 -2
- package/dist-cjs/version.js +3 -3
- package/dist-cjs/version.js.map +1 -1
- package/dist-esm/index.d.mts +220 -117
- package/dist-esm/index.mjs +15 -8
- package/dist-esm/index.mjs.map +2 -2
- package/dist-esm/lib/config/TLSessionStateSnapshot.mjs +1 -1
- package/dist-esm/lib/config/TLSessionStateSnapshot.mjs.map +2 -2
- package/dist-esm/lib/editor/Editor.mjs +132 -101
- package/dist-esm/lib/editor/Editor.mjs.map +2 -2
- package/dist-esm/lib/editor/bindings/BindingUtil.mjs.map +2 -2
- package/dist-esm/lib/editor/derivations/bindingsIndex.mjs +22 -22
- package/dist-esm/lib/editor/derivations/bindingsIndex.mjs.map +2 -2
- package/dist-esm/lib/editor/derivations/notVisibleShapes.mjs +16 -20
- package/dist-esm/lib/editor/derivations/notVisibleShapes.mjs.map +3 -3
- package/dist-esm/lib/editor/derivations/parentsToChildren.mjs +16 -16
- package/dist-esm/lib/editor/derivations/parentsToChildren.mjs.map +2 -2
- package/dist-esm/lib/editor/managers/{ClickManager.mjs → ClickManager/ClickManager.mjs} +1 -1
- package/dist-esm/lib/editor/managers/ClickManager/ClickManager.mjs.map +7 -0
- package/dist-esm/lib/editor/managers/{EdgeScrollManager.mjs → EdgeScrollManager/EdgeScrollManager.mjs} +2 -2
- package/dist-esm/lib/editor/managers/EdgeScrollManager/EdgeScrollManager.mjs.map +7 -0
- package/dist-esm/lib/editor/managers/FocusManager/FocusManager.mjs.map +7 -0
- package/dist-esm/lib/editor/managers/{FontManager.mjs → FontManager/FontManager.mjs} +4 -1
- package/dist-esm/lib/editor/managers/FontManager/FontManager.mjs.map +7 -0
- package/dist-esm/lib/editor/managers/{HistoryManager.mjs → HistoryManager/HistoryManager.mjs} +63 -3
- package/dist-esm/lib/editor/managers/HistoryManager/HistoryManager.mjs.map +7 -0
- package/dist-esm/lib/editor/managers/{ScribbleManager.mjs → ScribbleManager/ScribbleManager.mjs} +1 -1
- package/dist-esm/lib/editor/managers/ScribbleManager/ScribbleManager.mjs.map +7 -0
- package/dist-esm/lib/editor/managers/{TextManager.mjs → TextManager/TextManager.mjs} +73 -42
- package/dist-esm/lib/editor/managers/TextManager/TextManager.mjs.map +7 -0
- package/dist-esm/lib/editor/managers/{TickManager.mjs → TickManager/TickManager.mjs} +1 -1
- package/dist-esm/lib/editor/managers/TickManager/TickManager.mjs.map +7 -0
- package/dist-esm/lib/editor/managers/{UserPreferencesManager.mjs → UserPreferencesManager/UserPreferencesManager.mjs} +1 -1
- package/dist-esm/lib/editor/managers/UserPreferencesManager/UserPreferencesManager.mjs.map +7 -0
- package/dist-esm/lib/editor/shapes/ShapeUtil.mjs +8 -10
- package/dist-esm/lib/editor/shapes/ShapeUtil.mjs.map +2 -2
- package/dist-esm/lib/editor/shapes/group/GroupShapeUtil.mjs +6 -0
- package/dist-esm/lib/editor/shapes/group/GroupShapeUtil.mjs.map +2 -2
- package/dist-esm/lib/editor/tools/BaseBoxShapeTool/children/Pointing.mjs +10 -6
- package/dist-esm/lib/editor/tools/BaseBoxShapeTool/children/Pointing.mjs.map +3 -3
- package/dist-esm/lib/editor/tools/StateNode.mjs +3 -3
- package/dist-esm/lib/editor/tools/StateNode.mjs.map +2 -2
- package/dist-esm/lib/exports/getSvgJsx.mjs.map +1 -1
- package/dist-esm/lib/hooks/useCanvasEvents.mjs +1 -2
- package/dist-esm/lib/hooks/useCanvasEvents.mjs.map +2 -2
- package/dist-esm/lib/primitives/Box.mjs +33 -33
- package/dist-esm/lib/primitives/Box.mjs.map +2 -2
- package/dist-esm/lib/primitives/Vec.mjs +13 -8
- package/dist-esm/lib/primitives/Vec.mjs.map +2 -2
- package/dist-esm/lib/primitives/geometry/Arc2d.mjs +41 -21
- package/dist-esm/lib/primitives/geometry/Arc2d.mjs.map +2 -2
- package/dist-esm/lib/primitives/geometry/Circle2d.mjs +11 -11
- package/dist-esm/lib/primitives/geometry/Circle2d.mjs.map +2 -2
- package/dist-esm/lib/primitives/geometry/CubicBezier2d.mjs +13 -16
- package/dist-esm/lib/primitives/geometry/CubicBezier2d.mjs.map +2 -2
- package/dist-esm/lib/primitives/geometry/CubicSpline2d.mjs +4 -4
- package/dist-esm/lib/primitives/geometry/CubicSpline2d.mjs.map +2 -2
- package/dist-esm/lib/primitives/geometry/Edge2d.mjs +14 -17
- package/dist-esm/lib/primitives/geometry/Edge2d.mjs.map +2 -2
- package/dist-esm/lib/primitives/geometry/Ellipse2d.mjs +11 -11
- package/dist-esm/lib/primitives/geometry/Ellipse2d.mjs.map +2 -2
- package/dist-esm/lib/primitives/geometry/Geometry2d.mjs +6 -2
- package/dist-esm/lib/primitives/geometry/Geometry2d.mjs.map +2 -2
- package/dist-esm/lib/primitives/geometry/Point2d.mjs +6 -6
- package/dist-esm/lib/primitives/geometry/Point2d.mjs.map +2 -2
- package/dist-esm/lib/primitives/geometry/Polygon2d.mjs +3 -0
- package/dist-esm/lib/primitives/geometry/Polygon2d.mjs.map +2 -2
- package/dist-esm/lib/primitives/geometry/Polyline2d.mjs +8 -5
- package/dist-esm/lib/primitives/geometry/Polyline2d.mjs.map +2 -2
- package/dist-esm/lib/primitives/geometry/Rectangle2d.mjs +22 -11
- package/dist-esm/lib/primitives/geometry/Rectangle2d.mjs.map +2 -2
- package/dist-esm/lib/primitives/geometry/Stadium2d.mjs +22 -22
- package/dist-esm/lib/primitives/geometry/Stadium2d.mjs.map +2 -2
- package/dist-esm/lib/utils/reorderShapes.mjs +11 -10
- package/dist-esm/lib/utils/reorderShapes.mjs.map +2 -2
- package/dist-esm/lib/utils/reparenting.mjs +216 -0
- package/dist-esm/lib/utils/reparenting.mjs.map +7 -0
- package/dist-esm/lib/utils/richText.mjs +8 -3
- package/dist-esm/lib/utils/richText.mjs.map +2 -2
- package/dist-esm/version.mjs +3 -3
- package/dist-esm/version.mjs.map +1 -1
- package/editor.css +442 -492
- package/package.json +8 -9
- package/src/index.ts +20 -7
- package/src/lib/config/TLSessionStateSnapshot.ts +1 -1
- package/src/lib/editor/Editor.test.ts +252 -3
- package/src/lib/editor/Editor.ts +150 -109
- package/src/lib/editor/bindings/BindingUtil.ts +6 -0
- package/src/lib/editor/derivations/bindingsIndex.ts +27 -26
- package/src/lib/editor/derivations/notVisibleShapes.ts +24 -25
- package/src/lib/editor/derivations/parentsToChildren.ts +28 -25
- package/src/lib/editor/managers/ClickManager/ClickManager.test.ts +442 -0
- package/src/lib/editor/managers/{ClickManager.ts → ClickManager/ClickManager.ts} +3 -3
- package/src/lib/editor/managers/EdgeScrollManager/EdgeScrollManager.test.ts +374 -0
- package/src/lib/editor/managers/{EdgeScrollManager.ts → EdgeScrollManager/EdgeScrollManager.ts} +3 -3
- package/src/lib/editor/managers/FocusManager/FocusManager.test.ts +455 -0
- package/src/lib/editor/managers/{FocusManager.ts → FocusManager/FocusManager.ts} +1 -1
- package/src/lib/editor/managers/FontManager/FontManager.test.ts +263 -0
- package/src/lib/editor/managers/{FontManager.ts → FontManager/FontManager.ts} +5 -2
- package/src/lib/editor/managers/{HistoryManager.test.ts → HistoryManager/HistoryManager.test.ts} +388 -1
- package/src/lib/editor/managers/{HistoryManager.ts → HistoryManager/HistoryManager.ts} +76 -3
- package/src/lib/editor/managers/ScribbleManager/ScribbleManager.test.ts +624 -0
- package/src/lib/editor/managers/{ScribbleManager.ts → ScribbleManager/ScribbleManager.ts} +2 -2
- package/src/lib/editor/managers/SnapManager/SnapManager.test.ts +485 -0
- package/src/lib/editor/managers/TextManager/TextManager.test.ts +407 -0
- package/src/lib/editor/managers/{TextManager.ts → TextManager/TextManager.ts} +119 -87
- package/src/lib/editor/managers/TickManager/TickManager.test.ts +314 -0
- package/src/lib/editor/managers/{TickManager.ts → TickManager/TickManager.ts} +2 -2
- package/src/lib/editor/managers/UserPreferencesManager/UserPreferencesManager.test.ts +591 -0
- package/src/lib/editor/managers/{UserPreferencesManager.ts → UserPreferencesManager/UserPreferencesManager.ts} +2 -2
- package/src/lib/editor/shapes/ShapeUtil.ts +57 -16
- package/src/lib/editor/shapes/group/GroupShapeUtil.tsx +8 -0
- package/src/lib/editor/tools/BaseBoxShapeTool/children/Pointing.ts +22 -17
- package/src/lib/editor/tools/StateNode.ts +3 -3
- package/src/lib/editor/types/emit-types.ts +4 -0
- package/src/lib/editor/types/external-content.ts +11 -2
- package/src/lib/exports/getSvgJsx.tsx +1 -1
- package/src/lib/hooks/useCanvasEvents.ts +0 -1
- package/src/lib/primitives/Box.test.ts +588 -7
- package/src/lib/primitives/Box.ts +33 -33
- package/src/lib/primitives/Vec.test.ts +2 -2
- package/src/lib/primitives/Vec.ts +13 -8
- package/src/lib/primitives/geometry/Arc2d.ts +42 -23
- package/src/lib/primitives/geometry/Circle2d.ts +12 -12
- package/src/lib/primitives/geometry/CubicBezier2d.test.ts +5 -0
- package/src/lib/primitives/geometry/CubicBezier2d.ts +13 -17
- package/src/lib/primitives/geometry/CubicSpline2d.ts +5 -5
- package/src/lib/primitives/geometry/Edge2d.ts +14 -18
- package/src/lib/primitives/geometry/Ellipse2d.ts +12 -13
- package/src/lib/primitives/geometry/Geometry2d.ts +7 -2
- package/src/lib/primitives/geometry/Point2d.ts +6 -6
- package/src/lib/primitives/geometry/Polygon2d.ts +4 -0
- package/src/lib/primitives/geometry/Polyline2d.ts +10 -7
- package/src/lib/primitives/geometry/Rectangle2d.ts +24 -11
- package/src/lib/primitives/geometry/Stadium2d.ts +22 -23
- package/src/lib/utils/reorderShapes.ts +10 -13
- package/src/lib/utils/reparenting.ts +383 -0
- package/src/lib/utils/richText.ts +10 -4
- package/src/version.ts +3 -3
- package/dist-cjs/lib/editor/managers/ClickManager.js.map +0 -7
- package/dist-cjs/lib/editor/managers/EdgeScrollManager.js.map +0 -7
- package/dist-cjs/lib/editor/managers/FocusManager.js.map +0 -7
- package/dist-cjs/lib/editor/managers/FontManager.js.map +0 -7
- package/dist-cjs/lib/editor/managers/HistoryManager.js.map +0 -7
- package/dist-cjs/lib/editor/managers/ScribbleManager.js.map +0 -7
- package/dist-cjs/lib/editor/managers/Stack.js +0 -82
- package/dist-cjs/lib/editor/managers/Stack.js.map +0 -7
- package/dist-cjs/lib/editor/managers/TextManager.js.map +0 -7
- package/dist-cjs/lib/editor/managers/TickManager.js.map +0 -7
- package/dist-cjs/lib/editor/managers/UserPreferencesManager.js.map +0 -7
- package/dist-esm/lib/editor/managers/ClickManager.mjs.map +0 -7
- package/dist-esm/lib/editor/managers/EdgeScrollManager.mjs.map +0 -7
- package/dist-esm/lib/editor/managers/FocusManager.mjs.map +0 -7
- package/dist-esm/lib/editor/managers/FontManager.mjs.map +0 -7
- package/dist-esm/lib/editor/managers/HistoryManager.mjs.map +0 -7
- package/dist-esm/lib/editor/managers/ScribbleManager.mjs.map +0 -7
- package/dist-esm/lib/editor/managers/Stack.mjs +0 -62
- package/dist-esm/lib/editor/managers/Stack.mjs.map +0 -7
- package/dist-esm/lib/editor/managers/TextManager.mjs.map +0 -7
- package/dist-esm/lib/editor/managers/TickManager.mjs.map +0 -7
- package/dist-esm/lib/editor/managers/UserPreferencesManager.mjs.map +0 -7
- package/src/lib/editor/managers/ScribbleManager.test.ts +0 -32
- package/src/lib/editor/managers/Stack.ts +0 -71
- /package/dist-cjs/lib/editor/managers/{FocusManager.js → FocusManager/FocusManager.js} +0 -0
- /package/dist-esm/lib/editor/managers/{FocusManager.mjs → FocusManager/FocusManager.mjs} +0 -0
|
@@ -0,0 +1,263 @@
|
|
|
1
|
+
import { TLShape, TLShapeId, createShapeId } from '@tldraw/tlschema'
|
|
2
|
+
import { Editor } from '../../Editor'
|
|
3
|
+
import { FontManager, TLFontFace } from './FontManager'
|
|
4
|
+
|
|
5
|
+
// Mock the Editor class
|
|
6
|
+
jest.mock('../../Editor')
|
|
7
|
+
|
|
8
|
+
// Mock globals
|
|
9
|
+
global.FontFace = jest.fn().mockImplementation((family, src, descriptors) => ({
|
|
10
|
+
family,
|
|
11
|
+
src,
|
|
12
|
+
...descriptors,
|
|
13
|
+
load: jest.fn(() => Promise.resolve()),
|
|
14
|
+
}))
|
|
15
|
+
|
|
16
|
+
Object.defineProperty(global.document, 'fonts', {
|
|
17
|
+
value: {
|
|
18
|
+
add: jest.fn(),
|
|
19
|
+
[Symbol.iterator]: jest.fn(() => [].values()),
|
|
20
|
+
},
|
|
21
|
+
configurable: true,
|
|
22
|
+
})
|
|
23
|
+
|
|
24
|
+
global.queueMicrotask = jest.fn((fn) => Promise.resolve().then(fn))
|
|
25
|
+
|
|
26
|
+
describe('FontManager', () => {
|
|
27
|
+
let editor: jest.Mocked<Editor>
|
|
28
|
+
let fontManager: FontManager
|
|
29
|
+
let mockAssetUrls: { [key: string]: string }
|
|
30
|
+
|
|
31
|
+
const createMockFont = (overrides: Partial<TLFontFace> = {}): TLFontFace => ({
|
|
32
|
+
family: 'Test Font',
|
|
33
|
+
src: { url: 'test-font.woff2' },
|
|
34
|
+
...overrides,
|
|
35
|
+
})
|
|
36
|
+
|
|
37
|
+
const createMockShape = (id: TLShapeId = createShapeId('test')): TLShape => ({
|
|
38
|
+
id,
|
|
39
|
+
type: 'text',
|
|
40
|
+
x: 0,
|
|
41
|
+
y: 0,
|
|
42
|
+
rotation: 0,
|
|
43
|
+
index: 'a1' as any,
|
|
44
|
+
parentId: 'page:page' as any,
|
|
45
|
+
opacity: 1,
|
|
46
|
+
isLocked: false,
|
|
47
|
+
meta: {},
|
|
48
|
+
props: {},
|
|
49
|
+
typeName: 'shape' as const,
|
|
50
|
+
})
|
|
51
|
+
|
|
52
|
+
beforeEach(() => {
|
|
53
|
+
jest.clearAllMocks()
|
|
54
|
+
|
|
55
|
+
mockAssetUrls = {
|
|
56
|
+
'test-font.woff2': 'https://example.com/fonts/test-font.woff2',
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const mockShapeUtil = {
|
|
60
|
+
getFontFaces: jest.fn(() => []),
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const mockStore = {
|
|
64
|
+
createComputedCache: jest.fn(() => ({
|
|
65
|
+
get: jest.fn(() => []),
|
|
66
|
+
})),
|
|
67
|
+
createCache: jest.fn(() => ({
|
|
68
|
+
get: jest.fn(() => ({ get: jest.fn(() => []) })),
|
|
69
|
+
})),
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
editor = {
|
|
73
|
+
store: mockStore,
|
|
74
|
+
getShapeUtil: jest.fn(() => mockShapeUtil),
|
|
75
|
+
getCurrentPageShapeIds: jest.fn(() => new Set()),
|
|
76
|
+
getShape: jest.fn(),
|
|
77
|
+
isDisposed: false,
|
|
78
|
+
} as any
|
|
79
|
+
|
|
80
|
+
fontManager = new FontManager(editor, mockAssetUrls)
|
|
81
|
+
})
|
|
82
|
+
|
|
83
|
+
describe('constructor', () => {
|
|
84
|
+
it('should initialize with editor reference', () => {
|
|
85
|
+
expect(fontManager).toBeDefined()
|
|
86
|
+
})
|
|
87
|
+
|
|
88
|
+
it('should initialize without assetUrls', () => {
|
|
89
|
+
const managerWithoutUrls = new FontManager(editor)
|
|
90
|
+
expect(managerWithoutUrls).toBeDefined()
|
|
91
|
+
})
|
|
92
|
+
})
|
|
93
|
+
|
|
94
|
+
describe('getShapeFontFaces', () => {
|
|
95
|
+
it('should return empty array when no fonts found', () => {
|
|
96
|
+
const shape = createMockShape()
|
|
97
|
+
const result = fontManager.getShapeFontFaces(shape)
|
|
98
|
+
expect(result).toEqual([])
|
|
99
|
+
})
|
|
100
|
+
|
|
101
|
+
it('should accept shape ID as parameter', () => {
|
|
102
|
+
const shapeId = createShapeId('test')
|
|
103
|
+
const result = fontManager.getShapeFontFaces(shapeId)
|
|
104
|
+
expect(result).toEqual([])
|
|
105
|
+
})
|
|
106
|
+
})
|
|
107
|
+
|
|
108
|
+
describe('trackFontsForShape', () => {
|
|
109
|
+
it('should track fonts for shape without throwing', () => {
|
|
110
|
+
const shape = createMockShape()
|
|
111
|
+
expect(() => fontManager.trackFontsForShape(shape)).not.toThrow()
|
|
112
|
+
})
|
|
113
|
+
|
|
114
|
+
it('should track fonts for shape ID without throwing', () => {
|
|
115
|
+
const shapeId = createShapeId('test')
|
|
116
|
+
expect(() => fontManager.trackFontsForShape(shapeId)).not.toThrow()
|
|
117
|
+
})
|
|
118
|
+
})
|
|
119
|
+
|
|
120
|
+
describe('loadRequiredFontsForCurrentPage', () => {
|
|
121
|
+
it('should complete without error when no fonts needed', async () => {
|
|
122
|
+
await expect(fontManager.loadRequiredFontsForCurrentPage()).resolves.toBeUndefined()
|
|
123
|
+
})
|
|
124
|
+
|
|
125
|
+
it('should respect font limit', async () => {
|
|
126
|
+
const shapeIds = Array.from({ length: 5 }, (_, i) => createShapeId(`test${i}`))
|
|
127
|
+
const shapes = shapeIds.map(createMockShape)
|
|
128
|
+
|
|
129
|
+
editor.getCurrentPageShapeIds.mockReturnValue(new Set(shapeIds))
|
|
130
|
+
editor.getShape.mockImplementation((id) => shapes.find((s) => s.id === id))
|
|
131
|
+
|
|
132
|
+
await expect(fontManager.loadRequiredFontsForCurrentPage(3)).resolves.toBeUndefined()
|
|
133
|
+
})
|
|
134
|
+
})
|
|
135
|
+
|
|
136
|
+
describe('ensureFontIsLoaded', () => {
|
|
137
|
+
it('should create and load font face', async () => {
|
|
138
|
+
const font = createMockFont()
|
|
139
|
+
|
|
140
|
+
await fontManager.ensureFontIsLoaded(font)
|
|
141
|
+
|
|
142
|
+
expect(global.FontFace).toHaveBeenCalledWith(
|
|
143
|
+
font.family,
|
|
144
|
+
expect.stringContaining('url('),
|
|
145
|
+
expect.any(Object)
|
|
146
|
+
)
|
|
147
|
+
})
|
|
148
|
+
|
|
149
|
+
it('should handle font loading errors gracefully', async () => {
|
|
150
|
+
const font = createMockFont()
|
|
151
|
+
const error = new Error('Font load failed')
|
|
152
|
+
|
|
153
|
+
;(global.FontFace as jest.Mock).mockReturnValue({
|
|
154
|
+
family: font.family,
|
|
155
|
+
load: jest.fn(() => Promise.reject(error)),
|
|
156
|
+
})
|
|
157
|
+
|
|
158
|
+
const consoleSpy = jest.spyOn(console, 'error').mockImplementation()
|
|
159
|
+
|
|
160
|
+
await fontManager.ensureFontIsLoaded(font)
|
|
161
|
+
|
|
162
|
+
expect(consoleSpy).toHaveBeenCalledWith(error)
|
|
163
|
+
consoleSpy.mockRestore()
|
|
164
|
+
})
|
|
165
|
+
|
|
166
|
+
it('should return same promise for concurrent requests', async () => {
|
|
167
|
+
const font = createMockFont()
|
|
168
|
+
|
|
169
|
+
const promise1 = fontManager.ensureFontIsLoaded(font)
|
|
170
|
+
const promise2 = fontManager.ensureFontIsLoaded(font)
|
|
171
|
+
|
|
172
|
+
expect(promise1).toBe(promise2)
|
|
173
|
+
await Promise.all([promise1, promise2])
|
|
174
|
+
})
|
|
175
|
+
})
|
|
176
|
+
|
|
177
|
+
describe('requestFonts', () => {
|
|
178
|
+
it('should queue fonts for loading', () => {
|
|
179
|
+
const fonts = [createMockFont({ family: 'Font1' }), createMockFont({ family: 'Font2' })]
|
|
180
|
+
|
|
181
|
+
fontManager.requestFonts(fonts)
|
|
182
|
+
|
|
183
|
+
expect(queueMicrotask).toHaveBeenCalled()
|
|
184
|
+
})
|
|
185
|
+
|
|
186
|
+
it('should deduplicate font requests', () => {
|
|
187
|
+
const font = createMockFont()
|
|
188
|
+
|
|
189
|
+
fontManager.requestFonts([font])
|
|
190
|
+
fontManager.requestFonts([font])
|
|
191
|
+
|
|
192
|
+
expect(queueMicrotask).toHaveBeenCalledTimes(1)
|
|
193
|
+
})
|
|
194
|
+
|
|
195
|
+
it('should handle editor disposal during async loading', () => {
|
|
196
|
+
const fonts = [createMockFont()]
|
|
197
|
+
editor.isDisposed = true
|
|
198
|
+
|
|
199
|
+
fontManager.requestFonts(fonts)
|
|
200
|
+
|
|
201
|
+
const callback = (queueMicrotask as jest.Mock).mock.calls[0][0]
|
|
202
|
+
expect(() => callback()).not.toThrow()
|
|
203
|
+
})
|
|
204
|
+
})
|
|
205
|
+
|
|
206
|
+
describe('toEmbeddedCssDeclaration', () => {
|
|
207
|
+
it('should generate font CSS without data conversion (simplified test)', async () => {
|
|
208
|
+
const font = createMockFont()
|
|
209
|
+
|
|
210
|
+
// Mock the actual method implementation to avoid FileHelpers dependency
|
|
211
|
+
const mockCssDeclaration = `@font-face {
|
|
212
|
+
font-family: "${font.family}";
|
|
213
|
+
src: url("mock-data-url");
|
|
214
|
+
}`
|
|
215
|
+
|
|
216
|
+
jest.spyOn(fontManager, 'toEmbeddedCssDeclaration').mockResolvedValue(mockCssDeclaration)
|
|
217
|
+
|
|
218
|
+
const result = await fontManager.toEmbeddedCssDeclaration(font)
|
|
219
|
+
|
|
220
|
+
expect(result).toContain(`font-family: "${font.family}";`)
|
|
221
|
+
expect(result).toContain('src:')
|
|
222
|
+
expect(result).toContain('@font-face {')
|
|
223
|
+
expect(result).toContain('}')
|
|
224
|
+
})
|
|
225
|
+
|
|
226
|
+
it('should call toEmbeddedCssDeclaration method', async () => {
|
|
227
|
+
const font = createMockFont()
|
|
228
|
+
|
|
229
|
+
// Simple spy to verify the method is called
|
|
230
|
+
const spy = jest.spyOn(fontManager, 'toEmbeddedCssDeclaration').mockResolvedValue('mock-css')
|
|
231
|
+
|
|
232
|
+
await fontManager.toEmbeddedCssDeclaration(font)
|
|
233
|
+
|
|
234
|
+
expect(spy).toHaveBeenCalledWith(font)
|
|
235
|
+
spy.mockRestore()
|
|
236
|
+
})
|
|
237
|
+
})
|
|
238
|
+
|
|
239
|
+
describe('error handling and edge cases', () => {
|
|
240
|
+
it('should handle empty getCurrentPageShapeIds', () => {
|
|
241
|
+
editor.getCurrentPageShapeIds.mockReturnValue(new Set())
|
|
242
|
+
|
|
243
|
+
expect(() => fontManager.loadRequiredFontsForCurrentPage()).not.toThrow()
|
|
244
|
+
})
|
|
245
|
+
|
|
246
|
+
it('should handle null shape from getShape', async () => {
|
|
247
|
+
const shapeId = createShapeId('test')
|
|
248
|
+
editor.getCurrentPageShapeIds.mockReturnValue(new Set([shapeId]))
|
|
249
|
+
editor.getShape.mockReturnValue(undefined)
|
|
250
|
+
|
|
251
|
+
await expect(fontManager.loadRequiredFontsForCurrentPage()).rejects.toThrow()
|
|
252
|
+
})
|
|
253
|
+
|
|
254
|
+
it('should handle fonts with minimal properties', async () => {
|
|
255
|
+
const minimalFont: TLFontFace = {
|
|
256
|
+
family: 'Minimal Font',
|
|
257
|
+
src: { url: 'minimal.woff2' },
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
await expect(fontManager.ensureFontIsLoaded(minimalFont)).resolves.toBeUndefined()
|
|
261
|
+
})
|
|
262
|
+
})
|
|
263
|
+
})
|
|
@@ -8,7 +8,7 @@ import {
|
|
|
8
8
|
mapObjectMapValues,
|
|
9
9
|
objectMapEntries,
|
|
10
10
|
} from '@tldraw/utils'
|
|
11
|
-
import { Editor } from '
|
|
11
|
+
import { Editor } from '../../Editor'
|
|
12
12
|
|
|
13
13
|
/**
|
|
14
14
|
* Represents the `src` property of a {@link TLFontFace}.
|
|
@@ -94,7 +94,10 @@ export class FontManager {
|
|
|
94
94
|
const shapeUtil = this.editor.getShapeUtil(shape)
|
|
95
95
|
return shapeUtil.getFontFaces(shape)
|
|
96
96
|
},
|
|
97
|
-
{
|
|
97
|
+
{
|
|
98
|
+
areResultsEqual: areArraysShallowEqual,
|
|
99
|
+
areRecordsEqual: (a, b) => a.props === b.props && a.meta === b.meta,
|
|
100
|
+
}
|
|
98
101
|
)
|
|
99
102
|
|
|
100
103
|
this.shapeFontLoadStateCache = editor.store.createCache<(FontState | null)[], TLShape>(
|
package/src/lib/editor/managers/{HistoryManager.test.ts → HistoryManager/HistoryManager.test.ts}
RENAMED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { BaseRecord, RecordId, Store, StoreSchema, createRecordType } from '@tldraw/store'
|
|
2
|
-
import { TLHistoryBatchOptions } from '
|
|
2
|
+
import { TLHistoryBatchOptions } from '../../types/history-types'
|
|
3
3
|
import { HistoryManager } from './HistoryManager'
|
|
4
4
|
|
|
5
5
|
interface TestRecord extends BaseRecord<'test', TestRecordId> {
|
|
@@ -472,3 +472,390 @@ describe('history options', () => {
|
|
|
472
472
|
expect(getState()).toMatchObject({ a: 0, b: 0 })
|
|
473
473
|
})
|
|
474
474
|
})
|
|
475
|
+
|
|
476
|
+
describe('HistoryManager constructor and lifecycle', () => {
|
|
477
|
+
let store: Store<TestRecord>
|
|
478
|
+
|
|
479
|
+
beforeEach(() => {
|
|
480
|
+
store = new Store({ schema: testSchema, props: null })
|
|
481
|
+
store.put([testSchema.types.test.create({ id: ids.a, value: 0 })])
|
|
482
|
+
})
|
|
483
|
+
|
|
484
|
+
it('should initialize with store reference', () => {
|
|
485
|
+
const manager = new HistoryManager({ store })
|
|
486
|
+
expect(manager).toBeDefined()
|
|
487
|
+
expect(manager.getNumUndos()).toBe(0)
|
|
488
|
+
expect(manager.getNumRedos()).toBe(0)
|
|
489
|
+
})
|
|
490
|
+
|
|
491
|
+
it('should initialize with optional annotateError callback', () => {
|
|
492
|
+
const mockAnnotateError = jest.fn()
|
|
493
|
+
const manager = new HistoryManager({ store, annotateError: mockAnnotateError })
|
|
494
|
+
expect(manager).toBeDefined()
|
|
495
|
+
})
|
|
496
|
+
|
|
497
|
+
it('should properly dispose and cleanup', () => {
|
|
498
|
+
const manager = new HistoryManager({ store })
|
|
499
|
+
expect(typeof manager.dispose).toBe('function')
|
|
500
|
+
|
|
501
|
+
// Should not throw when disposing
|
|
502
|
+
expect(() => manager.dispose()).not.toThrow()
|
|
503
|
+
})
|
|
504
|
+
|
|
505
|
+
it('should handle errors in batch operations with annotateError', () => {
|
|
506
|
+
const mockAnnotateError = jest.fn()
|
|
507
|
+
const manager = new HistoryManager({ store, annotateError: mockAnnotateError })
|
|
508
|
+
|
|
509
|
+
const errorFn = () => {
|
|
510
|
+
throw new Error('Test error')
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
expect(() => manager.batch(errorFn)).toThrow('Test error')
|
|
514
|
+
expect(mockAnnotateError).toHaveBeenCalledWith(expect.any(Error))
|
|
515
|
+
})
|
|
516
|
+
|
|
517
|
+
it('should handle nested batch error scenarios', () => {
|
|
518
|
+
const mockAnnotateError = jest.fn()
|
|
519
|
+
const manager = new HistoryManager({ store, annotateError: mockAnnotateError })
|
|
520
|
+
|
|
521
|
+
const nestedErrorFn = () => {
|
|
522
|
+
manager.batch(() => {
|
|
523
|
+
throw new Error('Nested error')
|
|
524
|
+
})
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
expect(() => manager.batch(nestedErrorFn)).toThrow('Nested error')
|
|
528
|
+
expect(mockAnnotateError).toHaveBeenCalledWith(expect.any(Error))
|
|
529
|
+
})
|
|
530
|
+
})
|
|
531
|
+
|
|
532
|
+
describe('HistoryManager getters and utilities', () => {
|
|
533
|
+
let manager: HistoryManager<TestRecord>
|
|
534
|
+
let store: Store<TestRecord>
|
|
535
|
+
|
|
536
|
+
beforeEach(() => {
|
|
537
|
+
store = new Store({ schema: testSchema, props: null })
|
|
538
|
+
store.put([testSchema.types.test.create({ id: ids.a, value: 0 })])
|
|
539
|
+
manager = new HistoryManager({ store })
|
|
540
|
+
})
|
|
541
|
+
|
|
542
|
+
describe('getNumUndos and getNumRedos', () => {
|
|
543
|
+
it('should return 0 for empty history', () => {
|
|
544
|
+
expect(manager.getNumUndos()).toBe(0)
|
|
545
|
+
expect(manager.getNumRedos()).toBe(0)
|
|
546
|
+
})
|
|
547
|
+
|
|
548
|
+
it('should count undos correctly with pending changes', () => {
|
|
549
|
+
store.update(ids.a, (s) => ({ ...s, value: 1 }))
|
|
550
|
+
expect(manager.getNumUndos()).toBe(1)
|
|
551
|
+
expect(manager.getNumRedos()).toBe(0)
|
|
552
|
+
})
|
|
553
|
+
|
|
554
|
+
it('should count undos and redos after operations', () => {
|
|
555
|
+
expect(manager.getNumUndos()).toBe(0)
|
|
556
|
+
expect(manager.getNumRedos()).toBe(0)
|
|
557
|
+
|
|
558
|
+
store.update(ids.a, (s) => ({ ...s, value: 1 }))
|
|
559
|
+
expect(manager.getNumUndos()).toBe(1)
|
|
560
|
+
expect(manager.getNumRedos()).toBe(0)
|
|
561
|
+
|
|
562
|
+
manager._mark('mark1')
|
|
563
|
+
|
|
564
|
+
expect(manager.getNumUndos()).toBe(2)
|
|
565
|
+
expect(manager.getNumRedos()).toBe(0)
|
|
566
|
+
|
|
567
|
+
store.update(ids.a, (s) => ({ ...s, value: 2 }))
|
|
568
|
+
|
|
569
|
+
// Based on actual behavior: 1 pending + 1 mark + 1 flushed = 3
|
|
570
|
+
expect(manager.getNumUndos()).toBe(3)
|
|
571
|
+
expect(manager.getNumRedos()).toBe(0)
|
|
572
|
+
|
|
573
|
+
manager.undo()
|
|
574
|
+
expect(manager.getNumUndos()).toBe(1) // After undo, we're back to just the first operation
|
|
575
|
+
expect(manager.getNumRedos()).toBe(2) // Undo moved 2 items to redo stack (pending + mark)
|
|
576
|
+
})
|
|
577
|
+
|
|
578
|
+
it('should count correctly after clearing redo stack', () => {
|
|
579
|
+
store.update(ids.a, (s) => ({ ...s, value: 1 }))
|
|
580
|
+
manager.undo()
|
|
581
|
+
expect(manager.getNumRedos()).toBe(1)
|
|
582
|
+
|
|
583
|
+
store.update(ids.a, (s) => ({ ...s, value: 2 }))
|
|
584
|
+
expect(manager.getNumRedos()).toBe(0)
|
|
585
|
+
})
|
|
586
|
+
})
|
|
587
|
+
|
|
588
|
+
describe('getMarkIdMatching', () => {
|
|
589
|
+
it('should return null when no marks exist', () => {
|
|
590
|
+
expect(manager.getMarkIdMatching('test')).toBeNull()
|
|
591
|
+
})
|
|
592
|
+
|
|
593
|
+
it('should find marks by substring', () => {
|
|
594
|
+
manager._mark('test-mark-1')
|
|
595
|
+
store.update(ids.a, (s) => ({ ...s, value: 1 }))
|
|
596
|
+
manager._mark('other-mark-2')
|
|
597
|
+
|
|
598
|
+
expect(manager.getMarkIdMatching('test')).toBe('test-mark-1')
|
|
599
|
+
expect(manager.getMarkIdMatching('other')).toBe('other-mark-2')
|
|
600
|
+
expect(manager.getMarkIdMatching('mark')).toBe('other-mark-2') // returns most recent
|
|
601
|
+
})
|
|
602
|
+
|
|
603
|
+
it('should return null for non-existent substrings', () => {
|
|
604
|
+
manager._mark('test-mark')
|
|
605
|
+
expect(manager.getMarkIdMatching('nonexistent')).toBeNull()
|
|
606
|
+
})
|
|
607
|
+
|
|
608
|
+
it('should find marks after undo operations', () => {
|
|
609
|
+
manager._mark('initial-mark')
|
|
610
|
+
store.update(ids.a, (s) => ({ ...s, value: 1 }))
|
|
611
|
+
manager._mark('second-mark')
|
|
612
|
+
store.update(ids.a, (s) => ({ ...s, value: 2 }))
|
|
613
|
+
|
|
614
|
+
// After undo, marks should still be findable in the undo stack
|
|
615
|
+
expect(manager.getMarkIdMatching('initial')).toBe('initial-mark')
|
|
616
|
+
expect(manager.getMarkIdMatching('second')).toBe('second-mark')
|
|
617
|
+
})
|
|
618
|
+
})
|
|
619
|
+
|
|
620
|
+
describe('clear method', () => {
|
|
621
|
+
it('should clear all history', () => {
|
|
622
|
+
store.update(ids.a, (s) => ({ ...s, value: 1 }))
|
|
623
|
+
manager._mark('test-mark')
|
|
624
|
+
store.update(ids.a, (s) => ({ ...s, value: 2 }))
|
|
625
|
+
|
|
626
|
+
expect(manager.getNumUndos()).toBeGreaterThan(0)
|
|
627
|
+
|
|
628
|
+
manager.clear()
|
|
629
|
+
|
|
630
|
+
expect(manager.getNumUndos()).toBe(0)
|
|
631
|
+
expect(manager.getNumRedos()).toBe(0)
|
|
632
|
+
})
|
|
633
|
+
|
|
634
|
+
it('should clear pending diffs', () => {
|
|
635
|
+
store.update(ids.a, (s) => ({ ...s, value: 1 }))
|
|
636
|
+
expect(manager.getNumUndos()).toBe(1)
|
|
637
|
+
|
|
638
|
+
manager.clear()
|
|
639
|
+
expect(manager.getNumUndos()).toBe(0)
|
|
640
|
+
})
|
|
641
|
+
|
|
642
|
+
it('should allow new operations after clearing', () => {
|
|
643
|
+
store.update(ids.a, (s) => ({ ...s, value: 1 }))
|
|
644
|
+
manager.clear()
|
|
645
|
+
|
|
646
|
+
store.update(ids.a, (s) => ({ ...s, value: 2 }))
|
|
647
|
+
expect(manager.getNumUndos()).toBe(1)
|
|
648
|
+
|
|
649
|
+
manager.undo()
|
|
650
|
+
expect(store.get(ids.a)!.value).toBe(1) // Should undo to value before the new operation
|
|
651
|
+
})
|
|
652
|
+
})
|
|
653
|
+
|
|
654
|
+
describe('debug method', () => {
|
|
655
|
+
it('should return debug information', () => {
|
|
656
|
+
store.update(ids.a, (s) => ({ ...s, value: 1 }))
|
|
657
|
+
manager._mark('test-mark')
|
|
658
|
+
store.update(ids.a, (s) => ({ ...s, value: 2 }))
|
|
659
|
+
|
|
660
|
+
const debug = manager.debug()
|
|
661
|
+
|
|
662
|
+
expect(debug).toHaveProperty('undos')
|
|
663
|
+
expect(debug).toHaveProperty('redos')
|
|
664
|
+
expect(debug).toHaveProperty('pendingDiff')
|
|
665
|
+
expect(debug).toHaveProperty('state')
|
|
666
|
+
expect(Array.isArray(debug.undos)).toBe(true)
|
|
667
|
+
expect(Array.isArray(debug.redos)).toBe(true)
|
|
668
|
+
})
|
|
669
|
+
|
|
670
|
+
it('should show correct state information', () => {
|
|
671
|
+
const debug = manager.debug()
|
|
672
|
+
expect(debug.state).toBe('recording')
|
|
673
|
+
})
|
|
674
|
+
|
|
675
|
+
it('should reflect changes after undo/redo', () => {
|
|
676
|
+
store.update(ids.a, (s) => ({ ...s, value: 1 }))
|
|
677
|
+
manager.undo()
|
|
678
|
+
|
|
679
|
+
const debug = manager.debug()
|
|
680
|
+
expect(debug.redos.length).toBeGreaterThan(0)
|
|
681
|
+
})
|
|
682
|
+
})
|
|
683
|
+
})
|
|
684
|
+
|
|
685
|
+
describe('HistoryManager error scenarios and edge cases', () => {
|
|
686
|
+
let manager: HistoryManager<TestRecord>
|
|
687
|
+
let store: Store<TestRecord>
|
|
688
|
+
|
|
689
|
+
beforeEach(() => {
|
|
690
|
+
store = new Store({ schema: testSchema, props: null })
|
|
691
|
+
store.put([testSchema.types.test.create({ id: ids.a, value: 0 })])
|
|
692
|
+
manager = new HistoryManager({ store })
|
|
693
|
+
})
|
|
694
|
+
|
|
695
|
+
describe('squashToMark error handling', () => {
|
|
696
|
+
it('should handle non-existent mark gracefully', () => {
|
|
697
|
+
store.update(ids.a, (s) => ({ ...s, value: 1 }))
|
|
698
|
+
const consoleSpy = jest.spyOn(console, 'error').mockImplementation()
|
|
699
|
+
|
|
700
|
+
manager.squashToMark('non-existent-mark')
|
|
701
|
+
|
|
702
|
+
expect(consoleSpy).toHaveBeenCalledWith(
|
|
703
|
+
'Could not find mark to squash to: ',
|
|
704
|
+
'non-existent-mark'
|
|
705
|
+
)
|
|
706
|
+
consoleSpy.mockRestore()
|
|
707
|
+
})
|
|
708
|
+
|
|
709
|
+
it('should handle empty stack when squashing', () => {
|
|
710
|
+
const consoleSpy = jest.spyOn(console, 'error').mockImplementation()
|
|
711
|
+
|
|
712
|
+
manager.squashToMark('non-existent')
|
|
713
|
+
|
|
714
|
+
expect(consoleSpy).toHaveBeenCalled()
|
|
715
|
+
consoleSpy.mockRestore()
|
|
716
|
+
})
|
|
717
|
+
|
|
718
|
+
it('should return early when no changes to squash', () => {
|
|
719
|
+
manager._mark('test-mark')
|
|
720
|
+
|
|
721
|
+
// No operations between marks
|
|
722
|
+
const result = manager.squashToMark('test-mark')
|
|
723
|
+
expect(result).toBe(manager) // Should return manager instance
|
|
724
|
+
})
|
|
725
|
+
})
|
|
726
|
+
|
|
727
|
+
describe('bailToMark with non-existent marks', () => {
|
|
728
|
+
it('should handle non-existent mark in bailToMark', () => {
|
|
729
|
+
store.update(ids.a, (s) => ({ ...s, value: 1 }))
|
|
730
|
+
const originalValue = store.get(ids.a)!.value
|
|
731
|
+
|
|
732
|
+
manager.bailToMark('non-existent-mark')
|
|
733
|
+
|
|
734
|
+
// Should not change anything when mark doesn't exist
|
|
735
|
+
expect(store.get(ids.a)!.value).toBe(originalValue)
|
|
736
|
+
})
|
|
737
|
+
|
|
738
|
+
it('should find mark correctly when it exists', () => {
|
|
739
|
+
manager._mark('existing-mark')
|
|
740
|
+
store.update(ids.a, (s) => ({ ...s, value: 1 }))
|
|
741
|
+
store.update(ids.a, (s) => ({ ...s, value: 2 }))
|
|
742
|
+
|
|
743
|
+
manager.bailToMark('existing-mark')
|
|
744
|
+
expect(store.get(ids.a)!.value).toBe(0)
|
|
745
|
+
})
|
|
746
|
+
})
|
|
747
|
+
|
|
748
|
+
describe('empty stack operations', () => {
|
|
749
|
+
it('should handle undo on empty stack', () => {
|
|
750
|
+
expect(() => manager.undo()).not.toThrow()
|
|
751
|
+
expect(manager.getNumUndos()).toBe(0)
|
|
752
|
+
})
|
|
753
|
+
|
|
754
|
+
it('should handle redo on empty stack', () => {
|
|
755
|
+
expect(() => manager.redo()).not.toThrow()
|
|
756
|
+
expect(manager.getNumRedos()).toBe(0)
|
|
757
|
+
})
|
|
758
|
+
|
|
759
|
+
it('should handle bail on empty stack', () => {
|
|
760
|
+
expect(() => manager.bail()).not.toThrow()
|
|
761
|
+
expect(manager.getNumUndos()).toBe(0)
|
|
762
|
+
})
|
|
763
|
+
})
|
|
764
|
+
|
|
765
|
+
describe('batch operation edge cases', () => {
|
|
766
|
+
it('should handle nested batches correctly', () => {
|
|
767
|
+
let callCount = 0
|
|
768
|
+
|
|
769
|
+
manager.batch(() => {
|
|
770
|
+
callCount++
|
|
771
|
+
manager.batch(() => {
|
|
772
|
+
callCount++
|
|
773
|
+
store.update(ids.a, (s) => ({ ...s, value: 1 }))
|
|
774
|
+
})
|
|
775
|
+
store.update(ids.a, (s) => ({ ...s, value: 2 }))
|
|
776
|
+
})
|
|
777
|
+
|
|
778
|
+
expect(callCount).toBe(2)
|
|
779
|
+
expect(store.get(ids.a)!.value).toBe(2)
|
|
780
|
+
})
|
|
781
|
+
|
|
782
|
+
it('should maintain batch state correctly during errors', () => {
|
|
783
|
+
const mockAnnotateError = jest.fn()
|
|
784
|
+
const errorManager = new HistoryManager({ store, annotateError: mockAnnotateError })
|
|
785
|
+
|
|
786
|
+
try {
|
|
787
|
+
errorManager.batch(() => {
|
|
788
|
+
store.update(ids.a, (s) => ({ ...s, value: 1 }))
|
|
789
|
+
throw new Error('Test error')
|
|
790
|
+
})
|
|
791
|
+
} catch (_e) {
|
|
792
|
+
// Expected to throw
|
|
793
|
+
}
|
|
794
|
+
|
|
795
|
+
// Should be able to perform normal operations after error
|
|
796
|
+
expect(() => {
|
|
797
|
+
errorManager.batch(() => {
|
|
798
|
+
store.update(ids.a, (s) => ({ ...s, value: 2 }))
|
|
799
|
+
})
|
|
800
|
+
}).not.toThrow()
|
|
801
|
+
})
|
|
802
|
+
|
|
803
|
+
it('should handle batch with undefined history option', () => {
|
|
804
|
+
expect(() => {
|
|
805
|
+
manager.batch(() => {
|
|
806
|
+
store.update(ids.a, (s) => ({ ...s, value: 1 }))
|
|
807
|
+
}, undefined)
|
|
808
|
+
}).not.toThrow()
|
|
809
|
+
})
|
|
810
|
+
})
|
|
811
|
+
|
|
812
|
+
describe('large history operations', () => {
|
|
813
|
+
it('should handle many undo operations', () => {
|
|
814
|
+
// Create operations with marks - based on existing test pattern
|
|
815
|
+
for (let i = 1; i <= 10; i++) {
|
|
816
|
+
store.update(ids.a, (s) => ({ ...s, value: i }))
|
|
817
|
+
if (i % 3 === 0) manager._mark(`mark-${i}`)
|
|
818
|
+
}
|
|
819
|
+
|
|
820
|
+
const undoCount = manager.getNumUndos()
|
|
821
|
+
expect(undoCount).toBeGreaterThan(3)
|
|
822
|
+
|
|
823
|
+
// Undo some operations
|
|
824
|
+
const undosToPerform = 3
|
|
825
|
+
for (let i = 0; i < undosToPerform; i++) {
|
|
826
|
+
manager.undo()
|
|
827
|
+
}
|
|
828
|
+
|
|
829
|
+
// Due to marks, the redo count might be different
|
|
830
|
+
expect(manager.getNumRedos()).toBeGreaterThan(0)
|
|
831
|
+
})
|
|
832
|
+
|
|
833
|
+
it('should handle alternating undo/redo operations', () => {
|
|
834
|
+
for (let i = 1; i <= 10; i++) {
|
|
835
|
+
store.update(ids.a, (s) => ({ ...s, value: i }))
|
|
836
|
+
}
|
|
837
|
+
|
|
838
|
+
// Alternate undo/redo
|
|
839
|
+
for (let i = 0; i < 5; i++) {
|
|
840
|
+
manager.undo()
|
|
841
|
+
manager.redo()
|
|
842
|
+
}
|
|
843
|
+
|
|
844
|
+
expect(store.get(ids.a)!.value).toBe(10)
|
|
845
|
+
})
|
|
846
|
+
})
|
|
847
|
+
|
|
848
|
+
describe('concurrent-like operations', () => {
|
|
849
|
+
it('should handle rapid sequential operations', () => {
|
|
850
|
+
const initialValue = store.get(ids.a)!.value
|
|
851
|
+
|
|
852
|
+
// Rapid fire updates
|
|
853
|
+
for (let i = 1; i <= 20; i++) {
|
|
854
|
+
store.update(ids.a, (s) => ({ ...s, value: i }))
|
|
855
|
+
}
|
|
856
|
+
|
|
857
|
+
manager.undo()
|
|
858
|
+
expect(store.get(ids.a)!.value).toBe(initialValue)
|
|
859
|
+
})
|
|
860
|
+
})
|
|
861
|
+
})
|