@remit/ui 0.0.128 → 0.0.129
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 +2 -3
- package/src/components/brief-sections.keyboard.test.ts +8 -36
- package/src/components/compose-body.language.test.ts +8 -58
- package/src/components/compose-body.spellcheck.test.ts +8 -55
- package/src/components/event-editor.render.test.ts +2 -2
- package/src/components/filter-rule-editor.create.test.ts +10 -59
- package/src/components/folder-tree-picker.create.test.ts +18 -63
- package/src/components/message-list-pane.brief-filter.test.ts +4 -31
- package/src/components/nav-link-surface.interaction.test.ts +7 -34
- package/src/components/nav-sidebar.keyboard.test.ts +11 -40
- package/src/components/password-input.render.test.ts +8 -50
- package/src/components/plain-text-editor.mount.test.ts +11 -66
- package/src/components/rich-text-editor.mount.test.ts +9 -63
- package/src/components/rich-text-formatting.test.ts +27 -45
- package/src/components/rich-text-image-node.test.ts +60 -83
- package/src/components/rich-text-markdown.test.ts +3 -25
- package/src/components/rich-text-paste.test.ts +28 -42
- package/src/components/rich-text-spellcheck-menu.test.ts +28 -94
- package/src/components/rich-text-spellcheck.test.ts +11 -65
- package/src/components/swipeable-row.gesture.test.ts +8 -42
- package/src/components/touch-list.follows-sections.test.ts +4 -27
- package/src/lib/adopted-html.test.ts +3 -22
- package/src/lib/compose-language.test.ts +2 -7
- package/src/lib/roving-focus.test.ts +16 -43
- package/src/lib/use-list-cursor.test.ts +3 -27
- package/src/lib/use-list-keyboard.test.ts +6 -30
- package/src/lib/use-long-press.test.ts +12 -43
- package/src/lib/use-rendered-row-ids.test.ts +3 -24
- package/src/lib/use-selection.hook.test.ts +3 -26
- package/src/lib/use-suggest-list.test.ts +5 -29
- package/src/lib/use-triage-keyboard.test.ts +6 -29
|
@@ -4,98 +4,75 @@
|
|
|
4
4
|
* writer sees, and reopened from its own serialization, so it is the draft that
|
|
5
5
|
* comes back rather than the document that was saved.
|
|
6
6
|
*/
|
|
7
|
+
import "@remit/test-dom";
|
|
7
8
|
import assert from "node:assert/strict";
|
|
8
|
-
import {
|
|
9
|
-
import
|
|
10
|
-
|
|
11
|
-
|
|
9
|
+
import { describe, it } from "node:test";
|
|
10
|
+
import {
|
|
11
|
+
$getRoot,
|
|
12
|
+
$insertNodes,
|
|
13
|
+
$isElementNode,
|
|
14
|
+
createEditor,
|
|
15
|
+
type ElementNode,
|
|
16
|
+
type LexicalEditor,
|
|
17
|
+
} from "lexical";
|
|
18
|
+
import { $adoptHtml, $readRichText } from "./rich-text-document.js";
|
|
19
|
+
import { ImageNode } from "./rich-text-image-node.js";
|
|
20
|
+
import { RICH_TEXT_NODES, richTextTheme } from "./rich-text-nodes.js";
|
|
12
21
|
|
|
13
22
|
const LOGO = "https://example.com/logo.png";
|
|
14
23
|
const PIXEL =
|
|
15
24
|
"data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7";
|
|
16
25
|
const PASTED = `<p>Chart: <img src="${LOGO}" alt="The logo"></p>`;
|
|
17
26
|
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
);
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
globalThis.Range = dom.window.Range;
|
|
38
|
-
globalThis.getComputedStyle = dom.window.getComputedStyle.bind(dom.window);
|
|
39
|
-
|
|
40
|
-
const { $getRoot, $insertNodes, $isElementNode, createEditor } = await import(
|
|
41
|
-
"lexical"
|
|
42
|
-
);
|
|
43
|
-
const { $adoptHtml, $readRichText } = await import("./rich-text-document.js");
|
|
44
|
-
const { ImageNode } = await import("./rich-text-image-node.js");
|
|
45
|
-
const { RICH_TEXT_NODES, richTextTheme } = await import(
|
|
46
|
-
"./rich-text-nodes.js"
|
|
47
|
-
);
|
|
48
|
-
|
|
49
|
-
openEditor = (html: string) => {
|
|
50
|
-
const editor: LexicalEditor = createEditor({
|
|
51
|
-
namespace: "test",
|
|
52
|
-
nodes: [...RICH_TEXT_NODES],
|
|
53
|
-
onError: (error) => {
|
|
54
|
-
throw error;
|
|
27
|
+
const openEditor = (html: string) => {
|
|
28
|
+
const editor: LexicalEditor = createEditor({
|
|
29
|
+
namespace: "test",
|
|
30
|
+
nodes: [...RICH_TEXT_NODES],
|
|
31
|
+
onError: (error) => {
|
|
32
|
+
throw error;
|
|
33
|
+
},
|
|
34
|
+
theme: richTextTheme,
|
|
35
|
+
});
|
|
36
|
+
const root = document.createElement("div");
|
|
37
|
+
root.contentEditable = "true";
|
|
38
|
+
document.body.appendChild(root);
|
|
39
|
+
editor.setRootElement(root);
|
|
40
|
+
if (html !== "")
|
|
41
|
+
editor.update(
|
|
42
|
+
() => {
|
|
43
|
+
$getRoot().clear();
|
|
44
|
+
$getRoot().select();
|
|
45
|
+
$insertNodes($adoptHtml(editor, html));
|
|
55
46
|
},
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
imageOf = (editor: LexicalEditor) => {
|
|
75
|
-
const image = editor.getRootElement()?.querySelector("img");
|
|
76
|
-
if (!image) throw new Error("the editor is showing no image");
|
|
77
|
-
return image;
|
|
47
|
+
{ discrete: true },
|
|
48
|
+
);
|
|
49
|
+
return editor;
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
const imageOf = (editor: LexicalEditor) => {
|
|
53
|
+
const image = editor.getRootElement()?.querySelector("img");
|
|
54
|
+
if (!image) throw new Error("the editor is showing no image");
|
|
55
|
+
return image;
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
const $theImage = (): ImageNode => {
|
|
59
|
+
const found: ImageNode[] = [];
|
|
60
|
+
const visit = (node: ElementNode) => {
|
|
61
|
+
for (const child of node.getChildren()) {
|
|
62
|
+
if (child instanceof ImageNode) found.push(child);
|
|
63
|
+
if ($isElementNode(child)) visit(child);
|
|
64
|
+
}
|
|
78
65
|
};
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
if (found.length !== 1)
|
|
90
|
-
throw new Error(`the document holds ${found.length} images`);
|
|
91
|
-
return found[0];
|
|
92
|
-
};
|
|
93
|
-
|
|
94
|
-
readHtml = (editor: LexicalEditor) =>
|
|
95
|
-
editor.read(() => $readRichText(editor)).html;
|
|
96
|
-
readText = (editor: LexicalEditor) =>
|
|
97
|
-
editor.read(() => $readRichText(editor)).text;
|
|
98
|
-
});
|
|
66
|
+
visit($getRoot());
|
|
67
|
+
if (found.length !== 1)
|
|
68
|
+
throw new Error(`the document holds ${found.length} images`);
|
|
69
|
+
return found[0];
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
const readHtml = (editor: LexicalEditor) =>
|
|
73
|
+
editor.read(() => $readRichText(editor)).html;
|
|
74
|
+
const readText = (editor: LexicalEditor) =>
|
|
75
|
+
editor.read(() => $readRichText(editor)).text;
|
|
99
76
|
|
|
100
77
|
describe("the image the composer shows", () => {
|
|
101
78
|
it("draws the picture rather than a placeholder for it", () => {
|
|
@@ -8,9 +8,10 @@
|
|
|
8
8
|
* so a table pasted there and a table pasted in rich mode and then switched
|
|
9
9
|
* produce the same characters.
|
|
10
10
|
*/
|
|
11
|
+
import "@remit/test-dom";
|
|
11
12
|
import assert from "node:assert/strict";
|
|
12
|
-
import {
|
|
13
|
-
import
|
|
13
|
+
import { describe, it } from "node:test";
|
|
14
|
+
import { htmlToMarkdown, markdownToHtml } from "./rich-text-document.js";
|
|
14
15
|
|
|
15
16
|
const TABLE_HTML = [
|
|
16
17
|
"<table><thead><tr><th>Region</th><th>Total</th></tr></thead>",
|
|
@@ -25,29 +26,6 @@ const TABLE_MARKDOWN = [
|
|
|
25
26
|
"| Americas | 388 |",
|
|
26
27
|
].join("\n");
|
|
27
28
|
|
|
28
|
-
let htmlToMarkdown: (html: string) => string;
|
|
29
|
-
let markdownToHtml: (markdown: string) => string;
|
|
30
|
-
|
|
31
|
-
before(async () => {
|
|
32
|
-
const { JSDOM: JSDOMCtor } = await import("jsdom");
|
|
33
|
-
const dom: JSDOM = new JSDOMCtor(
|
|
34
|
-
"<!doctype html><html><body></body></html>",
|
|
35
|
-
{
|
|
36
|
-
url: "http://localhost/",
|
|
37
|
-
},
|
|
38
|
-
);
|
|
39
|
-
globalThis.window = dom.window as unknown as typeof globalThis.window;
|
|
40
|
-
globalThis.document = dom.window.document;
|
|
41
|
-
globalThis.DOMParser = dom.window.DOMParser;
|
|
42
|
-
globalThis.HTMLElement = dom.window.HTMLElement;
|
|
43
|
-
globalThis.Element = dom.window.Element;
|
|
44
|
-
globalThis.Node = dom.window.Node;
|
|
45
|
-
|
|
46
|
-
({ htmlToMarkdown, markdownToHtml } = await import(
|
|
47
|
-
"./rich-text-document.js"
|
|
48
|
-
));
|
|
49
|
-
});
|
|
50
|
-
|
|
51
29
|
describe("HTML down-converted for the plain surface", () => {
|
|
52
30
|
it("writes a table as pipe rows under a divider", () => {
|
|
53
31
|
assert.equal(htmlToMarkdown(TABLE_HTML).trim(), TABLE_MARKDOWN);
|
|
@@ -5,10 +5,17 @@
|
|
|
5
5
|
* sanitize, parse, convert — and reads the document back out as the HTML that
|
|
6
6
|
* would be sent.
|
|
7
7
|
*/
|
|
8
|
+
import "@remit/test-dom";
|
|
8
9
|
import assert from "node:assert/strict";
|
|
9
|
-
import {
|
|
10
|
-
import
|
|
11
|
-
|
|
10
|
+
import { describe, it } from "node:test";
|
|
11
|
+
import {
|
|
12
|
+
$getRoot,
|
|
13
|
+
$insertNodes,
|
|
14
|
+
createEditor,
|
|
15
|
+
type LexicalEditor,
|
|
16
|
+
} from "lexical";
|
|
17
|
+
import { $adoptHtml, $readRichText } from "./rich-text-document.js";
|
|
18
|
+
import { RICH_TEXT_NODES } from "./rich-text-nodes.js";
|
|
12
19
|
|
|
13
20
|
const PASTED_IMAGE = [
|
|
14
21
|
"<p>before</p>",
|
|
@@ -29,46 +36,25 @@ const PASTED = [
|
|
|
29
36
|
"<script>alert(1)</script>",
|
|
30
37
|
].join("");
|
|
31
38
|
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
+
const adoptAndSerialize = (html: string) => {
|
|
40
|
+
const editor: LexicalEditor = createEditor({
|
|
41
|
+
namespace: "test",
|
|
42
|
+
nodes: [...RICH_TEXT_NODES],
|
|
43
|
+
onError: (error) => {
|
|
44
|
+
throw error;
|
|
45
|
+
},
|
|
46
|
+
});
|
|
47
|
+
editor.update(
|
|
48
|
+
() => {
|
|
49
|
+
const root = $getRoot();
|
|
50
|
+
root.clear();
|
|
51
|
+
root.select();
|
|
52
|
+
$insertNodes($adoptHtml(editor, html));
|
|
53
|
+
},
|
|
54
|
+
{ discrete: true },
|
|
39
55
|
);
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
globalThis.DOMParser = dom.window.DOMParser;
|
|
43
|
-
globalThis.HTMLElement = dom.window.HTMLElement;
|
|
44
|
-
globalThis.Element = dom.window.Element;
|
|
45
|
-
globalThis.Node = dom.window.Node;
|
|
46
|
-
|
|
47
|
-
const { $getRoot, $insertNodes, createEditor } = await import("lexical");
|
|
48
|
-
const { $adoptHtml, $readRichText } = await import("./rich-text-document.js");
|
|
49
|
-
const { RICH_TEXT_NODES } = await import("./rich-text-nodes.js");
|
|
50
|
-
|
|
51
|
-
adoptAndSerialize = (html: string) => {
|
|
52
|
-
const editor: LexicalEditor = createEditor({
|
|
53
|
-
namespace: "test",
|
|
54
|
-
nodes: [...RICH_TEXT_NODES],
|
|
55
|
-
onError: (error) => {
|
|
56
|
-
throw error;
|
|
57
|
-
},
|
|
58
|
-
});
|
|
59
|
-
editor.update(
|
|
60
|
-
() => {
|
|
61
|
-
const root = $getRoot();
|
|
62
|
-
root.clear();
|
|
63
|
-
root.select();
|
|
64
|
-
$insertNodes($adoptHtml(editor, html));
|
|
65
|
-
},
|
|
66
|
-
{ discrete: true },
|
|
67
|
-
);
|
|
68
|
-
return editor.read(() => $readRichText(editor));
|
|
69
|
-
};
|
|
70
|
-
});
|
|
71
|
-
|
|
56
|
+
return editor.read(() => $readRichText(editor));
|
|
57
|
+
};
|
|
72
58
|
describe("adopting pasted HTML", () => {
|
|
73
59
|
it("keeps the heading, the list and the table", () => {
|
|
74
60
|
const { html } = adoptAndSerialize(PASTED);
|
|
@@ -3,21 +3,14 @@
|
|
|
3
3
|
* findings the marks already hold, the suggestions arrive after it, and what
|
|
4
4
|
* the writer picks lands in the document as one undoable step.
|
|
5
5
|
*/
|
|
6
|
+
import "@remit/test-dom";
|
|
6
7
|
import assert from "node:assert/strict";
|
|
7
|
-
import {
|
|
8
|
-
import
|
|
9
|
-
import type
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
} from "lexical";
|
|
14
|
-
import type {
|
|
15
|
-
act as reactAct,
|
|
16
|
-
createElement as reactCreateElement,
|
|
17
|
-
useEffect as reactUseEffect,
|
|
18
|
-
} from "react";
|
|
19
|
-
import type { Root, createRoot as reactCreateRoot } from "react-dom/client";
|
|
20
|
-
import type { RichTextEditor as RichTextEditorType } from "./rich-text-editor.js";
|
|
8
|
+
import { afterEach, before, beforeEach, describe, it } from "node:test";
|
|
9
|
+
import { useLexicalComposerContext } from "@lexical/react/LexicalComposerContext";
|
|
10
|
+
import { $getRoot, type LexicalEditor, UNDO_COMMAND } from "lexical";
|
|
11
|
+
import { act, createElement, useEffect } from "react";
|
|
12
|
+
import { createRoot, type Root } from "react-dom/client";
|
|
13
|
+
import { RichTextEditor } from "./rich-text-editor.js";
|
|
21
14
|
import type {
|
|
22
15
|
CheckRequest,
|
|
23
16
|
CheckResponse,
|
|
@@ -47,17 +40,8 @@ interface MarkHost {
|
|
|
47
40
|
Highlight: typeof StubMarks;
|
|
48
41
|
}
|
|
49
42
|
|
|
50
|
-
let dom: JSDOM;
|
|
51
43
|
let container: HTMLElement;
|
|
52
44
|
const roots: Root[] = [];
|
|
53
|
-
let act: typeof reactAct;
|
|
54
|
-
let createElement: typeof reactCreateElement;
|
|
55
|
-
let useEffect: typeof reactUseEffect;
|
|
56
|
-
let createRoot: typeof reactCreateRoot;
|
|
57
|
-
let RichTextEditor: typeof RichTextEditorType;
|
|
58
|
-
let useLexicalComposerContext: () => [LexicalEditor];
|
|
59
|
-
let $getRoot: typeof getRootType;
|
|
60
|
-
let UNDO_COMMAND: typeof undoCommandType;
|
|
61
45
|
let desktopLayout = true;
|
|
62
46
|
|
|
63
47
|
const offsets = (): [number, number][] =>
|
|
@@ -140,8 +124,8 @@ const mount = async (
|
|
|
140
124
|
return null;
|
|
141
125
|
};
|
|
142
126
|
|
|
143
|
-
container =
|
|
144
|
-
|
|
127
|
+
container = document.createElement("div");
|
|
128
|
+
document.body.append(container);
|
|
145
129
|
await act(async () => {
|
|
146
130
|
const root = createRoot(container);
|
|
147
131
|
roots.push(root);
|
|
@@ -187,7 +171,7 @@ const characters = (): Node => {
|
|
|
187
171
|
* browser is the one that does this; here the test has to.
|
|
188
172
|
*/
|
|
189
173
|
const putCaret = (offset: number): void => {
|
|
190
|
-
|
|
174
|
+
document.getSelection()?.setPosition(characters(), offset);
|
|
191
175
|
};
|
|
192
176
|
|
|
193
177
|
/**
|
|
@@ -197,19 +181,15 @@ const putCaret = (offset: number): void => {
|
|
|
197
181
|
* the desktop gate stays in `container`, and this still reaches it there.
|
|
198
182
|
*/
|
|
199
183
|
const menu = (): HTMLElement | null =>
|
|
200
|
-
|
|
201
|
-
"[data-testid=spell-menu]",
|
|
202
|
-
);
|
|
184
|
+
document.body.querySelector<HTMLElement>("[data-testid=spell-menu]");
|
|
203
185
|
|
|
204
186
|
const rows = (testId: string): HTMLElement[] => [
|
|
205
|
-
...
|
|
206
|
-
`[data-testid=${testId}]`,
|
|
207
|
-
),
|
|
187
|
+
...document.body.querySelectorAll<HTMLElement>(`[data-testid=${testId}]`),
|
|
208
188
|
];
|
|
209
189
|
|
|
210
190
|
/** Any element inside the menu, wherever it landed — see {@link menu}. */
|
|
211
191
|
const spellNode = (selector: string): HTMLElement | null =>
|
|
212
|
-
|
|
192
|
+
document.body.querySelector<HTMLElement>(selector);
|
|
213
193
|
|
|
214
194
|
/**
|
|
215
195
|
* jsdom carries no `PointerEvent`, and what the editor reads off one is the
|
|
@@ -220,7 +200,7 @@ const pointerEvent = (
|
|
|
220
200
|
pointerType: string,
|
|
221
201
|
at: { clientX: number; clientY: number; button?: number },
|
|
222
202
|
): MouseEvent => {
|
|
223
|
-
const event = new
|
|
203
|
+
const event = new MouseEvent(type, { bubbles: true, ...at });
|
|
224
204
|
Object.defineProperty(event, "pointerType", { value: pointerType });
|
|
225
205
|
return event;
|
|
226
206
|
};
|
|
@@ -288,7 +268,7 @@ const caretAt = async (
|
|
|
288
268
|
const keyDownOn = async (key: string): Promise<void> => {
|
|
289
269
|
await act(async () => {
|
|
290
270
|
editable().dispatchEvent(
|
|
291
|
-
new
|
|
271
|
+
new KeyboardEvent("keydown", {
|
|
292
272
|
key,
|
|
293
273
|
bubbles: true,
|
|
294
274
|
cancelable: true,
|
|
@@ -304,7 +284,7 @@ const chordAt = async (
|
|
|
304
284
|
await caretAt(editor, offset);
|
|
305
285
|
await act(async () => {
|
|
306
286
|
editable().dispatchEvent(
|
|
307
|
-
new
|
|
287
|
+
new KeyboardEvent("keydown", {
|
|
308
288
|
key: ".",
|
|
309
289
|
ctrlKey: true,
|
|
310
290
|
bubbles: true,
|
|
@@ -320,40 +300,24 @@ const click = async (element: HTMLElement): Promise<void> => {
|
|
|
320
300
|
});
|
|
321
301
|
};
|
|
322
302
|
|
|
323
|
-
before(
|
|
324
|
-
|
|
325
|
-
dom = new JSDOMCtor(
|
|
326
|
-
"<!doctype html><html><body><div id=root></div></body></html>",
|
|
327
|
-
{ url: "http://localhost/", pretendToBeVisual: true },
|
|
328
|
-
);
|
|
329
|
-
globalThis.window = dom.window as unknown as typeof globalThis.window;
|
|
330
|
-
globalThis.document = dom.window.document;
|
|
331
|
-
globalThis.HTMLElement = dom.window.HTMLElement;
|
|
332
|
-
globalThis.Element = dom.window.Element;
|
|
333
|
-
globalThis.Node = dom.window.Node;
|
|
334
|
-
globalThis.Event = dom.window.Event;
|
|
335
|
-
globalThis.MouseEvent = dom.window.MouseEvent;
|
|
336
|
-
globalThis.KeyboardEvent = dom.window.KeyboardEvent;
|
|
337
|
-
globalThis.DOMParser = dom.window.DOMParser;
|
|
338
|
-
globalThis.MutationObserver = dom.window.MutationObserver;
|
|
339
|
-
globalThis.Range = dom.window.Range;
|
|
340
|
-
Object.defineProperty(dom.window.Range.prototype, "getBoundingClientRect", {
|
|
303
|
+
before(() => {
|
|
304
|
+
Object.defineProperty(Range.prototype, "getBoundingClientRect", {
|
|
341
305
|
value: () => ({ top: 12, bottom: 24, left: 8, right: 40 }),
|
|
342
306
|
configurable: true,
|
|
343
307
|
});
|
|
344
|
-
Object.defineProperty(
|
|
308
|
+
Object.defineProperty(Element.prototype, "getBoundingClientRect", {
|
|
345
309
|
value: () => ({ top: 0, bottom: 400, left: 0, right: 600 }),
|
|
346
310
|
configurable: true,
|
|
347
311
|
});
|
|
348
312
|
// jsdom lays nothing out, and the sheet reads its own height to know how far
|
|
349
313
|
// down it has been dragged.
|
|
350
|
-
Object.defineProperty(
|
|
314
|
+
Object.defineProperty(HTMLElement.prototype, "offsetHeight", {
|
|
351
315
|
value: 360,
|
|
352
316
|
configurable: true,
|
|
353
317
|
});
|
|
354
318
|
// The layout question every surface here answers: a popover on the word, or
|
|
355
319
|
// the same rows in a sheet.
|
|
356
|
-
Object.defineProperty(
|
|
320
|
+
Object.defineProperty(window, "matchMedia", {
|
|
357
321
|
value: () => ({
|
|
358
322
|
matches: desktopLayout,
|
|
359
323
|
addEventListener: () => {},
|
|
@@ -366,19 +330,6 @@ before(async () => {
|
|
|
366
330
|
unobserve(): void {}
|
|
367
331
|
disconnect(): void {}
|
|
368
332
|
} as unknown as typeof ResizeObserver;
|
|
369
|
-
globalThis.AbortController = dom.window.AbortController;
|
|
370
|
-
globalThis.AbortSignal = dom.window.AbortSignal;
|
|
371
|
-
globalThis.getComputedStyle = dom.window.getComputedStyle.bind(dom.window);
|
|
372
|
-
globalThis.requestAnimationFrame = dom.window.requestAnimationFrame.bind(
|
|
373
|
-
dom.window,
|
|
374
|
-
);
|
|
375
|
-
globalThis.cancelAnimationFrame = dom.window.cancelAnimationFrame.bind(
|
|
376
|
-
dom.window,
|
|
377
|
-
);
|
|
378
|
-
Object.defineProperty(globalThis, "navigator", {
|
|
379
|
-
value: dom.window.navigator,
|
|
380
|
-
configurable: true,
|
|
381
|
-
});
|
|
382
333
|
Object.defineProperty(globalThis, "CSS", {
|
|
383
334
|
value: { highlights: new Map<string, StubMarks>() },
|
|
384
335
|
configurable: true,
|
|
@@ -387,17 +338,6 @@ before(async () => {
|
|
|
387
338
|
value: StubMarks,
|
|
388
339
|
configurable: true,
|
|
389
340
|
});
|
|
390
|
-
(
|
|
391
|
-
globalThis as { IS_REACT_ACT_ENVIRONMENT?: boolean }
|
|
392
|
-
).IS_REACT_ACT_ENVIRONMENT = true;
|
|
393
|
-
|
|
394
|
-
({ act, createElement, useEffect } = await import("react"));
|
|
395
|
-
({ createRoot } = await import("react-dom/client"));
|
|
396
|
-
({ $getRoot, UNDO_COMMAND } = await import("lexical"));
|
|
397
|
-
({ useLexicalComposerContext } = (await import(
|
|
398
|
-
"@lexical/react/LexicalComposerContext"
|
|
399
|
-
)) as unknown as { useLexicalComposerContext: () => [LexicalEditor] });
|
|
400
|
-
({ RichTextEditor } = await import("./rich-text-editor.js"));
|
|
401
341
|
});
|
|
402
342
|
|
|
403
343
|
beforeEach(() => {
|
|
@@ -414,10 +354,6 @@ afterEach(async () => {
|
|
|
414
354
|
container.remove();
|
|
415
355
|
});
|
|
416
356
|
|
|
417
|
-
after(() => {
|
|
418
|
-
dom.window.close();
|
|
419
|
-
});
|
|
420
|
-
|
|
421
357
|
describe("the correction menu", () => {
|
|
422
358
|
it("opens on the word under the pointer and stands in for the suggestions until they land", async () => {
|
|
423
359
|
const spellcheck = stubSpellcheck({ holdSuggestions: true });
|
|
@@ -629,9 +565,7 @@ describe("the correction menu", () => {
|
|
|
629
565
|
await pressAt(1, "mouse", { travel: 40 });
|
|
630
566
|
assert.equal(menu(), null, "and so was a mouse that was dragged");
|
|
631
567
|
|
|
632
|
-
|
|
633
|
-
.getSelection()
|
|
634
|
-
?.setBaseAndExtent(characters(), 0, characters(), 3);
|
|
568
|
+
document.getSelection()?.setBaseAndExtent(characters(), 0, characters(), 3);
|
|
635
569
|
await act(async () => {
|
|
636
570
|
editable().dispatchEvent(
|
|
637
571
|
pointerEvent("pointerdown", "touch", { clientX: 40, clientY: 20 }),
|
|
@@ -693,7 +627,7 @@ describe("the correction menu", () => {
|
|
|
693
627
|
await pressAt(1, "mouse", { button: 2 });
|
|
694
628
|
assert.equal(menu(), null, "the right button raises nothing of ours");
|
|
695
629
|
|
|
696
|
-
const contextMenu = new
|
|
630
|
+
const contextMenu = new MouseEvent("contextmenu", {
|
|
697
631
|
bubbles: true,
|
|
698
632
|
cancelable: true,
|
|
699
633
|
});
|
|
@@ -782,7 +716,7 @@ describe("the correction menu", () => {
|
|
|
782
716
|
assert.ok(menu());
|
|
783
717
|
await act(async () => {
|
|
784
718
|
menu()?.dispatchEvent(
|
|
785
|
-
new
|
|
719
|
+
new KeyboardEvent("keydown", {
|
|
786
720
|
key: "Escape",
|
|
787
721
|
bubbles: true,
|
|
788
722
|
}),
|
|
@@ -790,7 +724,7 @@ describe("the correction menu", () => {
|
|
|
790
724
|
});
|
|
791
725
|
assert.equal(menu(), null, "Escape closes it");
|
|
792
726
|
assert.equal(
|
|
793
|
-
|
|
727
|
+
document.activeElement,
|
|
794
728
|
editable(),
|
|
795
729
|
"and hands the message back its caret",
|
|
796
730
|
);
|
|
@@ -798,8 +732,8 @@ describe("the correction menu", () => {
|
|
|
798
732
|
await clickAt(1);
|
|
799
733
|
assert.ok(menu());
|
|
800
734
|
await act(async () => {
|
|
801
|
-
|
|
802
|
-
new
|
|
735
|
+
document.body.dispatchEvent(
|
|
736
|
+
new MouseEvent("pointerdown", { bubbles: true }),
|
|
803
737
|
);
|
|
804
738
|
});
|
|
805
739
|
assert.equal(menu(), null, "so does a press outside it");
|
|
@@ -864,7 +798,7 @@ describe("the correction menu", () => {
|
|
|
864
798
|
|
|
865
799
|
await act(async () => {
|
|
866
800
|
menu()?.dispatchEvent(
|
|
867
|
-
new
|
|
801
|
+
new KeyboardEvent("keydown", {
|
|
868
802
|
key: "Escape",
|
|
869
803
|
bubbles: true,
|
|
870
804
|
}),
|
|
@@ -4,17 +4,14 @@
|
|
|
4
4
|
* registry is stubbed because jsdom carries no CSS Custom Highlight API, so
|
|
5
5
|
* what is asserted is the ranges the editor would have handed a browser.
|
|
6
6
|
*/
|
|
7
|
+
import "@remit/test-dom";
|
|
7
8
|
import assert from "node:assert/strict";
|
|
8
|
-
import {
|
|
9
|
-
import
|
|
10
|
-
import
|
|
11
|
-
import
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
useEffect as reactUseEffect,
|
|
15
|
-
} from "react";
|
|
16
|
-
import type { Root, createRoot as reactCreateRoot } from "react-dom/client";
|
|
17
|
-
import type { RichTextEditor as RichTextEditorType } from "./rich-text-editor.js";
|
|
9
|
+
import { afterEach, before, beforeEach, describe, it } from "node:test";
|
|
10
|
+
import { useLexicalComposerContext } from "@lexical/react/LexicalComposerContext";
|
|
11
|
+
import { $getRoot, type LexicalEditor } from "lexical";
|
|
12
|
+
import { act, createElement, useEffect } from "react";
|
|
13
|
+
import { createRoot, type Root } from "react-dom/client";
|
|
14
|
+
import { RichTextEditor } from "./rich-text-editor.js";
|
|
18
15
|
import type {
|
|
19
16
|
CheckRequest,
|
|
20
17
|
CheckResponse,
|
|
@@ -51,18 +48,10 @@ interface MarkHost {
|
|
|
51
48
|
Highlight: typeof StubMarks;
|
|
52
49
|
}
|
|
53
50
|
|
|
54
|
-
let dom: JSDOM;
|
|
55
51
|
let container: HTMLElement;
|
|
56
52
|
let root: Root;
|
|
57
53
|
const roots: Root[] = [];
|
|
58
54
|
const containers: HTMLElement[] = [];
|
|
59
|
-
let act: typeof reactAct;
|
|
60
|
-
let createElement: typeof reactCreateElement;
|
|
61
|
-
let useEffect: typeof reactUseEffect;
|
|
62
|
-
let createRoot: typeof reactCreateRoot;
|
|
63
|
-
let RichTextEditor: typeof RichTextEditorType;
|
|
64
|
-
let useLexicalComposerContext: () => [LexicalEditor];
|
|
65
|
-
let $getRoot: typeof getRootType;
|
|
66
55
|
|
|
67
56
|
const marks = (): StubMarks | undefined =>
|
|
68
57
|
(globalThis as unknown as MarkHost).CSS.highlights.get("spell-error");
|
|
@@ -175,8 +164,8 @@ const mount = async (
|
|
|
175
164
|
return null;
|
|
176
165
|
};
|
|
177
166
|
|
|
178
|
-
container =
|
|
179
|
-
|
|
167
|
+
container = document.createElement("div");
|
|
168
|
+
document.body.append(container);
|
|
180
169
|
containers.push(container);
|
|
181
170
|
await act(async () => {
|
|
182
171
|
root = createRoot(container);
|
|
@@ -223,25 +212,10 @@ const editable = (scope: HTMLElement = container): HTMLElement => {
|
|
|
223
212
|
return element;
|
|
224
213
|
};
|
|
225
214
|
|
|
226
|
-
before(
|
|
227
|
-
const { JSDOM: JSDOMCtor } = await import("jsdom");
|
|
228
|
-
dom = new JSDOMCtor(
|
|
229
|
-
"<!doctype html><html><body><div id=root></div></body></html>",
|
|
230
|
-
{ url: "http://localhost/", pretendToBeVisual: true },
|
|
231
|
-
);
|
|
232
|
-
globalThis.window = dom.window as unknown as typeof globalThis.window;
|
|
233
|
-
globalThis.document = dom.window.document;
|
|
234
|
-
globalThis.HTMLElement = dom.window.HTMLElement;
|
|
235
|
-
globalThis.Element = dom.window.Element;
|
|
236
|
-
globalThis.Node = dom.window.Node;
|
|
237
|
-
globalThis.Event = dom.window.Event;
|
|
238
|
-
globalThis.MouseEvent = dom.window.MouseEvent;
|
|
239
|
-
globalThis.DOMParser = dom.window.DOMParser;
|
|
240
|
-
globalThis.MutationObserver = dom.window.MutationObserver;
|
|
241
|
-
globalThis.Range = dom.window.Range;
|
|
215
|
+
before(() => {
|
|
242
216
|
// jsdom has no layout, and Lexical measures the caret's range whenever it
|
|
243
217
|
// writes the DOM selection.
|
|
244
|
-
Object.defineProperty(
|
|
218
|
+
Object.defineProperty(Range.prototype, "getBoundingClientRect", {
|
|
245
219
|
value: () => ({
|
|
246
220
|
top: 0,
|
|
247
221
|
bottom: 0,
|
|
@@ -254,19 +228,6 @@ before(async () => {
|
|
|
254
228
|
}),
|
|
255
229
|
configurable: true,
|
|
256
230
|
});
|
|
257
|
-
globalThis.AbortController = dom.window.AbortController;
|
|
258
|
-
globalThis.AbortSignal = dom.window.AbortSignal;
|
|
259
|
-
globalThis.getComputedStyle = dom.window.getComputedStyle.bind(dom.window);
|
|
260
|
-
globalThis.requestAnimationFrame = dom.window.requestAnimationFrame.bind(
|
|
261
|
-
dom.window,
|
|
262
|
-
);
|
|
263
|
-
globalThis.cancelAnimationFrame = dom.window.cancelAnimationFrame.bind(
|
|
264
|
-
dom.window,
|
|
265
|
-
);
|
|
266
|
-
Object.defineProperty(globalThis, "navigator", {
|
|
267
|
-
value: dom.window.navigator,
|
|
268
|
-
configurable: true,
|
|
269
|
-
});
|
|
270
231
|
Object.defineProperty(globalThis, "CSS", {
|
|
271
232
|
value: { highlights: new Map<string, StubMarks>() },
|
|
272
233
|
configurable: true,
|
|
@@ -275,17 +236,6 @@ before(async () => {
|
|
|
275
236
|
value: StubMarks,
|
|
276
237
|
configurable: true,
|
|
277
238
|
});
|
|
278
|
-
(
|
|
279
|
-
globalThis as { IS_REACT_ACT_ENVIRONMENT?: boolean }
|
|
280
|
-
).IS_REACT_ACT_ENVIRONMENT = true;
|
|
281
|
-
|
|
282
|
-
({ act, createElement, useEffect } = await import("react"));
|
|
283
|
-
({ createRoot } = await import("react-dom/client"));
|
|
284
|
-
({ $getRoot } = await import("lexical"));
|
|
285
|
-
({ useLexicalComposerContext } = (await import(
|
|
286
|
-
"@lexical/react/LexicalComposerContext"
|
|
287
|
-
)) as unknown as { useLexicalComposerContext: () => [LexicalEditor] });
|
|
288
|
-
({ RichTextEditor } = await import("./rich-text-editor.js"));
|
|
289
239
|
});
|
|
290
240
|
|
|
291
241
|
beforeEach(() => {
|
|
@@ -297,10 +247,6 @@ afterEach(async () => {
|
|
|
297
247
|
container.remove();
|
|
298
248
|
});
|
|
299
249
|
|
|
300
|
-
after(() => {
|
|
301
|
-
dom.window.close();
|
|
302
|
-
});
|
|
303
|
-
|
|
304
250
|
describe("spellcheck marks", () => {
|
|
305
251
|
it("marks the misspelt words in the document it opens on", async () => {
|
|
306
252
|
const spellcheck = stubSpellcheck();
|