@lazlon-platform/html-editor 0.1.0 → 0.2.1
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/package.json +14 -11
- package/.claude/settings.local.json +0 -9
- package/.github/workflows/ci.yml +0 -34
- package/demo/App.tsx +0 -62
- package/demo/EditorView/PageView/NodeContent.tsx +0 -35
- package/demo/EditorView/PageView/SnapLines.tsx +0 -28
- package/demo/EditorView/PageView/index.tsx +0 -45
- package/demo/EditorView/SelectionFrame/Corner.tsx +0 -24
- package/demo/EditorView/SelectionFrame/Edge.tsx +0 -21
- package/demo/EditorView/SelectionFrame/index.tsx +0 -27
- package/demo/EditorView/SelectionOverlay/ActionHud.tsx +0 -32
- package/demo/EditorView/SelectionOverlay/Rotation.tsx +0 -39
- package/demo/EditorView/SelectionOverlay/Toolbar.tsx +0 -128
- package/demo/EditorView/SelectionOverlay/index.tsx +0 -21
- package/demo/EditorView/Toolbar/index.tsx +0 -68
- package/demo/EditorView/index.tsx +0 -47
- package/demo/Navbar/index.tsx +0 -33
- package/demo/Sidebar/index.tsx +0 -71
- package/demo/hotkeys.ts +0 -93
- package/demo/main.tsx +0 -10
- package/demo/style.css +0 -1
- package/eslint.config.js +0 -43
- package/index.html +0 -14
- package/tests/createTestEditor.ts +0 -19
- package/tests/hooks/actions.test.tsx +0 -736
- package/tests/hooks/batch.test.tsx +0 -332
- package/tests/hooks/editor.test.tsx +0 -56
- package/tests/hooks/page.test.tsx +0 -135
- package/tests/hooks/pointer/pointer.test.tsx +0 -244
- package/tests/hooks/textMarks.test.tsx +0 -624
- package/tests/model/editor.test.ts +0 -384
- package/tests/model/history.test.ts +0 -293
- package/tests/model/node/group.test.ts +0 -294
- package/tests/model/node/image.test.ts +0 -150
- package/tests/model/node/polygon.test.ts +0 -408
- package/tests/model/node/text.test.ts +0 -158
- package/tests/model/node.test.ts +0 -276
- package/tests/model/page.test.ts +0 -150
- package/tests/setup.ts +0 -7
- package/tsconfig.json +0 -28
- package/vite.config.ts +0 -9
- package/vitest.config.ts +0 -13
|
@@ -1,624 +0,0 @@
|
|
|
1
|
-
import { act, renderHook } from "@testing-library/react"
|
|
2
|
-
import { beforeEach, describe, expect, it } from "vitest"
|
|
3
|
-
import { EditorContext } from "../../lib/hooks/editor"
|
|
4
|
-
import { blurNode, useTextMarks } from "../../lib/hooks/textMarks"
|
|
5
|
-
import {
|
|
6
|
-
FormattableNode,
|
|
7
|
-
type FormattableNodeProps,
|
|
8
|
-
} from "../../lib/model/node/formattable"
|
|
9
|
-
import { TextNode, TextNodeProps } from "../../lib/model/node/text"
|
|
10
|
-
import { Page } from "../../lib/model/page"
|
|
11
|
-
import { createTestEditor } from "../createTestEditor"
|
|
12
|
-
import { EditableNode } from "../../lib/model"
|
|
13
|
-
|
|
14
|
-
// Concrete FormattableNode implementation for testing
|
|
15
|
-
class TestFormattableNode extends FormattableNode {
|
|
16
|
-
get name() {
|
|
17
|
-
return "test-formattable"
|
|
18
|
-
}
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
describe("useTextMarks", () => {
|
|
22
|
-
let editor: ReturnType<typeof createTestEditor>
|
|
23
|
-
let page: Page
|
|
24
|
-
|
|
25
|
-
beforeEach(() => {
|
|
26
|
-
editor = createTestEditor()
|
|
27
|
-
page = new Page(editor, { id: "page-1" })
|
|
28
|
-
editor.pages = new Map([["page-1", page]])
|
|
29
|
-
})
|
|
30
|
-
|
|
31
|
-
function wrapper({ children }: { children: React.ReactNode }) {
|
|
32
|
-
return <EditorContext value={editor}>{children}</EditorContext>
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
function createTextNode(props: Partial<TextNodeProps> = {}): TextNode {
|
|
36
|
-
return new TextNode(editor, page, {
|
|
37
|
-
id: editor.id(),
|
|
38
|
-
x: 0,
|
|
39
|
-
y: 0,
|
|
40
|
-
width: 100,
|
|
41
|
-
height: 50,
|
|
42
|
-
...props,
|
|
43
|
-
})
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
function createFormattableNode(
|
|
47
|
-
props: Partial<FormattableNodeProps> = {},
|
|
48
|
-
): TestFormattableNode {
|
|
49
|
-
return new TestFormattableNode(editor, page, {
|
|
50
|
-
id: editor.id(),
|
|
51
|
-
x: 0,
|
|
52
|
-
y: 0,
|
|
53
|
-
width: 100,
|
|
54
|
-
height: 50,
|
|
55
|
-
...props,
|
|
56
|
-
})
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
describe("with empty selection", () => {
|
|
60
|
-
it("returns null state when no nodes provided", () => {
|
|
61
|
-
const { result } = renderHook(
|
|
62
|
-
() =>
|
|
63
|
-
useTextMarks({
|
|
64
|
-
editables: [],
|
|
65
|
-
formattables: [],
|
|
66
|
-
}),
|
|
67
|
-
{ wrapper },
|
|
68
|
-
)
|
|
69
|
-
|
|
70
|
-
expect(result.current.state).toBeNull()
|
|
71
|
-
})
|
|
72
|
-
})
|
|
73
|
-
|
|
74
|
-
describe("with EditableNodes (TextNode)", () => {
|
|
75
|
-
it("returns text marks state from single editable node", () => {
|
|
76
|
-
const node = createTextNode()
|
|
77
|
-
page.nodes = new Map([[node.id, node]])
|
|
78
|
-
|
|
79
|
-
const { result } = renderHook(
|
|
80
|
-
() =>
|
|
81
|
-
useTextMarks({
|
|
82
|
-
editables: [node],
|
|
83
|
-
formattables: [],
|
|
84
|
-
}),
|
|
85
|
-
{ wrapper },
|
|
86
|
-
)
|
|
87
|
-
|
|
88
|
-
// Default state values
|
|
89
|
-
expect(result.current.state).not.toBeNull()
|
|
90
|
-
expect(result.current.state?.isBold).toBe(false)
|
|
91
|
-
expect(result.current.state?.isItalic).toBe(false)
|
|
92
|
-
expect(result.current.state?.isUnderline).toBe(false)
|
|
93
|
-
expect(result.current.state?.isStrike).toBe(false)
|
|
94
|
-
})
|
|
95
|
-
|
|
96
|
-
it("toggleBold toggles bold on all selected nodes", () => {
|
|
97
|
-
const node = createTextNode()
|
|
98
|
-
page.nodes = new Map([[node.id, node]])
|
|
99
|
-
|
|
100
|
-
// Add some text content so it's not empty
|
|
101
|
-
node.tiptap.commands.setContent({
|
|
102
|
-
type: "doc",
|
|
103
|
-
content: [{ type: "paragraph", content: [{ type: "text", text: "Hello" }] }],
|
|
104
|
-
})
|
|
105
|
-
node.tiptap.commands.selectAll()
|
|
106
|
-
|
|
107
|
-
const { result } = renderHook(
|
|
108
|
-
() =>
|
|
109
|
-
useTextMarks({
|
|
110
|
-
editables: [node],
|
|
111
|
-
formattables: [],
|
|
112
|
-
}),
|
|
113
|
-
{ wrapper },
|
|
114
|
-
)
|
|
115
|
-
|
|
116
|
-
act(() => {
|
|
117
|
-
result.current.toggle("Bold")
|
|
118
|
-
})
|
|
119
|
-
|
|
120
|
-
// Verify bold was applied via Tiptap
|
|
121
|
-
expect(node.tiptap.isActive("bold")).toBe(true)
|
|
122
|
-
})
|
|
123
|
-
|
|
124
|
-
it("setColor sets color on editable nodes", () => {
|
|
125
|
-
const node = createTextNode()
|
|
126
|
-
page.nodes = new Map([[node.id, node]])
|
|
127
|
-
|
|
128
|
-
node.tiptap.commands.setContent({
|
|
129
|
-
type: "doc",
|
|
130
|
-
content: [{ type: "paragraph", content: [{ type: "text", text: "Hello" }] }],
|
|
131
|
-
})
|
|
132
|
-
node.tiptap.commands.selectAll()
|
|
133
|
-
|
|
134
|
-
const { result } = renderHook(
|
|
135
|
-
() =>
|
|
136
|
-
useTextMarks({
|
|
137
|
-
editables: [node],
|
|
138
|
-
formattables: [],
|
|
139
|
-
}),
|
|
140
|
-
{ wrapper },
|
|
141
|
-
)
|
|
142
|
-
|
|
143
|
-
act(() => {
|
|
144
|
-
result.current.setColor("#ff0000", { end: true })
|
|
145
|
-
})
|
|
146
|
-
|
|
147
|
-
// Color is applied via Tiptap textStyle mark
|
|
148
|
-
const attrs = node.tiptap.getAttributes("textStyle")
|
|
149
|
-
expect(attrs.color).toBe("#ff0000")
|
|
150
|
-
})
|
|
151
|
-
|
|
152
|
-
it("setSize sets font size on editable nodes", () => {
|
|
153
|
-
const node = createTextNode()
|
|
154
|
-
page.nodes = new Map([[node.id, node]])
|
|
155
|
-
|
|
156
|
-
node.tiptap.commands.setContent({
|
|
157
|
-
type: "doc",
|
|
158
|
-
content: [{ type: "paragraph", content: [{ type: "text", text: "Hello" }] }],
|
|
159
|
-
})
|
|
160
|
-
node.tiptap.commands.selectAll()
|
|
161
|
-
|
|
162
|
-
const { result } = renderHook(
|
|
163
|
-
() =>
|
|
164
|
-
useTextMarks({
|
|
165
|
-
editables: [node],
|
|
166
|
-
formattables: [],
|
|
167
|
-
}),
|
|
168
|
-
{ wrapper },
|
|
169
|
-
)
|
|
170
|
-
|
|
171
|
-
act(() => {
|
|
172
|
-
result.current.setSize(24)
|
|
173
|
-
})
|
|
174
|
-
|
|
175
|
-
const attrs = node.tiptap.getAttributes("textStyle")
|
|
176
|
-
expect(attrs.fontSize).toBe("24px")
|
|
177
|
-
})
|
|
178
|
-
|
|
179
|
-
it("setFamily sets font family on editable nodes", () => {
|
|
180
|
-
const node = createTextNode()
|
|
181
|
-
page.nodes = new Map([[node.id, node]])
|
|
182
|
-
|
|
183
|
-
node.tiptap.commands.setContent({
|
|
184
|
-
type: "doc",
|
|
185
|
-
content: [{ type: "paragraph", content: [{ type: "text", text: "Hello" }] }],
|
|
186
|
-
})
|
|
187
|
-
node.tiptap.commands.selectAll()
|
|
188
|
-
|
|
189
|
-
const { result } = renderHook(
|
|
190
|
-
() =>
|
|
191
|
-
useTextMarks({
|
|
192
|
-
editables: [node],
|
|
193
|
-
formattables: [],
|
|
194
|
-
}),
|
|
195
|
-
{ wrapper },
|
|
196
|
-
)
|
|
197
|
-
|
|
198
|
-
act(() => {
|
|
199
|
-
result.current.setFamily("Arial")
|
|
200
|
-
})
|
|
201
|
-
|
|
202
|
-
const attrs = node.tiptap.getAttributes("textStyle")
|
|
203
|
-
expect(attrs.fontFamily).toBe("Arial")
|
|
204
|
-
})
|
|
205
|
-
|
|
206
|
-
it("setFamily with null unsets font family", () => {
|
|
207
|
-
const node = createTextNode()
|
|
208
|
-
page.nodes = new Map([[node.id, node]])
|
|
209
|
-
|
|
210
|
-
node.tiptap.commands.setContent({
|
|
211
|
-
type: "doc",
|
|
212
|
-
content: [{ type: "paragraph", content: [{ type: "text", text: "Hello" }] }],
|
|
213
|
-
})
|
|
214
|
-
node.tiptap.commands.selectAll()
|
|
215
|
-
node.tiptap.commands.setFontFamily("Arial")
|
|
216
|
-
|
|
217
|
-
const { result } = renderHook(
|
|
218
|
-
() =>
|
|
219
|
-
useTextMarks({
|
|
220
|
-
editables: [node],
|
|
221
|
-
formattables: [],
|
|
222
|
-
}),
|
|
223
|
-
{ wrapper },
|
|
224
|
-
)
|
|
225
|
-
|
|
226
|
-
act(() => {
|
|
227
|
-
result.current.setFamily(null)
|
|
228
|
-
})
|
|
229
|
-
|
|
230
|
-
const attrs = node.tiptap.getAttributes("textStyle")
|
|
231
|
-
expect(attrs.fontFamily).toBeUndefined()
|
|
232
|
-
})
|
|
233
|
-
})
|
|
234
|
-
|
|
235
|
-
describe("with FormattableNodes", () => {
|
|
236
|
-
it("returns text marks state from single formattable node", () => {
|
|
237
|
-
const node = createFormattableNode({
|
|
238
|
-
bold: true,
|
|
239
|
-
italic: false,
|
|
240
|
-
color: "#333333",
|
|
241
|
-
size: 18,
|
|
242
|
-
family: "Roboto",
|
|
243
|
-
spacing: 1.5,
|
|
244
|
-
})
|
|
245
|
-
page.nodes = new Map([[node.id, node]])
|
|
246
|
-
|
|
247
|
-
const { result } = renderHook(
|
|
248
|
-
() =>
|
|
249
|
-
useTextMarks({
|
|
250
|
-
editables: [],
|
|
251
|
-
formattables: [node],
|
|
252
|
-
}),
|
|
253
|
-
{ wrapper },
|
|
254
|
-
)
|
|
255
|
-
|
|
256
|
-
expect(result.current.state).not.toBeNull()
|
|
257
|
-
expect(result.current.state?.isBold).toBe(true)
|
|
258
|
-
expect(result.current.state?.isItalic).toBe(false)
|
|
259
|
-
expect(result.current.state?.color).toBe("#333333")
|
|
260
|
-
expect(result.current.state?.size).toBe(18)
|
|
261
|
-
expect(result.current.state?.family).toBe("Roboto")
|
|
262
|
-
expect(result.current.state?.spacing).toBe(1.5)
|
|
263
|
-
})
|
|
264
|
-
|
|
265
|
-
it("toggle updates formattable node properties", () => {
|
|
266
|
-
const node = createFormattableNode({ bold: false })
|
|
267
|
-
page.nodes = new Map([[node.id, node]])
|
|
268
|
-
|
|
269
|
-
const { result } = renderHook(
|
|
270
|
-
() =>
|
|
271
|
-
useTextMarks({
|
|
272
|
-
editables: [],
|
|
273
|
-
formattables: [node],
|
|
274
|
-
}),
|
|
275
|
-
{ wrapper },
|
|
276
|
-
)
|
|
277
|
-
|
|
278
|
-
act(() => {
|
|
279
|
-
result.current.toggle("Bold")
|
|
280
|
-
})
|
|
281
|
-
|
|
282
|
-
expect(node.bold).toBe(true)
|
|
283
|
-
})
|
|
284
|
-
|
|
285
|
-
it("toggle turns off mark when already active", () => {
|
|
286
|
-
const node = createFormattableNode({ bold: true })
|
|
287
|
-
page.nodes = new Map([[node.id, node]])
|
|
288
|
-
|
|
289
|
-
const { result } = renderHook(
|
|
290
|
-
() =>
|
|
291
|
-
useTextMarks({
|
|
292
|
-
editables: [],
|
|
293
|
-
formattables: [node],
|
|
294
|
-
}),
|
|
295
|
-
{ wrapper },
|
|
296
|
-
)
|
|
297
|
-
|
|
298
|
-
act(() => {
|
|
299
|
-
result.current.toggle("Bold")
|
|
300
|
-
})
|
|
301
|
-
|
|
302
|
-
expect(node.bold).toBe(false)
|
|
303
|
-
})
|
|
304
|
-
|
|
305
|
-
it("setColor updates formattable node color with history", () => {
|
|
306
|
-
const node = createFormattableNode({ color: "#000000" })
|
|
307
|
-
page.nodes = new Map([[node.id, node]])
|
|
308
|
-
|
|
309
|
-
const { result } = renderHook(
|
|
310
|
-
() =>
|
|
311
|
-
useTextMarks({
|
|
312
|
-
editables: [],
|
|
313
|
-
formattables: [node],
|
|
314
|
-
}),
|
|
315
|
-
{ wrapper },
|
|
316
|
-
)
|
|
317
|
-
|
|
318
|
-
act(() => {
|
|
319
|
-
result.current.setColor("#ff0000", { end: true })
|
|
320
|
-
})
|
|
321
|
-
|
|
322
|
-
expect(node.color).toBe("#ff0000")
|
|
323
|
-
expect(editor.history.undoHistory.length).toBeGreaterThan(0)
|
|
324
|
-
})
|
|
325
|
-
|
|
326
|
-
it("setColor with end:false does not create history entry", () => {
|
|
327
|
-
const node = createFormattableNode({ color: "#000000" })
|
|
328
|
-
page.nodes = new Map([[node.id, node]])
|
|
329
|
-
|
|
330
|
-
const { result } = renderHook(
|
|
331
|
-
() =>
|
|
332
|
-
useTextMarks({
|
|
333
|
-
editables: [],
|
|
334
|
-
formattables: [node],
|
|
335
|
-
}),
|
|
336
|
-
{ wrapper },
|
|
337
|
-
)
|
|
338
|
-
|
|
339
|
-
const historyLengthBefore = editor.history.undoHistory.length
|
|
340
|
-
|
|
341
|
-
act(() => {
|
|
342
|
-
result.current.setColor("#ff0000", { end: false })
|
|
343
|
-
})
|
|
344
|
-
|
|
345
|
-
expect(node.color).toBe("#ff0000")
|
|
346
|
-
expect(editor.history.undoHistory.length).toBe(historyLengthBefore)
|
|
347
|
-
})
|
|
348
|
-
|
|
349
|
-
it("setSize updates formattable node size with history", () => {
|
|
350
|
-
const node = createFormattableNode({ size: 16 })
|
|
351
|
-
page.nodes = new Map([[node.id, node]])
|
|
352
|
-
|
|
353
|
-
const { result } = renderHook(
|
|
354
|
-
() =>
|
|
355
|
-
useTextMarks({
|
|
356
|
-
editables: [],
|
|
357
|
-
formattables: [node],
|
|
358
|
-
}),
|
|
359
|
-
{ wrapper },
|
|
360
|
-
)
|
|
361
|
-
|
|
362
|
-
act(() => {
|
|
363
|
-
result.current.setSize(24)
|
|
364
|
-
})
|
|
365
|
-
|
|
366
|
-
expect(node.size).toBe(24)
|
|
367
|
-
expect(editor.history.undoHistory.length).toBeGreaterThan(0)
|
|
368
|
-
})
|
|
369
|
-
|
|
370
|
-
it("setFamily updates formattable node family", () => {
|
|
371
|
-
const node = createFormattableNode({ family: null })
|
|
372
|
-
page.nodes = new Map([[node.id, node]])
|
|
373
|
-
|
|
374
|
-
const { result } = renderHook(
|
|
375
|
-
() =>
|
|
376
|
-
useTextMarks({
|
|
377
|
-
editables: [],
|
|
378
|
-
formattables: [node],
|
|
379
|
-
}),
|
|
380
|
-
{ wrapper },
|
|
381
|
-
)
|
|
382
|
-
|
|
383
|
-
act(() => {
|
|
384
|
-
result.current.setFamily("Georgia")
|
|
385
|
-
})
|
|
386
|
-
|
|
387
|
-
expect(node.family).toBe("Georgia")
|
|
388
|
-
})
|
|
389
|
-
|
|
390
|
-
it("setSpacing updates formattable node spacing", () => {
|
|
391
|
-
const node = createFormattableNode({ spacing: 0 })
|
|
392
|
-
page.nodes = new Map([[node.id, node]])
|
|
393
|
-
|
|
394
|
-
const { result } = renderHook(
|
|
395
|
-
() =>
|
|
396
|
-
useTextMarks({
|
|
397
|
-
editables: [],
|
|
398
|
-
formattables: [node],
|
|
399
|
-
}),
|
|
400
|
-
{ wrapper },
|
|
401
|
-
)
|
|
402
|
-
|
|
403
|
-
act(() => {
|
|
404
|
-
result.current.setSpacing(2.5, { end: true })
|
|
405
|
-
})
|
|
406
|
-
|
|
407
|
-
expect(node.spacing).toBe(2.5)
|
|
408
|
-
expect(editor.history.undoHistory.length).toBeGreaterThan(0)
|
|
409
|
-
})
|
|
410
|
-
})
|
|
411
|
-
|
|
412
|
-
describe("with mixed EditableNodes and FormattableNodes", () => {
|
|
413
|
-
it("merges state from both node types", () => {
|
|
414
|
-
const textNode = createTextNode()
|
|
415
|
-
const formattableNode = createFormattableNode({
|
|
416
|
-
bold: true,
|
|
417
|
-
color: "#ff0000",
|
|
418
|
-
})
|
|
419
|
-
page.nodes = new Map<string, EditableNode | FormattableNode>([
|
|
420
|
-
[textNode.id, textNode],
|
|
421
|
-
[formattableNode.id, formattableNode],
|
|
422
|
-
])
|
|
423
|
-
|
|
424
|
-
const { result } = renderHook(
|
|
425
|
-
() =>
|
|
426
|
-
useTextMarks({
|
|
427
|
-
editables: [textNode],
|
|
428
|
-
formattables: [formattableNode],
|
|
429
|
-
}),
|
|
430
|
-
{ wrapper },
|
|
431
|
-
)
|
|
432
|
-
|
|
433
|
-
// State should be merged - since textNode is empty, only formattable state is used
|
|
434
|
-
expect(result.current.state).not.toBeNull()
|
|
435
|
-
})
|
|
436
|
-
|
|
437
|
-
it("toggle updates both editable and formattable nodes", () => {
|
|
438
|
-
const textNode = createTextNode()
|
|
439
|
-
const formattableNode = createFormattableNode({ italic: false })
|
|
440
|
-
page.nodes = new Map<string, EditableNode | FormattableNode>([
|
|
441
|
-
[textNode.id, textNode],
|
|
442
|
-
[formattableNode.id, formattableNode],
|
|
443
|
-
])
|
|
444
|
-
|
|
445
|
-
// Add content to text node
|
|
446
|
-
textNode.tiptap.commands.setContent({
|
|
447
|
-
type: "doc",
|
|
448
|
-
content: [{ type: "paragraph", content: [{ type: "text", text: "Test" }] }],
|
|
449
|
-
})
|
|
450
|
-
textNode.tiptap.commands.selectAll()
|
|
451
|
-
|
|
452
|
-
const { result } = renderHook(
|
|
453
|
-
() =>
|
|
454
|
-
useTextMarks({
|
|
455
|
-
editables: [textNode],
|
|
456
|
-
formattables: [formattableNode],
|
|
457
|
-
}),
|
|
458
|
-
{ wrapper },
|
|
459
|
-
)
|
|
460
|
-
|
|
461
|
-
act(() => {
|
|
462
|
-
result.current.toggle("Italic")
|
|
463
|
-
})
|
|
464
|
-
|
|
465
|
-
expect(textNode.tiptap.isActive("italic")).toBe(true)
|
|
466
|
-
expect(formattableNode.italic).toBe(true)
|
|
467
|
-
})
|
|
468
|
-
})
|
|
469
|
-
|
|
470
|
-
describe("state merging", () => {
|
|
471
|
-
it("returns null for conflicting boolean values", () => {
|
|
472
|
-
const node1 = createFormattableNode({ bold: true })
|
|
473
|
-
const node2 = createFormattableNode({ bold: false })
|
|
474
|
-
page.nodes = new Map([
|
|
475
|
-
[node1.id, node1],
|
|
476
|
-
[node2.id, node2],
|
|
477
|
-
])
|
|
478
|
-
|
|
479
|
-
const { result } = renderHook(
|
|
480
|
-
() =>
|
|
481
|
-
useTextMarks({
|
|
482
|
-
editables: [],
|
|
483
|
-
formattables: [node1, node2],
|
|
484
|
-
}),
|
|
485
|
-
{ wrapper },
|
|
486
|
-
)
|
|
487
|
-
|
|
488
|
-
// When values differ, merged value becomes null but state.isBold defaults to false
|
|
489
|
-
expect(result.current.state?.isBold).toBe(false)
|
|
490
|
-
})
|
|
491
|
-
|
|
492
|
-
it("returns consistent value when all nodes match", () => {
|
|
493
|
-
const node1 = createFormattableNode({ bold: true, color: "#ff0000" })
|
|
494
|
-
const node2 = createFormattableNode({ bold: true, color: "#ff0000" })
|
|
495
|
-
page.nodes = new Map([
|
|
496
|
-
[node1.id, node1],
|
|
497
|
-
[node2.id, node2],
|
|
498
|
-
])
|
|
499
|
-
|
|
500
|
-
const { result } = renderHook(
|
|
501
|
-
() =>
|
|
502
|
-
useTextMarks({
|
|
503
|
-
editables: [],
|
|
504
|
-
formattables: [node1, node2],
|
|
505
|
-
}),
|
|
506
|
-
{ wrapper },
|
|
507
|
-
)
|
|
508
|
-
|
|
509
|
-
expect(result.current.state?.isBold).toBe(true)
|
|
510
|
-
expect(result.current.state?.color).toBe("#ff0000")
|
|
511
|
-
})
|
|
512
|
-
|
|
513
|
-
it("returns null color for conflicting colors", () => {
|
|
514
|
-
const node1 = createFormattableNode({ color: "#ff0000" })
|
|
515
|
-
const node2 = createFormattableNode({ color: "#00ff00" })
|
|
516
|
-
page.nodes = new Map([
|
|
517
|
-
[node1.id, node1],
|
|
518
|
-
[node2.id, node2],
|
|
519
|
-
])
|
|
520
|
-
|
|
521
|
-
const { result } = renderHook(
|
|
522
|
-
() =>
|
|
523
|
-
useTextMarks({
|
|
524
|
-
editables: [],
|
|
525
|
-
formattables: [node1, node2],
|
|
526
|
-
}),
|
|
527
|
-
{ wrapper },
|
|
528
|
-
)
|
|
529
|
-
|
|
530
|
-
expect(result.current.state?.color).toBeNull()
|
|
531
|
-
})
|
|
532
|
-
})
|
|
533
|
-
|
|
534
|
-
describe("blurNode", () => {
|
|
535
|
-
it("clears the last focused editor reference", () => {
|
|
536
|
-
const node = createTextNode()
|
|
537
|
-
page.nodes = new Map([[node.id, node]])
|
|
538
|
-
|
|
539
|
-
// Focus the editor
|
|
540
|
-
node.tiptap.commands.focus()
|
|
541
|
-
|
|
542
|
-
// Render the hook to establish the focused editor tracking
|
|
543
|
-
renderHook(
|
|
544
|
-
() =>
|
|
545
|
-
useTextMarks({
|
|
546
|
-
editables: [node],
|
|
547
|
-
formattables: [],
|
|
548
|
-
}),
|
|
549
|
-
{ wrapper },
|
|
550
|
-
)
|
|
551
|
-
|
|
552
|
-
// blurNode should clear the reference
|
|
553
|
-
blurNode(node)
|
|
554
|
-
|
|
555
|
-
// This test mainly verifies blurNode doesn't throw
|
|
556
|
-
expect(true).toBe(true)
|
|
557
|
-
})
|
|
558
|
-
})
|
|
559
|
-
|
|
560
|
-
describe("all toggle marks", () => {
|
|
561
|
-
it.each([
|
|
562
|
-
["Bold", "bold"],
|
|
563
|
-
["Italic", "italic"],
|
|
564
|
-
["Underline", "underline"],
|
|
565
|
-
["Strike", "strike"],
|
|
566
|
-
["Superscript", "superscript"],
|
|
567
|
-
["Subscript", "subscript"],
|
|
568
|
-
] as const)("toggle %s updates formattable node %s property", (mark, prop) => {
|
|
569
|
-
const node = createFormattableNode({ [prop]: false })
|
|
570
|
-
page.nodes = new Map([[node.id, node]])
|
|
571
|
-
|
|
572
|
-
const { result } = renderHook(
|
|
573
|
-
() =>
|
|
574
|
-
useTextMarks({
|
|
575
|
-
editables: [],
|
|
576
|
-
formattables: [node],
|
|
577
|
-
}),
|
|
578
|
-
{ wrapper },
|
|
579
|
-
)
|
|
580
|
-
|
|
581
|
-
act(() => {
|
|
582
|
-
result.current.toggle(mark)
|
|
583
|
-
})
|
|
584
|
-
|
|
585
|
-
expect(node[prop]).toBe(true)
|
|
586
|
-
})
|
|
587
|
-
})
|
|
588
|
-
|
|
589
|
-
describe("default values", () => {
|
|
590
|
-
it("uses default size from computed style when none specified", () => {
|
|
591
|
-
const node = createFormattableNode()
|
|
592
|
-
page.nodes = new Map([[node.id, node]])
|
|
593
|
-
|
|
594
|
-
const { result } = renderHook(
|
|
595
|
-
() =>
|
|
596
|
-
useTextMarks({
|
|
597
|
-
editables: [],
|
|
598
|
-
formattables: [node],
|
|
599
|
-
}),
|
|
600
|
-
{ wrapper },
|
|
601
|
-
)
|
|
602
|
-
|
|
603
|
-
// Size should be the node's size (default 16) or computed font size
|
|
604
|
-
expect(result.current.state?.size).toBe(16)
|
|
605
|
-
})
|
|
606
|
-
|
|
607
|
-
it("uses Inter as default font family", () => {
|
|
608
|
-
const node = createFormattableNode({ family: null })
|
|
609
|
-
page.nodes = new Map([[node.id, node]])
|
|
610
|
-
|
|
611
|
-
const { result } = renderHook(
|
|
612
|
-
() =>
|
|
613
|
-
useTextMarks({
|
|
614
|
-
editables: [],
|
|
615
|
-
formattables: [node],
|
|
616
|
-
}),
|
|
617
|
-
{ wrapper },
|
|
618
|
-
)
|
|
619
|
-
|
|
620
|
-
// When all families are null, default is Inter
|
|
621
|
-
expect(result.current.state?.family).toBe("Inter")
|
|
622
|
-
})
|
|
623
|
-
})
|
|
624
|
-
})
|