@remit/ui 0.0.128 → 0.0.130

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.
Files changed (33) hide show
  1. package/package.json +2 -3
  2. package/src/components/brief-sections.keyboard.test.ts +8 -36
  3. package/src/components/compose-body.language.test.ts +8 -58
  4. package/src/components/compose-body.spellcheck.test.ts +8 -55
  5. package/src/components/event-editor.render.test.ts +2 -2
  6. package/src/components/filter-rule-editor.create.test.ts +10 -59
  7. package/src/components/folder-tree-picker.create.test.ts +18 -63
  8. package/src/components/mail-action-toolbar.tsx +17 -10
  9. package/src/components/message-list-pane.brief-filter.test.ts +4 -31
  10. package/src/components/nav-link-surface.interaction.test.ts +7 -34
  11. package/src/components/nav-sidebar.keyboard.test.ts +11 -40
  12. package/src/components/password-input.render.test.ts +8 -50
  13. package/src/components/plain-text-editor.mount.test.ts +11 -66
  14. package/src/components/reading-pane.tsx +10 -1
  15. package/src/components/rich-text-editor.mount.test.ts +9 -63
  16. package/src/components/rich-text-formatting.test.ts +27 -45
  17. package/src/components/rich-text-image-node.test.ts +60 -83
  18. package/src/components/rich-text-markdown.test.ts +3 -25
  19. package/src/components/rich-text-paste.test.ts +28 -42
  20. package/src/components/rich-text-spellcheck-menu.test.ts +28 -94
  21. package/src/components/rich-text-spellcheck.test.ts +11 -65
  22. package/src/components/swipeable-row.gesture.test.ts +8 -42
  23. package/src/components/touch-list.follows-sections.test.ts +4 -27
  24. package/src/lib/adopted-html.test.ts +3 -22
  25. package/src/lib/compose-language.test.ts +2 -7
  26. package/src/lib/roving-focus.test.ts +16 -43
  27. package/src/lib/use-list-cursor.test.ts +3 -27
  28. package/src/lib/use-list-keyboard.test.ts +6 -30
  29. package/src/lib/use-long-press.test.ts +12 -43
  30. package/src/lib/use-rendered-row-ids.test.ts +3 -24
  31. package/src/lib/use-selection.hook.test.ts +3 -26
  32. package/src/lib/use-suggest-list.test.ts +5 -29
  33. 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 { before, describe, it } from "node:test";
9
- import type { JSDOM } from "jsdom";
10
- import type { ElementNode, LexicalEditor } from "lexical";
11
- import type { ImageNode as ImageNodeClass } from "./rich-text-image-node.js";
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
- let openEditor: (html: string) => LexicalEditor;
19
- let imageOf: (editor: LexicalEditor) => HTMLImageElement;
20
- let $theImage: () => ImageNodeClass;
21
- let readHtml: (editor: LexicalEditor) => string;
22
- let readText: (editor: LexicalEditor) => string;
23
-
24
- before(async () => {
25
- const { JSDOM: JSDOMCtor } = await import("jsdom");
26
- const dom: JSDOM = new JSDOMCtor(
27
- "<!doctype html><html><body></body></html>",
28
- { url: "http://localhost/", pretendToBeVisual: true },
29
- );
30
- globalThis.window = dom.window as unknown as typeof globalThis.window;
31
- globalThis.document = dom.window.document;
32
- globalThis.DOMParser = dom.window.DOMParser;
33
- globalThis.HTMLElement = dom.window.HTMLElement;
34
- globalThis.Element = dom.window.Element;
35
- globalThis.Node = dom.window.Node;
36
- globalThis.MutationObserver = dom.window.MutationObserver;
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
- theme: richTextTheme,
57
- });
58
- const root = document.createElement("div");
59
- root.contentEditable = "true";
60
- document.body.appendChild(root);
61
- editor.setRootElement(root);
62
- if (html !== "")
63
- editor.update(
64
- () => {
65
- $getRoot().clear();
66
- $getRoot().select();
67
- $insertNodes($adoptHtml(editor, html));
68
- },
69
- { discrete: true },
70
- );
71
- return editor;
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
- $theImage = () => {
81
- const found: ImageNodeClass[] = [];
82
- const visit = (node: ElementNode) => {
83
- for (const child of node.getChildren()) {
84
- if (child instanceof ImageNode) found.push(child);
85
- if ($isElementNode(child)) visit(child);
86
- }
87
- };
88
- visit($getRoot());
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 { before, describe, it } from "node:test";
13
- import type { JSDOM } from "jsdom";
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 { before, describe, it } from "node:test";
10
- import type { JSDOM } from "jsdom";
11
- import type { LexicalEditor } from "lexical";
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
- let adoptAndSerialize: (html: string) => { html: string; text: string };
33
-
34
- before(async () => {
35
- const { JSDOM: JSDOMCtor } = await import("jsdom");
36
- const dom: JSDOM = new JSDOMCtor(
37
- "<!doctype html><html><body></body></html>",
38
- { url: "http://localhost/" },
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
- globalThis.window = dom.window as unknown as typeof globalThis.window;
41
- globalThis.document = dom.window.document;
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 { after, afterEach, before, beforeEach, describe, it } from "node:test";
8
- import type { JSDOM } from "jsdom";
9
- import type {
10
- $getRoot as getRootType,
11
- LexicalEditor,
12
- UNDO_COMMAND as undoCommandType,
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 = dom.window.document.createElement("div");
144
- dom.window.document.body.append(container);
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
- dom.window.document.getSelection()?.setPosition(characters(), offset);
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
- dom.window.document.body.querySelector<HTMLElement>(
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
- ...dom.window.document.body.querySelectorAll<HTMLElement>(
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
- dom.window.document.body.querySelector<HTMLElement>(selector);
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 dom.window.MouseEvent(type, { bubbles: true, ...at });
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 dom.window.KeyboardEvent("keydown", {
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 dom.window.KeyboardEvent("keydown", {
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(async () => {
324
- const { JSDOM: JSDOMCtor } = await import("jsdom");
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(dom.window.Element.prototype, "getBoundingClientRect", {
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(dom.window.HTMLElement.prototype, "offsetHeight", {
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(dom.window, "matchMedia", {
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
- dom.window.document
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 dom.window.MouseEvent("contextmenu", {
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 dom.window.KeyboardEvent("keydown", {
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
- dom.window.document.activeElement,
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
- dom.window.document.body.dispatchEvent(
802
- new dom.window.MouseEvent("pointerdown", { bubbles: true }),
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 dom.window.KeyboardEvent("keydown", {
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 { after, afterEach, before, beforeEach, describe, it } from "node:test";
9
- import type { JSDOM } from "jsdom";
10
- import type { $getRoot as getRootType, LexicalEditor } from "lexical";
11
- import type {
12
- act as reactAct,
13
- createElement as reactCreateElement,
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 = dom.window.document.createElement("div");
179
- dom.window.document.body.append(container);
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(async () => {
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(dom.window.Range.prototype, "getBoundingClientRect", {
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();