@remit/ui 0.0.118 → 0.0.120

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@remit/ui",
3
- "version": "0.0.118",
3
+ "version": "0.0.120",
4
4
  "type": "module",
5
5
  "files": [
6
6
  "src"
@@ -8,6 +8,12 @@
8
8
  * A language the build has no dictionary for is not a failure: the provider
9
9
  * answers null and the browser's own checking is switched back on, so the
10
10
  * writer never faces a surface that has silently stopped marking anything.
11
+ *
12
+ * A checker that was supposed to run and is not is the case that has to be
13
+ * loud, in either of the two ways it happens — one that never opens, and one
14
+ * that opens and then stops. Both say so through `onStatus`, both hand the
15
+ * message back to the browser, and the second takes its own marks down on the
16
+ * way out so nothing is left underlined by a checker that is gone.
11
17
  */
12
18
 
13
19
  import assert from "node:assert/strict";
@@ -21,6 +27,7 @@ import type { Root, createRoot as reactCreateRoot } from "react-dom/client";
21
27
  import type { ComposeBody as ComposeBodyType } from "./compose-body.js";
22
28
  import type {
23
29
  CheckRequest,
30
+ ProviderStatus,
24
31
  SpellcheckOptions,
25
32
  SpellProvider,
26
33
  SuggestRequest,
@@ -87,6 +94,84 @@ const recordingSpellcheck = (): {
87
94
  return { options, asked, closed };
88
95
  };
89
96
 
97
+ /** A checker whose engine never loads, so nothing was ever opened to stop. */
98
+ const brokenSpellcheck = (): {
99
+ options: SpellcheckOptions;
100
+ statuses: ProviderStatus[];
101
+ } => {
102
+ const statuses: ProviderStatus[] = [];
103
+ return {
104
+ options: {
105
+ provider: () => Promise.reject(new Error("boom")),
106
+ onStatus: (status) => statuses.push(status),
107
+ },
108
+ statuses,
109
+ };
110
+ };
111
+
112
+ /** A checker that comes up, marks the message, and then stops running. */
113
+ const stoppingSpellcheck = (): {
114
+ options: SpellcheckOptions;
115
+ statuses: ProviderStatus[];
116
+ stop: () => void;
117
+ } => {
118
+ const statuses: ProviderStatus[] = [];
119
+ const listeners: ((status: ProviderStatus) => void)[] = [];
120
+ const options: SpellcheckOptions = {
121
+ provider: (language) => {
122
+ const provider: SpellProvider = {
123
+ language,
124
+ onStatus: (listener) => {
125
+ listeners.push(listener);
126
+ listener({ state: "ready", language });
127
+ return () => {};
128
+ },
129
+ check: (request: CheckRequest) =>
130
+ Promise.resolve({
131
+ requestId: request.requestId,
132
+ revision: request.revision,
133
+ findings: request.spans.map((span) => ({
134
+ spanId: span.spanId,
135
+ start: 0,
136
+ end: 3,
137
+ kind: "spelling" as const,
138
+ suggestions: ["This"],
139
+ })),
140
+ }),
141
+ suggest: (request: SuggestRequest) =>
142
+ Promise.resolve({
143
+ requestId: request.requestId,
144
+ word: request.word,
145
+ suggestions: [],
146
+ }),
147
+ close: () => {},
148
+ };
149
+ return Promise.resolve(provider);
150
+ },
151
+ onStatus: (status) => statuses.push(status),
152
+ };
153
+ return {
154
+ options,
155
+ statuses,
156
+ stop: () => {
157
+ for (const listener of listeners)
158
+ listener({
159
+ state: "failed",
160
+ language: "en",
161
+ reason: "worker",
162
+ detail: "the worker went away",
163
+ });
164
+ },
165
+ };
166
+ };
167
+
168
+ const marked = (): [number, number][] =>
169
+ (
170
+ (
171
+ globalThis as unknown as { CSS: { highlights: Map<string, Marks> } }
172
+ ).CSS.highlights.get("spell-error")?.ranges ?? []
173
+ ).map((range) => [range.startOffset, range.endOffset]);
174
+
90
175
  const settle = async (): Promise<void> => {
91
176
  await act(async () => {
92
177
  await Promise.resolve();
@@ -96,6 +181,14 @@ const settle = async (): Promise<void> => {
96
181
  });
97
182
  };
98
183
 
184
+ /** Long enough for the editor's idle to expire and the pass to come back. */
185
+ const checked = async (): Promise<void> => {
186
+ await act(async () => {
187
+ await new Promise((resolve) => setTimeout(resolve, 300));
188
+ });
189
+ await settle();
190
+ };
191
+
99
192
  const editable = (): HTMLElement => {
100
193
  const surface = container.querySelector<HTMLElement>(
101
194
  "[data-testid=compose-body]",
@@ -248,6 +341,52 @@ describe("the composer's spellchecker", () => {
248
341
  );
249
342
  });
250
343
 
344
+ it("names a checker that never came up, and hands the message back", async () => {
345
+ const { options, statuses } = brokenSpellcheck();
346
+
347
+ await mount(options);
348
+
349
+ assert.deepEqual(
350
+ statuses,
351
+ [{ state: "failed", language: "en", reason: "worker", detail: "boom" }],
352
+ "the composer hears why checking is not happening, in the shape it reports",
353
+ );
354
+ assert.equal(
355
+ editable().getAttribute("spellcheck"),
356
+ "true",
357
+ "a checker that never started leaves the browser's own checking on",
358
+ );
359
+ });
360
+
361
+ it("takes its marks down with a checker that stops mid-message", async () => {
362
+ const { options, statuses, stop } = stoppingSpellcheck();
363
+ await mount(options);
364
+ await checked();
365
+
366
+ assert.deepEqual(
367
+ marked(),
368
+ [[0, 3]],
369
+ "the misspelling is marked while the checker is running",
370
+ );
371
+
372
+ await act(async () => {
373
+ stop();
374
+ });
375
+ await settle();
376
+
377
+ assert.deepEqual(
378
+ statuses.map((status) => status.state),
379
+ ["ready", "failed"],
380
+ "the stop is reported, not swallowed",
381
+ );
382
+ assert.deepEqual(marked(), [], "no mark outlives the checker that drew it");
383
+ assert.equal(
384
+ editable().getAttribute("spellcheck"),
385
+ "true",
386
+ "the browser takes the message back the moment ours stops",
387
+ );
388
+ });
389
+
251
390
  it("leaves the browser to it when the composer is handed no checker", async () => {
252
391
  await act(async () => {
253
392
  root = createRoot(container);
@@ -177,6 +177,20 @@ const characters = (): Node => {
177
177
  return leaf;
178
178
  };
179
179
 
180
+ /**
181
+ * jsdom lays out nothing and moves no caret, so a selection Lexical set on its
182
+ * own — `select()`, or an edit that carries the caret with it — lives only in
183
+ * Lexical's own state. The next commit reconciles against the document's
184
+ * selection, finds none, and puts the anchor back at the start. Everything
185
+ * about a withheld mark is read off the caret at paint time, so a test that
186
+ * left the browser's selection behind is asserting on whichever landed first:
187
+ * the answer to the check, or the reconciliation that undoes the caret. The
188
+ * browser is the one that does this; here the test has to.
189
+ */
190
+ const putCaret = (offset: number): void => {
191
+ dom.window.document.getSelection()?.setPosition(characters(), offset);
192
+ };
193
+
180
194
  /**
181
195
  * The desktop popover portals to the document body, clear of whatever
182
196
  * ancestor would otherwise clip it, so the menu and its rows are found
@@ -217,7 +231,7 @@ const pressDownAt = async (
217
231
  pointerType: string,
218
232
  button = 0,
219
233
  ): Promise<void> => {
220
- dom.window.document.getSelection()?.setPosition(characters(), offset);
234
+ putCaret(offset);
221
235
  await act(async () => {
222
236
  editable().dispatchEvent(
223
237
  pointerEvent("pointerdown", pointerType, {
@@ -268,6 +282,7 @@ const caretAt = async (
268
282
  text?.select(offset, offset);
269
283
  });
270
284
  });
285
+ putCaret(offset);
271
286
  };
272
287
 
273
288
  /** A key on the writing surface, whatever it turns out to do. */
@@ -745,6 +760,7 @@ describe("the correction menu", () => {
745
760
  text?.spliceText(18, 0, "y", true);
746
761
  });
747
762
  });
763
+ putCaret(19);
748
764
  await settle();
749
765
 
750
766
  assert.equal(editable().textContent, "Ths report is redyy today");