@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
|
@@ -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
|
-
|
|
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");
|