@remit/ui 0.0.119 → 0.0.121
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 +4 -1
- package/src/components/compose-body.spellcheck.test.ts +5 -2
- package/src/components/rich-text-editor.stories.tsx +190 -16
- package/src/components/rich-text-editor.tsx +72 -29
- package/src/components/rich-text-spellcheck-double.ts +65 -0
- package/src/components/rich-text-spellcheck-engine.ts +267 -0
- package/src/components/rich-text-spellcheck-languages.ts +62 -0
- package/src/components/rich-text-spellcheck-menu.test.ts +23 -8
- package/src/components/rich-text-spellcheck-notice.tsx +168 -0
- package/src/components/rich-text-spellcheck-provider.test.ts +296 -197
- package/src/components/rich-text-spellcheck-provider.ts +64 -10
- package/src/components/rich-text-spellcheck-words.ts +16 -354
- package/src/components/rich-text-spellcheck-worker-provider.ts +17 -7
- package/src/components/rich-text-spellcheck-worker.test.ts +253 -0
- package/src/components/rich-text-spellcheck-worker.ts +63 -15
- package/src/components/rich-text-spellcheck.test.ts +123 -9
- package/src/components/rich-text-spellcheck.ts +31 -2
- package/src/rich-text.ts +1 -1
|
@@ -1,246 +1,312 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* The provider
|
|
3
|
-
*
|
|
4
|
-
*
|
|
2
|
+
* The provider against the messages a worker sends, without a worker: what is
|
|
3
|
+
* under test is the protocol — which status a message publishes, which request
|
|
4
|
+
* an answer settles, and what a menu is told when nothing comes back. The
|
|
5
|
+
* engine itself is proved against real dictionaries in
|
|
6
|
+
* `rich-text-spellcheck-worker.test.ts`.
|
|
5
7
|
*/
|
|
6
8
|
import assert from "node:assert/strict";
|
|
7
|
-
import {
|
|
9
|
+
import { describe, it, mock } from "node:test";
|
|
8
10
|
import type {
|
|
9
11
|
ProviderStatus,
|
|
10
12
|
SpellWorkerRequest,
|
|
11
13
|
SpellWorkerResponse,
|
|
12
14
|
} from "./rich-text-spellcheck.js";
|
|
13
15
|
import {
|
|
16
|
+
dictionaryTagFor,
|
|
17
|
+
spellcheckBase,
|
|
18
|
+
spellcheckBytes,
|
|
19
|
+
spellcheckLanguages,
|
|
20
|
+
} from "./rich-text-spellcheck-languages.js";
|
|
21
|
+
import {
|
|
22
|
+
CHECK_DEADLINE_MS,
|
|
14
23
|
openSpellProvider,
|
|
15
24
|
type SpellWorkerPort,
|
|
16
25
|
SUGGEST_DEADLINE_MS,
|
|
17
26
|
} from "./rich-text-spellcheck-provider.js";
|
|
18
27
|
import {
|
|
19
|
-
dictionaryFor,
|
|
20
28
|
findMisspellings,
|
|
21
|
-
|
|
29
|
+
normaliseWord,
|
|
30
|
+
wordsIn,
|
|
22
31
|
} from "./rich-text-spellcheck-words.js";
|
|
23
32
|
|
|
24
|
-
interface
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
):
|
|
33
|
+
interface Wire {
|
|
34
|
+
readonly port: SpellWorkerPort;
|
|
35
|
+
readonly posted: SpellWorkerRequest[];
|
|
36
|
+
answer(message: SpellWorkerResponse): void;
|
|
37
|
+
fall(detail: string): void;
|
|
38
|
+
terminated(): number;
|
|
30
39
|
}
|
|
31
40
|
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
let
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
postMessage: (message) => deliver?.(message),
|
|
51
|
-
addEventListener: (_type, listener) => {
|
|
52
|
-
receive = listener;
|
|
41
|
+
const wire = (): Wire => {
|
|
42
|
+
const posted: SpellWorkerRequest[] = [];
|
|
43
|
+
let deliver: ((message: SpellWorkerResponse) => void) | undefined;
|
|
44
|
+
let raise: ((detail: string) => void) | undefined;
|
|
45
|
+
let terminated = 0;
|
|
46
|
+
return {
|
|
47
|
+
posted,
|
|
48
|
+
port: {
|
|
49
|
+
post: (message) => posted.push(message),
|
|
50
|
+
listen: (listener) => {
|
|
51
|
+
deliver = listener;
|
|
52
|
+
},
|
|
53
|
+
fail: (listener) => {
|
|
54
|
+
raise = listener;
|
|
55
|
+
},
|
|
56
|
+
terminate: () => {
|
|
57
|
+
terminated += 1;
|
|
58
|
+
},
|
|
53
59
|
},
|
|
60
|
+
answer: (message) => deliver?.(message),
|
|
61
|
+
fall: (detail) => raise?.(detail),
|
|
62
|
+
terminated: () => terminated,
|
|
54
63
|
};
|
|
55
|
-
|
|
56
|
-
value: scope.postMessage,
|
|
57
|
-
configurable: true,
|
|
58
|
-
});
|
|
59
|
-
Object.defineProperty(globalThis, "addEventListener", {
|
|
60
|
-
value: scope.addEventListener,
|
|
61
|
-
configurable: true,
|
|
62
|
-
});
|
|
63
|
-
await import("./rich-text-spellcheck-worker.js");
|
|
64
|
-
});
|
|
64
|
+
};
|
|
65
65
|
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
"
|
|
66
|
+
const ready = (wired: Wire, language: string): void =>
|
|
67
|
+
wired.answer({ type: "ready", language });
|
|
68
|
+
|
|
69
|
+
describe("the tokeniser", () => {
|
|
70
|
+
it("takes words and leaves single letters alone", () => {
|
|
71
|
+
assert.deepEqual(
|
|
72
|
+
wordsIn("A report is redy").map((range) => range.start),
|
|
73
|
+
[2, 9, 12],
|
|
74
|
+
"an initial is not a word to check",
|
|
73
75
|
);
|
|
74
76
|
});
|
|
75
77
|
|
|
76
|
-
it("ranges the
|
|
77
|
-
const
|
|
78
|
-
assert.
|
|
79
|
-
assert.deepEqual(findMisspellings("Ths report is redy today", words), [
|
|
78
|
+
it("ranges only what the checker does not know", () => {
|
|
79
|
+
const known = (word: string) => word.toLowerCase() !== "ths";
|
|
80
|
+
assert.deepEqual(findMisspellings("Ths report", known), [
|
|
80
81
|
{ start: 0, end: 3 },
|
|
81
|
-
{ start: 14, end: 18 },
|
|
82
82
|
]);
|
|
83
|
-
assert.deepEqual(
|
|
84
|
-
findMisspellings("A report", words),
|
|
85
|
-
[],
|
|
86
|
-
"a single letter is not a word to check",
|
|
87
|
-
);
|
|
88
83
|
});
|
|
89
84
|
|
|
90
|
-
it("
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
85
|
+
it("reads a curly apostrophe as the straight one", () => {
|
|
86
|
+
assert.equal(normaliseWord("Don’t"), "don't");
|
|
87
|
+
});
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* The build writes these in; a test process has neither, so each one is put
|
|
92
|
+
* where the compiled define would have been and taken away again.
|
|
93
|
+
*/
|
|
94
|
+
const staged = (
|
|
95
|
+
values: { base?: string; bytes?: Record<string, number>; baseURI?: string },
|
|
96
|
+
use: () => void,
|
|
97
|
+
): void => {
|
|
98
|
+
const names = ["__REMIT_SPELLCHECK_BASE__", "__REMIT_SPELLCHECK_BYTES__"];
|
|
99
|
+
if (values.base !== undefined)
|
|
100
|
+
Object.defineProperty(globalThis, names[0], {
|
|
101
|
+
value: values.base,
|
|
102
|
+
configurable: true,
|
|
103
|
+
});
|
|
104
|
+
if (values.bytes !== undefined)
|
|
105
|
+
Object.defineProperty(globalThis, names[1], {
|
|
106
|
+
value: values.bytes,
|
|
107
|
+
configurable: true,
|
|
108
|
+
});
|
|
109
|
+
if (values.baseURI !== undefined)
|
|
110
|
+
Object.defineProperty(globalThis, "document", {
|
|
111
|
+
value: { baseURI: values.baseURI },
|
|
112
|
+
configurable: true,
|
|
113
|
+
});
|
|
114
|
+
try {
|
|
115
|
+
use();
|
|
116
|
+
} finally {
|
|
117
|
+
for (const name of [...names, "document"])
|
|
118
|
+
Reflect.deleteProperty(globalThis, name);
|
|
119
|
+
}
|
|
120
|
+
};
|
|
121
|
+
|
|
122
|
+
describe("what this build carries", () => {
|
|
123
|
+
it("has nothing where no build staged anything", () => {
|
|
124
|
+
assert.deepEqual(spellcheckLanguages(), []);
|
|
125
|
+
assert.equal(spellcheckBase(), "/spellcheck/");
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
// Storybook builds relative and is published under /reader/pr/<n>/<sha>/,
|
|
129
|
+
// which no absolute path baked at build time can name. Resolving against the
|
|
130
|
+
// document is what makes the spellcheck stories work anywhere they land.
|
|
131
|
+
it("resolves a relative base against the page it is on", () => {
|
|
132
|
+
staged(
|
|
133
|
+
{
|
|
134
|
+
base: "spellcheck/0123456789abcdef/",
|
|
135
|
+
baseURI:
|
|
136
|
+
"https://remit-mail.github.io/reader/pr/755/abc123/iframe.html",
|
|
137
|
+
},
|
|
138
|
+
() =>
|
|
139
|
+
assert.equal(
|
|
140
|
+
spellcheckBase(),
|
|
141
|
+
"https://remit-mail.github.io/reader/pr/755/abc123/spellcheck/0123456789abcdef/",
|
|
142
|
+
),
|
|
98
143
|
);
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
it("leaves an app's own absolute base where it is", () => {
|
|
147
|
+
staged(
|
|
148
|
+
{
|
|
149
|
+
base: "/spellcheck/0123456789abcdef/",
|
|
150
|
+
baseURI: "https://mail.example.com/mail/inbox/42",
|
|
151
|
+
},
|
|
152
|
+
() =>
|
|
153
|
+
assert.equal(
|
|
154
|
+
spellcheckBase(),
|
|
155
|
+
"https://mail.example.com/spellcheck/0123456789abcdef/",
|
|
156
|
+
),
|
|
103
157
|
);
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
it("knows what opening a language weighs", () => {
|
|
161
|
+
staged({ bytes: { nl: 2_465_792 } }, () => {
|
|
162
|
+
assert.equal(spellcheckBytes("nl"), 2_465_792);
|
|
163
|
+
assert.equal(spellcheckBytes("de"), 0, "a language nothing staged");
|
|
164
|
+
});
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
it("takes the region dictionary where the build has it", () => {
|
|
168
|
+
const built = ["en", "en-GB", "nl"];
|
|
169
|
+
assert.equal(dictionaryTagFor("en-GB", built), "en-GB");
|
|
170
|
+
assert.equal(dictionaryTagFor("EN-gb", built), "en-GB");
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
it("falls back to the language where the region is not staged", () => {
|
|
174
|
+
assert.equal(dictionaryTagFor("nl-BE", ["en", "nl"]), "nl");
|
|
104
175
|
assert.equal(
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
"
|
|
108
|
-
);
|
|
109
|
-
assert.deepEqual(
|
|
110
|
-
suggestionsFor("qwertyuiop", words),
|
|
111
|
-
[],
|
|
112
|
-
"nothing near it is not the same as anything at all",
|
|
113
|
-
);
|
|
114
|
-
assert.deepEqual(
|
|
115
|
-
suggestionsFor("report", words),
|
|
116
|
-
[],
|
|
117
|
-
"a word it holds needs no correcting",
|
|
176
|
+
dictionaryTagFor("de", ["en", "nl"]),
|
|
177
|
+
null,
|
|
178
|
+
"no dictionary is not an empty one",
|
|
118
179
|
);
|
|
119
180
|
});
|
|
120
181
|
});
|
|
121
182
|
|
|
122
183
|
describe("a provider over a worker", () => {
|
|
123
|
-
it("opens,
|
|
124
|
-
const
|
|
125
|
-
const provider = openSpellProvider(
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
{ state: "ready", language: "en" },
|
|
131
|
-
"a listener that subscribes late still learns the worker is up",
|
|
184
|
+
it("opens on the language, the place the build serves it from, and its weight", () => {
|
|
185
|
+
const wired = wire();
|
|
186
|
+
const provider = openSpellProvider(
|
|
187
|
+
"nl",
|
|
188
|
+
"/spellcheck/",
|
|
189
|
+
wired.port,
|
|
190
|
+
2_465_792,
|
|
132
191
|
);
|
|
133
|
-
|
|
134
|
-
const response = await provider.check({
|
|
135
|
-
requestId: "7",
|
|
136
|
-
language: "en",
|
|
137
|
-
revision: 3,
|
|
138
|
-
spans: [{ spanId: "a", text: "Ths report" }],
|
|
139
|
-
});
|
|
140
|
-
|
|
141
|
-
assert.equal(response.requestId, "7");
|
|
142
|
-
assert.equal(response.revision, 3, "the answer carries the revision asked");
|
|
143
|
-
assert.deepEqual(response.findings, [
|
|
192
|
+
assert.deepEqual(wired.posted, [
|
|
144
193
|
{
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
suggestions: [],
|
|
194
|
+
type: "open",
|
|
195
|
+
language: "nl",
|
|
196
|
+
base: "/spellcheck/",
|
|
197
|
+
bytesExpected: 2_465_792,
|
|
150
198
|
},
|
|
151
199
|
]);
|
|
152
|
-
|
|
153
|
-
unsubscribe();
|
|
154
200
|
provider.close();
|
|
155
|
-
assert.equal(terminated, 1, "closing the provider takes the worker down");
|
|
156
201
|
});
|
|
157
202
|
|
|
158
|
-
it("
|
|
159
|
-
const
|
|
160
|
-
const [
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
203
|
+
it("carries the download rather than a verdict about it", () => {
|
|
204
|
+
const wired = wire();
|
|
205
|
+
const seen: ProviderStatus[] = [];
|
|
206
|
+
const provider = openSpellProvider("nl", "/spellcheck/", wired.port);
|
|
207
|
+
provider.onStatus((status) => seen.push(status));
|
|
208
|
+
|
|
209
|
+
assert.deepEqual(seen.at(-1), {
|
|
210
|
+
state: "opening",
|
|
211
|
+
language: "nl",
|
|
212
|
+
bytesLoaded: 0,
|
|
213
|
+
bytesTotal: 0,
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
wired.answer({
|
|
217
|
+
type: "opening",
|
|
218
|
+
language: "nl",
|
|
219
|
+
bytesLoaded: 65_536,
|
|
220
|
+
bytesTotal: 702_464,
|
|
221
|
+
});
|
|
174
222
|
|
|
175
|
-
assert.
|
|
176
|
-
|
|
177
|
-
|
|
223
|
+
assert.deepEqual(
|
|
224
|
+
seen.at(-1),
|
|
225
|
+
{
|
|
226
|
+
state: "opening",
|
|
227
|
+
language: "nl",
|
|
228
|
+
bytesLoaded: 65_536,
|
|
229
|
+
bytesTotal: 702_464,
|
|
230
|
+
},
|
|
231
|
+
"the two numbers reach whatever decides five seconds is long",
|
|
232
|
+
);
|
|
178
233
|
provider.close();
|
|
179
234
|
});
|
|
180
235
|
|
|
181
|
-
it("
|
|
182
|
-
const
|
|
183
|
-
const
|
|
184
|
-
|
|
236
|
+
it("settles each request with its own answer", async () => {
|
|
237
|
+
const wired = wire();
|
|
238
|
+
const provider = openSpellProvider("en", "/spellcheck/", wired.port);
|
|
239
|
+
ready(wired, "en");
|
|
240
|
+
|
|
241
|
+
const first = provider.check({
|
|
242
|
+
requestId: "1",
|
|
185
243
|
language: "en",
|
|
186
|
-
|
|
244
|
+
revision: 1,
|
|
245
|
+
spans: [{ spanId: "a", text: "Ths report" }],
|
|
246
|
+
});
|
|
247
|
+
const second = provider.check({
|
|
248
|
+
requestId: "2",
|
|
249
|
+
language: "en",
|
|
250
|
+
revision: 2,
|
|
251
|
+
spans: [{ spanId: "b", text: "the report" }],
|
|
187
252
|
});
|
|
188
253
|
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
254
|
+
wired.answer({
|
|
255
|
+
type: "checked",
|
|
256
|
+
requestId: "2",
|
|
257
|
+
revision: 2,
|
|
258
|
+
findings: [],
|
|
259
|
+
});
|
|
260
|
+
wired.answer({
|
|
261
|
+
type: "checked",
|
|
262
|
+
requestId: "1",
|
|
263
|
+
revision: 1,
|
|
264
|
+
findings: [
|
|
265
|
+
{ spanId: "a", start: 0, end: 3, kind: "spelling", suggestions: [] },
|
|
266
|
+
],
|
|
267
|
+
});
|
|
268
|
+
|
|
269
|
+
assert.equal((await second).findings.length, 0);
|
|
270
|
+
assert.equal(
|
|
271
|
+
(await first).revision,
|
|
272
|
+
1,
|
|
273
|
+
"the answer carries what was asked",
|
|
274
|
+
);
|
|
192
275
|
provider.close();
|
|
276
|
+
assert.equal(wired.terminated(), 1, "closing takes the worker down");
|
|
193
277
|
});
|
|
194
278
|
|
|
195
279
|
it("tells a waiting menu when the worker stops answering", async () => {
|
|
196
|
-
|
|
197
|
-
const provider = openSpellProvider("en",
|
|
198
|
-
...port,
|
|
199
|
-
post: () => {},
|
|
200
|
-
listen: () => {},
|
|
201
|
-
fail: (listener) => {
|
|
202
|
-
raise = listener;
|
|
203
|
-
},
|
|
204
|
-
});
|
|
280
|
+
const wired = wire();
|
|
281
|
+
const provider = openSpellProvider("en", "/spellcheck/", wired.port);
|
|
205
282
|
const asking = provider.suggest({
|
|
206
283
|
requestId: "1",
|
|
207
284
|
language: "en",
|
|
208
285
|
word: "redy",
|
|
209
286
|
});
|
|
210
|
-
|
|
287
|
+
wired.fall("worker exited");
|
|
211
288
|
|
|
212
289
|
await assert.rejects(asking, /worker exited/);
|
|
213
290
|
provider.close();
|
|
214
291
|
});
|
|
215
292
|
|
|
216
293
|
it("tells a waiting menu when the checker closes under it", async () => {
|
|
217
|
-
const
|
|
218
|
-
|
|
219
|
-
post: () => {},
|
|
220
|
-
listen: () => {},
|
|
221
|
-
});
|
|
294
|
+
const wired = wire();
|
|
295
|
+
const provider = openSpellProvider("en", "/spellcheck/", wired.port);
|
|
222
296
|
const asking = provider.suggest({
|
|
223
297
|
requestId: "1",
|
|
224
298
|
language: "en",
|
|
225
299
|
word: "redy",
|
|
226
300
|
});
|
|
227
|
-
|
|
228
301
|
provider.close();
|
|
229
302
|
|
|
230
|
-
await assert.rejects(
|
|
231
|
-
asking,
|
|
232
|
-
/the checker closed/,
|
|
233
|
-
"a menu waiting on a provider that went away hears about it",
|
|
234
|
-
);
|
|
303
|
+
await assert.rejects(asking, /the checker closed/);
|
|
235
304
|
});
|
|
236
305
|
|
|
237
306
|
it("gives up on a worker that took the word and went quiet", async () => {
|
|
238
307
|
mock.timers.enable({ apis: ["setTimeout"] });
|
|
239
|
-
const
|
|
240
|
-
|
|
241
|
-
post: () => {},
|
|
242
|
-
listen: () => {},
|
|
243
|
-
});
|
|
308
|
+
const wired = wire();
|
|
309
|
+
const provider = openSpellProvider("en", "/spellcheck/", wired.port);
|
|
244
310
|
const asking = provider.suggest({
|
|
245
311
|
requestId: "1",
|
|
246
312
|
language: "en",
|
|
@@ -258,66 +324,99 @@ describe("a provider over a worker", () => {
|
|
|
258
324
|
provider.close();
|
|
259
325
|
});
|
|
260
326
|
|
|
261
|
-
it("
|
|
262
|
-
|
|
263
|
-
const
|
|
327
|
+
it("calls a checker that took the pass and went quiet stopped", async () => {
|
|
328
|
+
mock.timers.enable({ apis: ["setTimeout"] });
|
|
329
|
+
const wired = wire();
|
|
330
|
+
const seen: ProviderStatus[] = [];
|
|
331
|
+
const provider = openSpellProvider("nl", "/spellcheck/", wired.port);
|
|
332
|
+
provider.onStatus((status) => seen.push(status));
|
|
333
|
+
ready(wired, "nl");
|
|
334
|
+
|
|
335
|
+
const pass = provider.check({
|
|
264
336
|
requestId: "1",
|
|
265
|
-
language: "
|
|
266
|
-
revision:
|
|
267
|
-
spans: [{ spanId: "a", text: "
|
|
337
|
+
language: "nl",
|
|
338
|
+
revision: 7,
|
|
339
|
+
spans: [{ spanId: "a", text: "De vergaderingg" }],
|
|
268
340
|
});
|
|
269
341
|
|
|
342
|
+
mock.timers.tick(CHECK_DEADLINE_MS);
|
|
343
|
+
mock.timers.reset();
|
|
344
|
+
|
|
270
345
|
assert.deepEqual(
|
|
271
|
-
|
|
272
|
-
[],
|
|
273
|
-
"a
|
|
346
|
+
await pass,
|
|
347
|
+
{ requestId: "1", revision: 7, findings: [] },
|
|
348
|
+
"a pass nobody answers ends, rather than hanging on a promise",
|
|
349
|
+
);
|
|
350
|
+
const last = seen.at(-1);
|
|
351
|
+
assert.ok(last?.state === "failed", "a frozen engine is a failure");
|
|
352
|
+
assert.equal(last.reason, "worker");
|
|
353
|
+
assert.match(
|
|
354
|
+
last.detail,
|
|
355
|
+
/nl checker did not answer/,
|
|
356
|
+
"a frozen engine is indistinguishable from clean text unless it says so",
|
|
357
|
+
);
|
|
358
|
+
provider.close();
|
|
359
|
+
});
|
|
360
|
+
|
|
361
|
+
it("says a checker stopped once, however many passes were waiting", async () => {
|
|
362
|
+
mock.timers.enable({ apis: ["setTimeout"] });
|
|
363
|
+
const wired = wire();
|
|
364
|
+
const seen: ProviderStatus[] = [];
|
|
365
|
+
const provider = openSpellProvider("en", "/spellcheck/", wired.port);
|
|
366
|
+
provider.onStatus((status) => seen.push(status));
|
|
367
|
+
ready(wired, "en");
|
|
368
|
+
|
|
369
|
+
const passes = [1, 2].map((nth) =>
|
|
370
|
+
provider.check({
|
|
371
|
+
requestId: `${nth}`,
|
|
372
|
+
language: "en",
|
|
373
|
+
revision: nth,
|
|
374
|
+
spans: [{ spanId: "a", text: "Ths report" }],
|
|
375
|
+
}),
|
|
376
|
+
);
|
|
377
|
+
|
|
378
|
+
mock.timers.tick(CHECK_DEADLINE_MS);
|
|
379
|
+
mock.timers.reset();
|
|
380
|
+
|
|
381
|
+
await Promise.all(passes);
|
|
382
|
+
assert.equal(
|
|
383
|
+
seen.filter((status) => status.state === "failed").length,
|
|
384
|
+
1,
|
|
385
|
+
"one stopped checker is one banner",
|
|
274
386
|
);
|
|
275
387
|
provider.close();
|
|
276
388
|
});
|
|
277
389
|
|
|
278
390
|
it("passes on a failure the worker names itself", () => {
|
|
391
|
+
const wired = wire();
|
|
279
392
|
const seen: ProviderStatus[] = [];
|
|
280
|
-
|
|
281
|
-
const provider = openSpellProvider("nl", {
|
|
282
|
-
...port,
|
|
283
|
-
post: () => {},
|
|
284
|
-
listen: (listener) => {
|
|
285
|
-
answer = listener;
|
|
286
|
-
},
|
|
287
|
-
});
|
|
393
|
+
const provider = openSpellProvider("nl", "/spellcheck/", wired.port);
|
|
288
394
|
provider.onStatus((status) => seen.push(status));
|
|
289
|
-
answer
|
|
395
|
+
wired.answer({
|
|
290
396
|
type: "failed",
|
|
291
397
|
language: "nl",
|
|
292
398
|
reason: "download",
|
|
293
|
-
detail: "
|
|
399
|
+
detail: "/spellcheck/dictionaries/nl/index.dic answered 503",
|
|
294
400
|
});
|
|
295
401
|
|
|
296
402
|
assert.deepEqual(seen.at(-1), {
|
|
297
403
|
state: "failed",
|
|
298
404
|
language: "nl",
|
|
299
405
|
reason: "download",
|
|
300
|
-
detail: "
|
|
406
|
+
detail: "/spellcheck/dictionaries/nl/index.dic answered 503",
|
|
301
407
|
});
|
|
302
408
|
provider.close();
|
|
303
409
|
});
|
|
304
410
|
|
|
305
411
|
it("reports a worker that fell over", () => {
|
|
412
|
+
const wired = wire();
|
|
306
413
|
const seen: ProviderStatus[] = [];
|
|
307
|
-
|
|
308
|
-
const provider = openSpellProvider("en", {
|
|
309
|
-
...port,
|
|
310
|
-
post: () => {},
|
|
311
|
-
listen: () => {},
|
|
312
|
-
fail: (listener) => {
|
|
313
|
-
raise = listener;
|
|
314
|
-
},
|
|
315
|
-
});
|
|
414
|
+
const provider = openSpellProvider("en", "/spellcheck/", wired.port);
|
|
316
415
|
provider.onStatus((status) => seen.push(status));
|
|
317
|
-
|
|
416
|
+
wired.fall("worker exited");
|
|
318
417
|
|
|
319
418
|
assert.deepEqual(seen, [
|
|
320
|
-
{ state: "opening", language: "en" },
|
|
419
|
+
{ state: "opening", language: "en", bytesLoaded: 0, bytesTotal: 0 },
|
|
321
420
|
{
|
|
322
421
|
state: "failed",
|
|
323
422
|
language: "en",
|