@fluidframework/react 2.90.0-378676 โ 2.90.0
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/CHANGELOG.md +9 -0
- package/README.md +2 -0
- package/lib/index.d.ts +2 -0
- package/lib/index.d.ts.map +1 -1
- package/lib/index.js +2 -0
- package/lib/index.js.map +1 -1
- package/lib/propNode.js.map +1 -1
- package/lib/test/mochaHooks.js +13 -0
- package/lib/test/mochaHooks.js.map +1 -0
- package/lib/test/text/plainUtils.test.js +75 -0
- package/lib/test/text/plainUtils.test.js.map +1 -0
- package/lib/test/text/textEditor.test.js +704 -0
- package/lib/test/text/textEditor.test.js.map +1 -0
- package/lib/test/undoRedo.test.js +62 -0
- package/lib/test/undoRedo.test.js.map +1 -0
- package/lib/test/useObservation.spec.js +0 -1
- package/lib/test/useObservation.spec.js.map +1 -1
- package/lib/test/useTree.spec.js +0 -1
- package/lib/test/useTree.spec.js.map +1 -1
- package/lib/text/formatted/index.d.ts +6 -0
- package/lib/text/formatted/index.d.ts.map +1 -0
- package/lib/text/formatted/index.js +6 -0
- package/lib/text/formatted/index.js.map +1 -0
- package/lib/text/formatted/quillFormattedView.d.ts +54 -0
- package/lib/text/formatted/quillFormattedView.d.ts.map +1 -0
- package/lib/text/formatted/quillFormattedView.js +426 -0
- package/lib/text/formatted/quillFormattedView.js.map +1 -0
- package/lib/text/index.d.ts +7 -0
- package/lib/text/index.d.ts.map +1 -0
- package/lib/text/index.js +7 -0
- package/lib/text/index.js.map +1 -0
- package/lib/text/plain/index.d.ts +7 -0
- package/lib/text/plain/index.d.ts.map +1 -0
- package/lib/text/plain/index.js +7 -0
- package/lib/text/plain/index.js.map +1 -0
- package/lib/text/plain/plainTextView.d.ts +14 -0
- package/lib/text/plain/plainTextView.d.ts.map +1 -0
- package/lib/text/plain/plainTextView.js +75 -0
- package/lib/text/plain/plainTextView.js.map +1 -0
- package/lib/text/plain/plainUtils.d.ts +23 -0
- package/lib/text/plain/plainUtils.d.ts.map +1 -0
- package/lib/text/plain/plainUtils.js +51 -0
- package/lib/text/plain/plainUtils.js.map +1 -0
- package/lib/text/plain/quillView.d.ts +22 -0
- package/lib/text/plain/quillView.d.ts.map +1 -0
- package/lib/text/plain/quillView.js +112 -0
- package/lib/text/plain/quillView.js.map +1 -0
- package/lib/undoRedo.d.ts +51 -0
- package/lib/undoRedo.d.ts.map +1 -0
- package/lib/undoRedo.js +76 -0
- package/lib/undoRedo.js.map +1 -0
- package/package.json +26 -45
- package/react.test-files.tar +0 -0
- package/src/index.ts +10 -0
- package/src/propNode.ts +1 -1
- package/src/text/formatted/index.ts +11 -0
- package/src/text/formatted/quillFormattedView.tsx +509 -0
- package/src/text/index.ts +15 -0
- package/src/text/plain/index.ts +7 -0
- package/src/text/plain/plainTextView.tsx +110 -0
- package/src/text/plain/plainUtils.ts +68 -0
- package/src/text/plain/quillView.tsx +149 -0
- package/src/undoRedo.ts +117 -0
- package/tsconfig.json +6 -0
- package/api-extractor/api-extractor-lint-alpha.cjs.json +0 -5
- package/api-extractor/api-extractor-lint-beta.cjs.json +0 -5
- package/api-extractor/api-extractor-lint-public.cjs.json +0 -5
- package/dist/alpha.d.ts +0 -45
- package/dist/beta.d.ts +0 -15
- package/dist/index.d.ts +0 -16
- package/dist/index.d.ts.map +0 -1
- package/dist/index.js +0 -26
- package/dist/index.js.map +0 -1
- package/dist/package.json +0 -4
- package/dist/propNode.d.ts +0 -114
- package/dist/propNode.d.ts.map +0 -1
- package/dist/propNode.js +0 -43
- package/dist/propNode.js.map +0 -1
- package/dist/public.d.ts +0 -15
- package/dist/reactSharedTreeView.d.ts +0 -119
- package/dist/reactSharedTreeView.d.ts.map +0 -1
- package/dist/reactSharedTreeView.js +0 -206
- package/dist/reactSharedTreeView.js.map +0 -1
- package/dist/simpleIdentifier.d.ts +0 -19
- package/dist/simpleIdentifier.d.ts.map +0 -1
- package/dist/simpleIdentifier.js +0 -33
- package/dist/simpleIdentifier.js.map +0 -1
- package/dist/useObservation.d.ts +0 -83
- package/dist/useObservation.d.ts.map +0 -1
- package/dist/useObservation.js +0 -295
- package/dist/useObservation.js.map +0 -1
- package/dist/useTree.d.ts +0 -80
- package/dist/useTree.d.ts.map +0 -1
- package/dist/useTree.js +0 -137
- package/dist/useTree.js.map +0 -1
- package/tsconfig.cjs.json +0 -7
|
@@ -0,0 +1,704 @@
|
|
|
1
|
+
/*!
|
|
2
|
+
* Copyright (c) Microsoft Corporation and contributors. All rights reserved.
|
|
3
|
+
* Licensed under the MIT License.
|
|
4
|
+
*/
|
|
5
|
+
import { strict as assert } from "node:assert";
|
|
6
|
+
import { TreeViewConfiguration } from "@fluidframework/tree";
|
|
7
|
+
import { TreeAlpha } from "@fluidframework/tree/alpha";
|
|
8
|
+
import { independentView, TextAsTree } from "@fluidframework/tree/internal";
|
|
9
|
+
import { render } from "@testing-library/react";
|
|
10
|
+
import globalJsdom from "global-jsdom";
|
|
11
|
+
import DeltaPackage from "quill-delta";
|
|
12
|
+
import * as React from "react";
|
|
13
|
+
import { toPropTreeNode } from "../../propNode.js";
|
|
14
|
+
import { clipboardFormatMatcher, FormattedTextAsTree, FormattedMainView, parseCssFontFamily, parseCssFontSize,
|
|
15
|
+
// Allow import of files being tested
|
|
16
|
+
// eslint-disable-next-line import-x/no-internal-modules
|
|
17
|
+
} from "../../text/formatted/quillFormattedView.js";
|
|
18
|
+
import { PlainTextMainView, QuillMainView,
|
|
19
|
+
// Allow import of files being tested
|
|
20
|
+
// eslint-disable-next-line import-x/no-internal-modules
|
|
21
|
+
} from "../../text/plain/index.js";
|
|
22
|
+
import { UndoRedoStacks } from "../../undoRedo.js";
|
|
23
|
+
const Delta = DeltaPackage.default;
|
|
24
|
+
// Configuration for creating formatted text views
|
|
25
|
+
const formattedTreeConfig = new TreeViewConfiguration({ schema: FormattedTextAsTree.Tree });
|
|
26
|
+
/**
|
|
27
|
+
* Creates a TreeView for formatted text, initialized with the provided initial value.
|
|
28
|
+
*/
|
|
29
|
+
function createFormattedTreeView(initialValue = "") {
|
|
30
|
+
const treeView = independentView(formattedTreeConfig);
|
|
31
|
+
treeView.initialize(FormattedTextAsTree.Tree.fromString(initialValue));
|
|
32
|
+
return { tree: treeView.root };
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Creates a TreeView for formatted text with events access (needed for undo/redo tests).
|
|
36
|
+
*/
|
|
37
|
+
function createFormattedTreeViewWithEvents(initialValue = "") {
|
|
38
|
+
const treeView = independentView(formattedTreeConfig);
|
|
39
|
+
treeView.initialize(FormattedTextAsTree.Tree.fromString(initialValue));
|
|
40
|
+
return treeView;
|
|
41
|
+
}
|
|
42
|
+
const views = [
|
|
43
|
+
{ name: "Quill", component: QuillMainView },
|
|
44
|
+
{ name: "Plain TextArea", component: PlainTextMainView },
|
|
45
|
+
];
|
|
46
|
+
// TODO add collaboration tests when rich formatting is supported using TestContainerRuntimeFactory from
|
|
47
|
+
// @fluidframework/test-utils to test rich formatting data sync between multiple collaborators
|
|
48
|
+
describe("textEditor", () => {
|
|
49
|
+
// Note: JSDOM is initialized once in mochaHooks.ts before Quill is imported,
|
|
50
|
+
// since Quill requires document at import time. See src/test/mochaHooks.ts.
|
|
51
|
+
// These tests reset up a clean DOM.
|
|
52
|
+
let cleanup;
|
|
53
|
+
// TODO: why does making this beforeEach/afterEach instead of before/after cause cleanup to crash?
|
|
54
|
+
// It seems like each test should be able to have its own clean DOM.
|
|
55
|
+
before(() => {
|
|
56
|
+
cleanup = globalJsdom();
|
|
57
|
+
});
|
|
58
|
+
after(() => {
|
|
59
|
+
cleanup();
|
|
60
|
+
});
|
|
61
|
+
// Loop through all registered views
|
|
62
|
+
for (const view of views) {
|
|
63
|
+
describe(`${view.name} view`, () => {
|
|
64
|
+
describe("dom tests", () => {
|
|
65
|
+
// Run without strict mode to make sure it works in a normal production setup.
|
|
66
|
+
// Run with strict mode to potentially detect additional issues.
|
|
67
|
+
for (const reactStrictMode of [false, true]) {
|
|
68
|
+
describe(`StrictMode: ${reactStrictMode}`, () => {
|
|
69
|
+
const ViewComponent = view.component;
|
|
70
|
+
it("renders MainView with editor container", () => {
|
|
71
|
+
const text = TextAsTree.Tree.fromString("");
|
|
72
|
+
const content = React.createElement(ViewComponent, { root: toPropTreeNode(text) });
|
|
73
|
+
const rendered = render(content, { reactStrictMode });
|
|
74
|
+
assert.match(rendered.baseElement.textContent ?? "", /Collaborative Text Editor/);
|
|
75
|
+
});
|
|
76
|
+
it("renders MainView with initial text content", () => {
|
|
77
|
+
const text = TextAsTree.Tree.fromString("Hello World");
|
|
78
|
+
const content = React.createElement(ViewComponent, { root: toPropTreeNode(text) });
|
|
79
|
+
const rendered = render(content, { reactStrictMode });
|
|
80
|
+
assert.match(rendered.baseElement.textContent ?? "", /Hello World/);
|
|
81
|
+
});
|
|
82
|
+
it("invalidates view when tree is mutated", () => {
|
|
83
|
+
const text = TextAsTree.Tree.fromString("Hello");
|
|
84
|
+
const content = React.createElement(ViewComponent, { root: toPropTreeNode(text) });
|
|
85
|
+
const rendered = render(content, { reactStrictMode });
|
|
86
|
+
// Mutate the tree by inserting text
|
|
87
|
+
text.insertAt(5, " World");
|
|
88
|
+
// Rerender and verify the view updates
|
|
89
|
+
rendered.rerender(content);
|
|
90
|
+
assert.match(rendered.baseElement.textContent ?? "", /Hello World/);
|
|
91
|
+
});
|
|
92
|
+
it("invalidates view when text is removed", () => {
|
|
93
|
+
const text = TextAsTree.Tree.fromString("Hello World");
|
|
94
|
+
const content = React.createElement(ViewComponent, { root: toPropTreeNode(text) });
|
|
95
|
+
const rendered = render(content, { reactStrictMode });
|
|
96
|
+
// Mutate the tree by removing " World" (indices 5 to 11)
|
|
97
|
+
text.removeRange(5, 11);
|
|
98
|
+
// Rerender and verify the view updates
|
|
99
|
+
rendered.rerender(content);
|
|
100
|
+
assert.match(rendered.baseElement.textContent ?? "", /Hello/);
|
|
101
|
+
assert(rendered.baseElement.textContent !== null);
|
|
102
|
+
assert.doesNotMatch(rendered.baseElement.textContent, /World/);
|
|
103
|
+
});
|
|
104
|
+
it("invalidates view when text is cleared and replaced", () => {
|
|
105
|
+
const text = TextAsTree.Tree.fromString("Original");
|
|
106
|
+
const content = React.createElement(ViewComponent, { root: toPropTreeNode(text) });
|
|
107
|
+
const rendered = render(content, { reactStrictMode });
|
|
108
|
+
// Clear all text
|
|
109
|
+
const length = [...text.characters()].length;
|
|
110
|
+
text.removeRange(0, length);
|
|
111
|
+
// Insert new text
|
|
112
|
+
text.insertAt(0, "Replaced");
|
|
113
|
+
// Rerender and verify the view updates
|
|
114
|
+
rendered.rerender(content);
|
|
115
|
+
assert.match(rendered.baseElement.textContent ?? "", /Replaced/);
|
|
116
|
+
assert(rendered.baseElement.textContent !== null);
|
|
117
|
+
assert.doesNotMatch(rendered.baseElement.textContent, /Original/);
|
|
118
|
+
});
|
|
119
|
+
// Tests for surrogate pair characters (emojis use 2 UTF-16 code units)
|
|
120
|
+
// These verify correct handling where editor indexing may differ from iteration.
|
|
121
|
+
it("renders MainView with surrogate pair characters", () => {
|
|
122
|
+
// ๐ is a surrogate pair: "๐".length === 2, but [..."๐"].length === 1
|
|
123
|
+
const text = TextAsTree.Tree.fromString("Hello ๐ World");
|
|
124
|
+
const content = React.createElement(ViewComponent, { root: toPropTreeNode(text) });
|
|
125
|
+
const rendered = render(content, { reactStrictMode });
|
|
126
|
+
assert.match(rendered.baseElement.textContent ?? "", /Hello ๐ World/);
|
|
127
|
+
});
|
|
128
|
+
it("inserts text after surrogate pair characters", () => {
|
|
129
|
+
const text = TextAsTree.Tree.fromString("A๐B");
|
|
130
|
+
const content = React.createElement(ViewComponent, { root: toPropTreeNode(text) });
|
|
131
|
+
const rendered = render(content, { reactStrictMode });
|
|
132
|
+
// Insert after the emoji (index 2 in character count: A, ๐, B)
|
|
133
|
+
text.insertAt(2, "X");
|
|
134
|
+
rendered.rerender(content);
|
|
135
|
+
assert.match(rendered.baseElement.textContent ?? "", /A๐XB/);
|
|
136
|
+
});
|
|
137
|
+
it("removes surrogate pair characters", () => {
|
|
138
|
+
const text = TextAsTree.Tree.fromString("A๐B");
|
|
139
|
+
const content = React.createElement(ViewComponent, { root: toPropTreeNode(text) });
|
|
140
|
+
const rendered = render(content, { reactStrictMode });
|
|
141
|
+
// Remove the emoji (index 1, length 1 in character count)
|
|
142
|
+
text.removeRange(1, 2);
|
|
143
|
+
rendered.rerender(content);
|
|
144
|
+
assert.match(rendered.baseElement.textContent ?? "", /AB/);
|
|
145
|
+
assert(rendered.baseElement.textContent !== null);
|
|
146
|
+
assert.doesNotMatch(rendered.baseElement.textContent, /๐/);
|
|
147
|
+
});
|
|
148
|
+
it("handles multiple surrogate pair characters", () => {
|
|
149
|
+
const text = TextAsTree.Tree.fromString("๐๐๐");
|
|
150
|
+
const content = React.createElement(ViewComponent, { root: toPropTreeNode(text) });
|
|
151
|
+
const rendered = render(content, { reactStrictMode });
|
|
152
|
+
// Insert between emojis
|
|
153
|
+
text.insertAt(2, "!");
|
|
154
|
+
rendered.rerender(content);
|
|
155
|
+
assert.match(rendered.baseElement.textContent ?? "", /๐๐!๐/);
|
|
156
|
+
});
|
|
157
|
+
});
|
|
158
|
+
}
|
|
159
|
+
});
|
|
160
|
+
});
|
|
161
|
+
}
|
|
162
|
+
// Formatted text view tests - Initial view rendering (matching plain text test structure)
|
|
163
|
+
describe("Formatted Quill view", () => {
|
|
164
|
+
describe("dom tests", () => {
|
|
165
|
+
for (const reactStrictMode of [false, true]) {
|
|
166
|
+
describe(`StrictMode: ${reactStrictMode}`, () => {
|
|
167
|
+
it("renders FormattedMainView with editor container", () => {
|
|
168
|
+
const { tree } = createFormattedTreeView();
|
|
169
|
+
const content = React.createElement(FormattedMainView, { root: toPropTreeNode(tree) });
|
|
170
|
+
const rendered = render(content, { reactStrictMode });
|
|
171
|
+
assert.match(rendered.baseElement.textContent ?? "", /Collaborative Formatted Text Editor/);
|
|
172
|
+
});
|
|
173
|
+
it("renders FormattedMainView with initial text content", () => {
|
|
174
|
+
const { tree } = createFormattedTreeView("Hello World");
|
|
175
|
+
const content = React.createElement(FormattedMainView, { root: toPropTreeNode(tree) });
|
|
176
|
+
const rendered = render(content, { reactStrictMode });
|
|
177
|
+
assert.match(rendered.baseElement.textContent ?? "", /Hello World/);
|
|
178
|
+
});
|
|
179
|
+
it("invalidates view when tree is mutated", () => {
|
|
180
|
+
const { tree: text } = createFormattedTreeView("Hello");
|
|
181
|
+
const content = React.createElement(FormattedMainView, { root: toPropTreeNode(text) });
|
|
182
|
+
const rendered = render(content, { reactStrictMode });
|
|
183
|
+
// Mutate the tree by inserting text
|
|
184
|
+
text.insertAt(5, " World");
|
|
185
|
+
// Rerender and verify the view updates
|
|
186
|
+
rendered.rerender(content);
|
|
187
|
+
assert.match(rendered.baseElement.textContent ?? "", /Hello World/);
|
|
188
|
+
});
|
|
189
|
+
it("invalidates view when text is removed", () => {
|
|
190
|
+
const { tree: text } = createFormattedTreeView("Hello World");
|
|
191
|
+
const content = React.createElement(FormattedMainView, { root: toPropTreeNode(text) });
|
|
192
|
+
const rendered = render(content, { reactStrictMode });
|
|
193
|
+
// Mutate the tree by removing " World" (indices 5 to 11)
|
|
194
|
+
text.removeRange(5, 11);
|
|
195
|
+
// Rerender and verify the view updates
|
|
196
|
+
rendered.rerender(content);
|
|
197
|
+
assert.match(rendered.baseElement.textContent ?? "", /Hello/);
|
|
198
|
+
assert(rendered.baseElement.textContent !== null);
|
|
199
|
+
assert.doesNotMatch(rendered.baseElement.textContent, /World/);
|
|
200
|
+
});
|
|
201
|
+
it("invalidates view when text is cleared and replaced", () => {
|
|
202
|
+
const { tree: text } = createFormattedTreeView("Original");
|
|
203
|
+
const content = React.createElement(FormattedMainView, { root: toPropTreeNode(text) });
|
|
204
|
+
const rendered = render(content, { reactStrictMode });
|
|
205
|
+
// Clear all text
|
|
206
|
+
const length = [...text.characters()].length;
|
|
207
|
+
text.removeRange(0, length);
|
|
208
|
+
// Insert new text
|
|
209
|
+
text.insertAt(0, "Replaced");
|
|
210
|
+
// Rerender and verify the view updates
|
|
211
|
+
rendered.rerender(content);
|
|
212
|
+
assert.match(rendered.baseElement.textContent ?? "", /Replaced/);
|
|
213
|
+
assert(rendered.baseElement.textContent !== null);
|
|
214
|
+
assert.doesNotMatch(rendered.baseElement.textContent, /Original/);
|
|
215
|
+
});
|
|
216
|
+
// Tests for surrogate pair characters (emojis use 2 UTF-16 code units)
|
|
217
|
+
// These verify correct handling where editor indexing may differ from iteration.
|
|
218
|
+
it("renders FormattedMainView with surrogate pair characters", () => {
|
|
219
|
+
// ๐ is a surrogate pair: "๐".length === 2, but [..."๐"].length === 1
|
|
220
|
+
const { tree: text } = createFormattedTreeView("Hello ๐ World");
|
|
221
|
+
const content = React.createElement(FormattedMainView, { root: toPropTreeNode(text) });
|
|
222
|
+
const rendered = render(content, { reactStrictMode });
|
|
223
|
+
assert.match(rendered.baseElement.textContent ?? "", /Hello ๐ World/);
|
|
224
|
+
});
|
|
225
|
+
it("inserts text after surrogate pair characters", () => {
|
|
226
|
+
const { tree: text } = createFormattedTreeView("A๐B");
|
|
227
|
+
const content = React.createElement(FormattedMainView, { root: toPropTreeNode(text) });
|
|
228
|
+
const rendered = render(content, { reactStrictMode });
|
|
229
|
+
// Insert after the emoji (index 2 in character count: A, ๐, B)
|
|
230
|
+
text.insertAt(2, "X");
|
|
231
|
+
rendered.rerender(content);
|
|
232
|
+
assert.match(rendered.baseElement.textContent ?? "", /A๐XB/);
|
|
233
|
+
});
|
|
234
|
+
it("removes surrogate pair characters", () => {
|
|
235
|
+
const { tree: text } = createFormattedTreeView("A๐B");
|
|
236
|
+
const content = React.createElement(FormattedMainView, { root: toPropTreeNode(text) });
|
|
237
|
+
const rendered = render(content, { reactStrictMode });
|
|
238
|
+
// Remove the emoji (index 1, length 1 in character count)
|
|
239
|
+
text.removeRange(1, 2);
|
|
240
|
+
rendered.rerender(content);
|
|
241
|
+
assert.match(rendered.baseElement.textContent ?? "", /AB/);
|
|
242
|
+
assert(rendered.baseElement.textContent !== null);
|
|
243
|
+
assert.doesNotMatch(rendered.baseElement.textContent, /๐/);
|
|
244
|
+
});
|
|
245
|
+
it("handles multiple surrogate pair characters", () => {
|
|
246
|
+
const { tree: text } = createFormattedTreeView("๐๐๐");
|
|
247
|
+
const content = React.createElement(FormattedMainView, { root: toPropTreeNode(text) });
|
|
248
|
+
const rendered = render(content, { reactStrictMode });
|
|
249
|
+
// Insert between emojis
|
|
250
|
+
text.insertAt(2, "!");
|
|
251
|
+
rendered.rerender(content);
|
|
252
|
+
assert.match(rendered.baseElement.textContent ?? "", /๐๐!๐/);
|
|
253
|
+
});
|
|
254
|
+
});
|
|
255
|
+
}
|
|
256
|
+
});
|
|
257
|
+
// Helper to create default format
|
|
258
|
+
function createPlainFormat() {
|
|
259
|
+
return new FormattedTextAsTree.CharacterFormat({
|
|
260
|
+
bold: false,
|
|
261
|
+
italic: false,
|
|
262
|
+
underline: false,
|
|
263
|
+
size: 12,
|
|
264
|
+
font: "Arial",
|
|
265
|
+
});
|
|
266
|
+
}
|
|
267
|
+
// Essential tests for character attributes
|
|
268
|
+
// Each attribute needs: insert, delete, and formatRange tests
|
|
269
|
+
describe("character attribute tests", () => {
|
|
270
|
+
for (const reactStrictMode of [false, true]) {
|
|
271
|
+
describe(`StrictMode: ${reactStrictMode}`, () => {
|
|
272
|
+
it("delete on empty string does not throw", () => {
|
|
273
|
+
const { tree: text } = createFormattedTreeView();
|
|
274
|
+
const content = React.createElement(FormattedMainView, { root: toPropTreeNode(text) });
|
|
275
|
+
const rendered = render(content, { reactStrictMode });
|
|
276
|
+
assert.doesNotThrow(() => {
|
|
277
|
+
text.removeRange(0, 0);
|
|
278
|
+
rendered.rerender(content);
|
|
279
|
+
});
|
|
280
|
+
});
|
|
281
|
+
describe("bold", () => {
|
|
282
|
+
it("inserts bold text and renders with <strong> tag", () => {
|
|
283
|
+
const { tree: text } = createFormattedTreeView("Hello");
|
|
284
|
+
const content = React.createElement(FormattedMainView, { root: toPropTreeNode(text) });
|
|
285
|
+
const rendered = render(content, { reactStrictMode });
|
|
286
|
+
assert.ok(!rendered.container.querySelector("strong"), "Initially: no <strong>");
|
|
287
|
+
text.defaultFormat = new FormattedTextAsTree.CharacterFormat({
|
|
288
|
+
bold: true,
|
|
289
|
+
italic: false,
|
|
290
|
+
underline: false,
|
|
291
|
+
size: 12,
|
|
292
|
+
font: "Arial",
|
|
293
|
+
});
|
|
294
|
+
text.insertAt(2, "BOLD");
|
|
295
|
+
rendered.rerender(content);
|
|
296
|
+
const el = rendered.container.querySelector("strong");
|
|
297
|
+
assert.ok(el, "Expected <strong> tag");
|
|
298
|
+
assert.match(el.textContent ?? "", /BOLD/);
|
|
299
|
+
});
|
|
300
|
+
it("deletes bold text and removes <strong> tag", () => {
|
|
301
|
+
const { tree: text } = createFormattedTreeView();
|
|
302
|
+
text.defaultFormat = new FormattedTextAsTree.CharacterFormat({
|
|
303
|
+
bold: true,
|
|
304
|
+
italic: false,
|
|
305
|
+
underline: false,
|
|
306
|
+
size: 12,
|
|
307
|
+
font: "Arial",
|
|
308
|
+
});
|
|
309
|
+
text.insertAt(0, "BOLD");
|
|
310
|
+
text.defaultFormat = createPlainFormat();
|
|
311
|
+
text.insertAt(4, "plain");
|
|
312
|
+
const content = React.createElement(FormattedMainView, { root: toPropTreeNode(text) });
|
|
313
|
+
const rendered = render(content, { reactStrictMode });
|
|
314
|
+
assert.ok(rendered.container.querySelector("strong"), "Initially: has <strong>");
|
|
315
|
+
text.removeRange(0, 4);
|
|
316
|
+
rendered.rerender(content);
|
|
317
|
+
assert.ok(!rendered.container.querySelector("strong"), "After delete: no <strong>");
|
|
318
|
+
});
|
|
319
|
+
it("applies bold via formatRange", () => {
|
|
320
|
+
const { tree: text } = createFormattedTreeView("Hello World");
|
|
321
|
+
const content = React.createElement(FormattedMainView, { root: toPropTreeNode(text) });
|
|
322
|
+
const rendered = render(content, { reactStrictMode });
|
|
323
|
+
text.formatRange(6, 11, { bold: true });
|
|
324
|
+
rendered.rerender(content);
|
|
325
|
+
const el = rendered.container.querySelector("strong");
|
|
326
|
+
assert.ok(el, "Expected <strong> after formatRange");
|
|
327
|
+
assert.match(el.textContent ?? "", /World/);
|
|
328
|
+
});
|
|
329
|
+
});
|
|
330
|
+
describe("italic", () => {
|
|
331
|
+
it("inserts italic text and renders with <em> tag", () => {
|
|
332
|
+
const { tree: text } = createFormattedTreeView("Hello");
|
|
333
|
+
const content = React.createElement(FormattedMainView, { root: toPropTreeNode(text) });
|
|
334
|
+
const rendered = render(content, { reactStrictMode });
|
|
335
|
+
assert.ok(!rendered.container.querySelector("em"), "Initially: no <em>");
|
|
336
|
+
text.defaultFormat = new FormattedTextAsTree.CharacterFormat({
|
|
337
|
+
bold: false,
|
|
338
|
+
italic: true,
|
|
339
|
+
underline: false,
|
|
340
|
+
size: 12,
|
|
341
|
+
font: "Arial",
|
|
342
|
+
});
|
|
343
|
+
text.insertAt(2, "ITAL");
|
|
344
|
+
rendered.rerender(content);
|
|
345
|
+
const el = rendered.container.querySelector("em");
|
|
346
|
+
assert.ok(el, "Expected <em> tag");
|
|
347
|
+
assert.match(el.textContent ?? "", /ITAL/);
|
|
348
|
+
});
|
|
349
|
+
it("deletes italic text and removes <em> tag", () => {
|
|
350
|
+
const { tree: text } = createFormattedTreeView();
|
|
351
|
+
text.defaultFormat = new FormattedTextAsTree.CharacterFormat({
|
|
352
|
+
bold: false,
|
|
353
|
+
italic: true,
|
|
354
|
+
underline: false,
|
|
355
|
+
size: 12,
|
|
356
|
+
font: "Arial",
|
|
357
|
+
});
|
|
358
|
+
text.insertAt(0, "ITAL");
|
|
359
|
+
text.defaultFormat = createPlainFormat();
|
|
360
|
+
text.insertAt(4, "plain");
|
|
361
|
+
const content = React.createElement(FormattedMainView, { root: toPropTreeNode(text) });
|
|
362
|
+
const rendered = render(content, { reactStrictMode });
|
|
363
|
+
assert.ok(rendered.container.querySelector("em"), "Initially: has <em>");
|
|
364
|
+
text.removeRange(0, 4);
|
|
365
|
+
rendered.rerender(content);
|
|
366
|
+
assert.ok(!rendered.container.querySelector("em"), "After delete: no <em>");
|
|
367
|
+
});
|
|
368
|
+
it("applies italic via formatRange", () => {
|
|
369
|
+
const { tree: text } = createFormattedTreeView("Hello World");
|
|
370
|
+
const content = React.createElement(FormattedMainView, { root: toPropTreeNode(text) });
|
|
371
|
+
const rendered = render(content, { reactStrictMode });
|
|
372
|
+
text.formatRange(6, 11, { italic: true });
|
|
373
|
+
rendered.rerender(content);
|
|
374
|
+
const el = rendered.container.querySelector("em");
|
|
375
|
+
assert.ok(el, "Expected <em> after formatRange");
|
|
376
|
+
assert.match(el.textContent ?? "", /World/);
|
|
377
|
+
});
|
|
378
|
+
});
|
|
379
|
+
describe("underline", () => {
|
|
380
|
+
it("inserts underlined text and renders with <u> tag", () => {
|
|
381
|
+
const { tree: text } = createFormattedTreeView("Hello");
|
|
382
|
+
const content = React.createElement(FormattedMainView, { root: toPropTreeNode(text) });
|
|
383
|
+
const rendered = render(content, { reactStrictMode });
|
|
384
|
+
assert.ok(!rendered.container.querySelector("u"), "Initially: no <u>");
|
|
385
|
+
text.defaultFormat = new FormattedTextAsTree.CharacterFormat({
|
|
386
|
+
bold: false,
|
|
387
|
+
italic: false,
|
|
388
|
+
underline: true,
|
|
389
|
+
size: 12,
|
|
390
|
+
font: "Arial",
|
|
391
|
+
});
|
|
392
|
+
text.insertAt(2, "UNDER");
|
|
393
|
+
rendered.rerender(content);
|
|
394
|
+
const el = rendered.container.querySelector("u");
|
|
395
|
+
assert.ok(el, "Expected <u> tag");
|
|
396
|
+
assert.match(el.textContent ?? "", /UNDER/);
|
|
397
|
+
});
|
|
398
|
+
it("deletes underlined text and removes <u> tag", () => {
|
|
399
|
+
const { tree: text } = createFormattedTreeView();
|
|
400
|
+
text.defaultFormat = new FormattedTextAsTree.CharacterFormat({
|
|
401
|
+
bold: false,
|
|
402
|
+
italic: false,
|
|
403
|
+
underline: true,
|
|
404
|
+
size: 12,
|
|
405
|
+
font: "Arial",
|
|
406
|
+
});
|
|
407
|
+
text.insertAt(0, "UNDER");
|
|
408
|
+
text.defaultFormat = createPlainFormat();
|
|
409
|
+
text.insertAt(5, "plain");
|
|
410
|
+
const content = React.createElement(FormattedMainView, { root: toPropTreeNode(text) });
|
|
411
|
+
const rendered = render(content, { reactStrictMode });
|
|
412
|
+
assert.ok(rendered.container.querySelector("u"), "Initially: has <u>");
|
|
413
|
+
text.removeRange(0, 5);
|
|
414
|
+
rendered.rerender(content);
|
|
415
|
+
assert.ok(!rendered.container.querySelector("u"), "After delete: no <u>");
|
|
416
|
+
});
|
|
417
|
+
it("applies underline via formatRange", () => {
|
|
418
|
+
const { tree: text } = createFormattedTreeView("Hello World");
|
|
419
|
+
const content = React.createElement(FormattedMainView, { root: toPropTreeNode(text) });
|
|
420
|
+
const rendered = render(content, { reactStrictMode });
|
|
421
|
+
text.formatRange(6, 11, { underline: true });
|
|
422
|
+
rendered.rerender(content);
|
|
423
|
+
const el = rendered.container.querySelector("u");
|
|
424
|
+
assert.ok(el, "Expected <u> after formatRange");
|
|
425
|
+
assert.match(el.textContent ?? "", /World/);
|
|
426
|
+
});
|
|
427
|
+
});
|
|
428
|
+
describe("size", () => {
|
|
429
|
+
it("inserts huge size text and renders with .ql-size-huge", () => {
|
|
430
|
+
const { tree: text } = createFormattedTreeView("Hello");
|
|
431
|
+
const content = React.createElement(FormattedMainView, { root: toPropTreeNode(text) });
|
|
432
|
+
const rendered = render(content, { reactStrictMode });
|
|
433
|
+
assert.ok(!rendered.container.querySelector(".ql-size-huge"), "Initially: no .ql-size-huge");
|
|
434
|
+
text.defaultFormat = new FormattedTextAsTree.CharacterFormat({
|
|
435
|
+
bold: false,
|
|
436
|
+
italic: false,
|
|
437
|
+
underline: false,
|
|
438
|
+
size: 24,
|
|
439
|
+
font: "Arial",
|
|
440
|
+
});
|
|
441
|
+
text.insertAt(2, "HUGE");
|
|
442
|
+
rendered.rerender(content);
|
|
443
|
+
const el = rendered.container.querySelector(".ql-size-huge");
|
|
444
|
+
assert.ok(el, "Expected .ql-size-huge");
|
|
445
|
+
assert.match(el.textContent ?? "", /HUGE/);
|
|
446
|
+
});
|
|
447
|
+
it("deletes huge size text and removes .ql-size-huge", () => {
|
|
448
|
+
const { tree: text } = createFormattedTreeView();
|
|
449
|
+
text.defaultFormat = new FormattedTextAsTree.CharacterFormat({
|
|
450
|
+
bold: false,
|
|
451
|
+
italic: false,
|
|
452
|
+
underline: false,
|
|
453
|
+
size: 24,
|
|
454
|
+
font: "Arial",
|
|
455
|
+
});
|
|
456
|
+
text.insertAt(0, "HUGE");
|
|
457
|
+
text.defaultFormat = createPlainFormat();
|
|
458
|
+
text.insertAt(4, "plain");
|
|
459
|
+
const content = React.createElement(FormattedMainView, { root: toPropTreeNode(text) });
|
|
460
|
+
const rendered = render(content, { reactStrictMode });
|
|
461
|
+
assert.ok(rendered.container.querySelector(".ql-size-huge"), "Initially: has .ql-size-huge");
|
|
462
|
+
text.removeRange(0, 4);
|
|
463
|
+
rendered.rerender(content);
|
|
464
|
+
assert.ok(!rendered.container.querySelector(".ql-size-huge"), "After delete: no .ql-size-huge");
|
|
465
|
+
});
|
|
466
|
+
it("applies size via formatRange", () => {
|
|
467
|
+
const { tree: text } = createFormattedTreeView("Hello World");
|
|
468
|
+
const content = React.createElement(FormattedMainView, { root: toPropTreeNode(text) });
|
|
469
|
+
const rendered = render(content, { reactStrictMode });
|
|
470
|
+
text.formatRange(6, 11, { size: 24 });
|
|
471
|
+
rendered.rerender(content);
|
|
472
|
+
const el = rendered.container.querySelector(".ql-size-huge");
|
|
473
|
+
assert.ok(el, "Expected .ql-size-huge after formatRange");
|
|
474
|
+
assert.match(el.textContent ?? "", /World/);
|
|
475
|
+
});
|
|
476
|
+
});
|
|
477
|
+
describe("font", () => {
|
|
478
|
+
it("inserts monospace font text and renders with .ql-font-monospace", () => {
|
|
479
|
+
const { tree: text } = createFormattedTreeView("Hello");
|
|
480
|
+
const content = React.createElement(FormattedMainView, { root: toPropTreeNode(text) });
|
|
481
|
+
const rendered = render(content, { reactStrictMode });
|
|
482
|
+
assert.ok(!rendered.container.querySelector(".ql-font-monospace"), "Initially: no .ql-font-monospace");
|
|
483
|
+
text.defaultFormat = new FormattedTextAsTree.CharacterFormat({
|
|
484
|
+
bold: false,
|
|
485
|
+
italic: false,
|
|
486
|
+
underline: false,
|
|
487
|
+
size: 12,
|
|
488
|
+
font: "monospace",
|
|
489
|
+
});
|
|
490
|
+
text.insertAt(2, "MONO");
|
|
491
|
+
rendered.rerender(content);
|
|
492
|
+
const el = rendered.container.querySelector(".ql-font-monospace");
|
|
493
|
+
assert.ok(el, "Expected .ql-font-monospace");
|
|
494
|
+
assert.match(el.textContent ?? "", /MONO/);
|
|
495
|
+
});
|
|
496
|
+
it("deletes monospace font text and removes .ql-font-monospace", () => {
|
|
497
|
+
const { tree: text } = createFormattedTreeView();
|
|
498
|
+
text.defaultFormat = new FormattedTextAsTree.CharacterFormat({
|
|
499
|
+
bold: false,
|
|
500
|
+
italic: false,
|
|
501
|
+
underline: false,
|
|
502
|
+
size: 12,
|
|
503
|
+
font: "monospace",
|
|
504
|
+
});
|
|
505
|
+
text.insertAt(0, "MONO");
|
|
506
|
+
text.defaultFormat = createPlainFormat();
|
|
507
|
+
text.insertAt(4, "plain");
|
|
508
|
+
const content = React.createElement(FormattedMainView, { root: toPropTreeNode(text) });
|
|
509
|
+
const rendered = render(content, { reactStrictMode });
|
|
510
|
+
assert.ok(rendered.container.querySelector(".ql-font-monospace"), "Initially: has .ql-font-monospace");
|
|
511
|
+
text.removeRange(0, 4);
|
|
512
|
+
rendered.rerender(content);
|
|
513
|
+
assert.ok(!rendered.container.querySelector(".ql-font-monospace"), "After delete: no .ql-font-monospace");
|
|
514
|
+
});
|
|
515
|
+
it("applies font via formatRange", () => {
|
|
516
|
+
const { tree: text } = createFormattedTreeView("Hello World");
|
|
517
|
+
const content = React.createElement(FormattedMainView, { root: toPropTreeNode(text) });
|
|
518
|
+
const rendered = render(content, { reactStrictMode });
|
|
519
|
+
text.formatRange(6, 11, { font: "monospace" });
|
|
520
|
+
rendered.rerender(content);
|
|
521
|
+
const el = rendered.container.querySelector(".ql-font-monospace");
|
|
522
|
+
assert.ok(el, "Expected .ql-font-monospace after formatRange");
|
|
523
|
+
assert.match(el.textContent ?? "", /World/);
|
|
524
|
+
});
|
|
525
|
+
});
|
|
526
|
+
});
|
|
527
|
+
}
|
|
528
|
+
});
|
|
529
|
+
// Undo/Redo tests for non-transactional edits
|
|
530
|
+
describe("undo/redo", () => {
|
|
531
|
+
for (const reactStrictMode of [false, true]) {
|
|
532
|
+
describe(`StrictMode: ${reactStrictMode}`, () => {
|
|
533
|
+
it("insert character, undo removes it, redo restores it", () => {
|
|
534
|
+
const treeView = createFormattedTreeViewWithEvents();
|
|
535
|
+
const text = treeView.root;
|
|
536
|
+
const undoRedo = new UndoRedoStacks(treeView.events);
|
|
537
|
+
const editorRef = React.createRef();
|
|
538
|
+
const content = (React.createElement(FormattedMainView, { ref: editorRef, root: toPropTreeNode(text), undoRedo: undoRedo }));
|
|
539
|
+
const rendered = render(content, { reactStrictMode });
|
|
540
|
+
// Insert a character
|
|
541
|
+
text.insertAt(0, "A");
|
|
542
|
+
rendered.rerender(content);
|
|
543
|
+
assert.match(rendered.baseElement.textContent ?? "", /A/);
|
|
544
|
+
// Undo - character should be removed
|
|
545
|
+
editorRef.current?.undo();
|
|
546
|
+
rendered.rerender(content);
|
|
547
|
+
assert(rendered.baseElement.textContent !== null);
|
|
548
|
+
assert.doesNotMatch(rendered.baseElement.textContent, /A/);
|
|
549
|
+
// Redo - character should be restored
|
|
550
|
+
editorRef.current?.redo();
|
|
551
|
+
rendered.rerender(content);
|
|
552
|
+
assert.match(rendered.baseElement.textContent ?? "", /A/);
|
|
553
|
+
});
|
|
554
|
+
it("insert character, make bold, undo removes bold but keeps character", () => {
|
|
555
|
+
const treeView = createFormattedTreeViewWithEvents();
|
|
556
|
+
const text = treeView.root;
|
|
557
|
+
const undoRedo = new UndoRedoStacks(treeView.events);
|
|
558
|
+
const editorRef = React.createRef();
|
|
559
|
+
const content = (React.createElement(FormattedMainView, { ref: editorRef, root: toPropTreeNode(text), undoRedo: undoRedo }));
|
|
560
|
+
const rendered = render(content, { reactStrictMode });
|
|
561
|
+
// Insert a character
|
|
562
|
+
text.insertAt(0, "B");
|
|
563
|
+
rendered.rerender(content);
|
|
564
|
+
assert.match(rendered.baseElement.textContent ?? "", /B/);
|
|
565
|
+
assert.ok(!rendered.container.querySelector("strong"), "Initially: no <strong>");
|
|
566
|
+
// Make it bold
|
|
567
|
+
text.formatRange(0, 1, { bold: true });
|
|
568
|
+
rendered.rerender(content);
|
|
569
|
+
assert.ok(rendered.container.querySelector("strong"), "After format: has <strong>");
|
|
570
|
+
// Undo - bold should be removed, character should remain
|
|
571
|
+
editorRef.current?.undo();
|
|
572
|
+
rendered.rerender(content);
|
|
573
|
+
assert.match(rendered.baseElement.textContent ?? "", /B/);
|
|
574
|
+
assert.ok(!rendered.container.querySelector("strong"), "After undo: no <strong>, character remains");
|
|
575
|
+
});
|
|
576
|
+
it("multiple operations in transaction undo together as one unit", () => {
|
|
577
|
+
const treeView = createFormattedTreeViewWithEvents();
|
|
578
|
+
const text = treeView.root;
|
|
579
|
+
const undoRedo = new UndoRedoStacks(treeView.events);
|
|
580
|
+
const editorRef = React.createRef();
|
|
581
|
+
const content = (React.createElement(FormattedMainView, { ref: editorRef, root: toPropTreeNode(text), undoRedo: undoRedo }));
|
|
582
|
+
const rendered = render(content, { reactStrictMode });
|
|
583
|
+
// Two operations in one transaction
|
|
584
|
+
TreeAlpha.branch(text)?.runTransaction(() => {
|
|
585
|
+
text.insertAt(0, "A");
|
|
586
|
+
text.insertAt(1, "B");
|
|
587
|
+
});
|
|
588
|
+
rendered.rerender(content);
|
|
589
|
+
assert.match(rendered.baseElement.textContent ?? "", /AB/);
|
|
590
|
+
// Single undo should remove both characters
|
|
591
|
+
editorRef.current?.undo();
|
|
592
|
+
rendered.rerender(content);
|
|
593
|
+
assert(rendered.baseElement.textContent !== null);
|
|
594
|
+
assert.doesNotMatch(rendered.baseElement.textContent, /A/);
|
|
595
|
+
assert.doesNotMatch(rendered.baseElement.textContent, /B/);
|
|
596
|
+
// Single redo should restore both characters
|
|
597
|
+
editorRef.current?.redo();
|
|
598
|
+
rendered.rerender(content);
|
|
599
|
+
assert.match(rendered.baseElement.textContent ?? "", /AB/);
|
|
600
|
+
});
|
|
601
|
+
});
|
|
602
|
+
}
|
|
603
|
+
});
|
|
604
|
+
describe("copy-paste helpers", () => {
|
|
605
|
+
/** Helper to create an HTMLElement with inline styles. */
|
|
606
|
+
function styledElement(styles) {
|
|
607
|
+
const el = document.createElement("span");
|
|
608
|
+
Object.assign(el.style, styles);
|
|
609
|
+
return el;
|
|
610
|
+
}
|
|
611
|
+
describe("parseCssFontSize", () => {
|
|
612
|
+
it("returns undefined when no fontSize is set", () => {
|
|
613
|
+
assert.equal(parseCssFontSize(styledElement({})), undefined);
|
|
614
|
+
});
|
|
615
|
+
it("returns Quill size name for supported pixel values", () => {
|
|
616
|
+
assert.equal(parseCssFontSize(styledElement({ fontSize: "10px" })), "small");
|
|
617
|
+
assert.equal(parseCssFontSize(styledElement({ fontSize: "18px" })), "large");
|
|
618
|
+
assert.equal(parseCssFontSize(styledElement({ fontSize: "24px" })), "huge");
|
|
619
|
+
});
|
|
620
|
+
it("returns undefined for default or unrecognized sizes", () => {
|
|
621
|
+
assert.equal(parseCssFontSize(styledElement({ fontSize: "12px" })), undefined);
|
|
622
|
+
assert.equal(parseCssFontSize(styledElement({ fontSize: "42px" })), undefined);
|
|
623
|
+
});
|
|
624
|
+
});
|
|
625
|
+
describe("parseCssFontFamily", () => {
|
|
626
|
+
it("returns undefined when no fontFamily is set", () => {
|
|
627
|
+
assert.equal(parseCssFontFamily(styledElement({})), undefined);
|
|
628
|
+
});
|
|
629
|
+
it("returns first recognized font in a comma-separated stack", () => {
|
|
630
|
+
assert.equal(parseCssFontFamily(styledElement({ fontFamily: "monospace" })), "monospace");
|
|
631
|
+
assert.equal(parseCssFontFamily(styledElement({ fontFamily: '"Courier New", monospace' })), "monospace");
|
|
632
|
+
assert.equal(parseCssFontFamily(styledElement({ fontFamily: '"Times New Roman", "Arial", serif' })), "Arial");
|
|
633
|
+
});
|
|
634
|
+
it("strips single quotes around recognized font names", () => {
|
|
635
|
+
assert.equal(parseCssFontFamily(styledElement({ fontFamily: "'Arial'" })), "Arial");
|
|
636
|
+
});
|
|
637
|
+
it("returns undefined for unrecognized fonts", () => {
|
|
638
|
+
assert.equal(parseCssFontFamily(styledElement({ fontFamily: '"Courier New", fantasy' })), undefined);
|
|
639
|
+
});
|
|
640
|
+
});
|
|
641
|
+
describe("clipboardFormatMatcher", () => {
|
|
642
|
+
it("returns delta unchanged for non-HTMLElement nodes", () => {
|
|
643
|
+
const delta = new Delta().insert("hello");
|
|
644
|
+
const text = document.createTextNode("hello");
|
|
645
|
+
const result = clipboardFormatMatcher(text, delta);
|
|
646
|
+
assert.deepEqual(result.ops, delta.ops);
|
|
647
|
+
});
|
|
648
|
+
it("applies size and font attributes from inline styles", () => {
|
|
649
|
+
const delta = new Delta().insert("hello");
|
|
650
|
+
const el = styledElement({ fontSize: "18px", fontFamily: "serif" });
|
|
651
|
+
const result = clipboardFormatMatcher(el, delta);
|
|
652
|
+
assert.equal(result.ops[0]?.attributes?.size, "large");
|
|
653
|
+
assert.equal(result.ops[0]?.attributes?.font, "serif");
|
|
654
|
+
});
|
|
655
|
+
it("returns delta unchanged when no recognized styles", () => {
|
|
656
|
+
const delta = new Delta().insert("hello");
|
|
657
|
+
const el = styledElement({});
|
|
658
|
+
const result = clipboardFormatMatcher(el, delta);
|
|
659
|
+
assert.deepEqual(result.ops, delta.ops);
|
|
660
|
+
});
|
|
661
|
+
});
|
|
662
|
+
});
|
|
663
|
+
// Unicode 16+ (joined emojis) section - test attribute cycling
|
|
664
|
+
describe("Unicode 16+ joined emoji attribute cycling", () => {
|
|
665
|
+
// ZWJ (Zero Width Joiner) emoji sequence: ๐จโ๐ฉโ๐งโ๐ฆ = family emoji
|
|
666
|
+
const joinedEmoji = "๐จโ๐ฉโ๐งโ๐ฆ";
|
|
667
|
+
for (const reactStrictMode of [false, true]) {
|
|
668
|
+
describe(`StrictMode: ${reactStrictMode}`, () => {
|
|
669
|
+
it("applies bold to joined emoji and removes it preserving text", () => {
|
|
670
|
+
const { tree: text } = createFormattedTreeView(`Test ${joinedEmoji} Text`);
|
|
671
|
+
const content = React.createElement(FormattedMainView, { root: toPropTreeNode(text) });
|
|
672
|
+
const rendered = render(content, { reactStrictMode });
|
|
673
|
+
const emojiStart = 5; // "Test " is 5 chars
|
|
674
|
+
const emojiLength = [...joinedEmoji].length;
|
|
675
|
+
text.formatRange(emojiStart, emojiStart + emojiLength, { bold: true });
|
|
676
|
+
rendered.rerender(content);
|
|
677
|
+
assert.ok(rendered.container.querySelector("strong"), "After bold: expected <strong>");
|
|
678
|
+
assert.ok((rendered.baseElement.textContent ?? "").includes(joinedEmoji), "After bold: emoji should be preserved");
|
|
679
|
+
text.formatRange(emojiStart, emojiStart + emojiLength, { bold: false });
|
|
680
|
+
rendered.rerender(content);
|
|
681
|
+
assert.ok(!rendered.container.querySelector("strong"), "After remove bold: no <strong> expected");
|
|
682
|
+
assert.ok((rendered.baseElement.textContent ?? "").includes(joinedEmoji), "After remove bold: emoji should still be preserved");
|
|
683
|
+
});
|
|
684
|
+
it("applies size to joined emoji and removes it preserving text", () => {
|
|
685
|
+
const { tree: text } = createFormattedTreeView(`Test ${joinedEmoji} Text`);
|
|
686
|
+
const content = React.createElement(FormattedMainView, { root: toPropTreeNode(text) });
|
|
687
|
+
const rendered = render(content, { reactStrictMode });
|
|
688
|
+
const emojiStart = 5;
|
|
689
|
+
const emojiLength = [...joinedEmoji].length;
|
|
690
|
+
text.formatRange(emojiStart, emojiStart + emojiLength, { size: 24 });
|
|
691
|
+
rendered.rerender(content);
|
|
692
|
+
assert.ok(rendered.container.querySelector(".ql-size-huge"), "After size: expected .ql-size-huge");
|
|
693
|
+
assert.ok((rendered.baseElement.textContent ?? "").includes(joinedEmoji), "Emoji preserved");
|
|
694
|
+
text.formatRange(emojiStart, emojiStart + emojiLength, { size: 12 });
|
|
695
|
+
rendered.rerender(content);
|
|
696
|
+
assert.ok(!rendered.container.querySelector(".ql-size-huge"), "After remove: no .ql-size-huge");
|
|
697
|
+
assert.ok((rendered.baseElement.textContent ?? "").includes(joinedEmoji), "Emoji still preserved");
|
|
698
|
+
});
|
|
699
|
+
});
|
|
700
|
+
}
|
|
701
|
+
});
|
|
702
|
+
});
|
|
703
|
+
});
|
|
704
|
+
//# sourceMappingURL=textEditor.test.js.map
|