@remit/web-client 0.0.199 → 0.0.201

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.
@@ -1,285 +0,0 @@
1
- import assert from "node:assert/strict";
2
- import { readdirSync, readFileSync } from "node:fs";
3
- import { dirname, resolve } from "node:path";
4
- import { describe, it } from "node:test";
5
- import { fileURLToPath } from "node:url";
6
-
7
- /**
8
- * One invariant, over every surface: a verb aimed at a selection reaches the
9
- * mail server only through the wizard, whose review screen names what it covers
10
- * (#477 1.4 and its FAQ, #508).
11
- *
12
- * There are two ways to ask for one — the selection bar and the keyboard — and
13
- * three surfaces that raise a bar. Checked here rather than by a spec per
14
- * surface, because what has repeatedly gone wrong is not a broken route but a
15
- * missing one: a fourth surface, or a sixth verb, quietly keeping a direct
16
- * handler while every other route was moved. That is invisible in review and
17
- * indistinguishable from correct until someone selects 3,412 messages and
18
- * presses a key.
19
- *
20
- * These read the source rather than rendering, for the reason
21
- * `MessageList.selection.test.ts` gives: these components wire the DOM, the
22
- * router and several data hooks together, and the rule is about which function
23
- * a prop names.
24
- */
25
-
26
- const here = dirname(fileURLToPath(import.meta.url));
27
- const read = (file: string): string =>
28
- readFileSync(resolve(here, file), "utf8");
29
-
30
- /** Every verb the bar can carry — the props `SelectionTopBar` accepts. */
31
- const BAR_VERB_PROPS = [
32
- "onDelete",
33
- "onMove",
34
- "onOrganize",
35
- "onJunk",
36
- "onMarkRead",
37
- ] as const;
38
-
39
- /**
40
- * How a surface is allowed to answer a verb prop. `startWizard`/`wizard.start`/
41
- * `startSelectionVerb` all open the wizard on the verb; `organizeSelection` and
42
- * `startFromSearch` open it on the search entry, which is the same wizard
43
- * reached by its other door (#477 1.8). Anything else is a route around the
44
- * review screen.
45
- */
46
- const OPENS_THE_WIZARD =
47
- /^(startWizard|wizard\.start|startSelectionVerb|organizeSelection|startFromSearch)\b/;
48
-
49
- const surfaceFiles = (): string[] =>
50
- readdirSync(here)
51
- .filter((file) => file.endsWith(".tsx"))
52
- .filter((file) => /<SelectionTopBar/.test(read(file)));
53
-
54
- /**
55
- * The bar element as written, to its own closing tag. Found by matching tags
56
- * rather than by stopping at the first `/>` on a line: a nested self-closing
57
- * element matches that too, so the scan would end early and silently skip
58
- * whatever a surface happened to write after it.
59
- */
60
- const barMarkup = (source: string): string => {
61
- const start = source.indexOf("<SelectionTopBar");
62
- if (start === -1) return "";
63
- let depth = 0;
64
- for (let i = start; i < source.length; i++) {
65
- if (source[i] === "<" && /[A-Za-z]/.test(source[i + 1] ?? "")) depth++;
66
- else if (source.startsWith("/>", i)) {
67
- depth--;
68
- if (depth === 0) return source.slice(start, i + 2);
69
- i++;
70
- } else if (source.startsWith("</", i)) depth--;
71
- }
72
- return "";
73
- };
74
-
75
- /** The body of `const <name> = useCallback(…)`, or of an inline arrow value. */
76
- const bracedFrom = (source: string, open: number): string => {
77
- let depth = 0;
78
- for (let i = open; i < source.length; i++) {
79
- if (source[i] === "{") depth++;
80
- else if (source[i] === "}") {
81
- depth--;
82
- if (depth === 0) return source.slice(open, i + 1);
83
- }
84
- }
85
- return source.slice(open);
86
- };
87
-
88
- const namedCallbackBody = (source: string, name: string): string => {
89
- const at = source.indexOf(`const ${name} = useCallback(`);
90
- if (at === -1) return "";
91
- const open = source.indexOf("{", at);
92
- return open === -1 ? "" : bracedFrom(source, open);
93
- };
94
-
95
- /** The triage handlers a pane wires into the keyboard layer, by name. */
96
- const declaredHandlers = (source: string): string[] => {
97
- const at = source.indexOf("handlers: {");
98
- if (at === -1) return [];
99
- return Array.from(
100
- bracedFrom(source, source.indexOf("{", at)).matchAll(/\n\t{3}(\w+): /g),
101
- (match) => match[1],
102
- );
103
- };
104
-
105
- /**
106
- * What a pane does for one keyboard verb: the handler it wired into the triage
107
- * layer, plus any callback that handler defers to for its target. A handler
108
- * that offers the list the press and then falls through to a helper picking the
109
- * selection is exactly as wrong as one picking it inline, so both are read.
110
- */
111
- const paneVerbBody = (source: string, handler: string): string => {
112
- const at = source.search(new RegExp(`\\n\\t{3}${handler}: `));
113
- if (at === -1) return "";
114
- const value =
115
- source.slice(at).match(new RegExp(`${handler}: (.*)`))?.[1] ?? "";
116
- const body = value.startsWith("(")
117
- ? bracedFrom(source, source.indexOf("{", at))
118
- : namedCallbackBody(source, value.replace(/[,\s].*$/, ""));
119
- const helpers = Array.from(body.matchAll(/\b(\w+MessageIds)\(\)/g), (match) =>
120
- namedCallbackBody(source, match[1]),
121
- );
122
- return [body, ...helpers].join("\n");
123
- };
124
-
125
- /**
126
- * What a verb prop is wired to, with the whitespace, the guard a verb is
127
- * offered behind, and the arrow taken off — what is left is the call itself.
128
- */
129
- const verbHandler = (bar: string, prop: string): string | undefined => {
130
- const assigned = bar.match(new RegExp(`${prop}=\\{([\\s\\S]*?)\\}\\n`))?.[1];
131
- if (assigned === undefined) return undefined;
132
- return assigned
133
- .replace(/\s+/g, " ")
134
- .replace(/^.*? \? /, "")
135
- .replace(/ : undefined$/, "")
136
- .replace(/^\(\)\s*=>\s*/, "")
137
- .trim();
138
- };
139
-
140
- describe("every verb on a selection bar opens the wizard", () => {
141
- it("covers the surfaces that raise one", () => {
142
- assert.deepEqual(surfaceFiles().sort(), [
143
- "DailyBrief.tsx",
144
- "MessageList.tsx",
145
- "ThreadListInteraction.tsx",
146
- ]);
147
- });
148
-
149
- for (const file of surfaceFiles()) {
150
- it(`${file} routes each verb it offers through the wizard`, () => {
151
- const bar = barMarkup(read(file));
152
- assert.notEqual(bar, "", `${file} renders a SelectionTopBar`);
153
- const offered = BAR_VERB_PROPS.filter((prop) => bar.includes(`${prop}=`));
154
- assert.ok(offered.length > 0, `${file} offers at least one verb`);
155
- for (const prop of offered) {
156
- const handler = verbHandler(bar, prop);
157
- assert.ok(handler, `${file}'s ${prop} is wired to something`);
158
- assert.match(
159
- handler,
160
- OPENS_THE_WIZARD,
161
- `${file}'s ${prop} must open the wizard, not run the verb`,
162
- );
163
- }
164
- });
165
- }
166
- });
167
-
168
- /**
169
- * The keyboard's half. A list offers the pane one seam — `requestVerb` — and
170
- * claims the press whenever it has a selection, so a pane's own handler only
171
- * ever sees a verb aimed at the bare cursor. A pane that runs a verb over
172
- * `selectedIds` without offering the list the press first is the hole this
173
- * binds shut.
174
- */
175
- describe("every keyboard verb over a selection goes through the list", () => {
176
- const LISTS = ["MessageList.tsx", "ThreadListInteraction.tsx"];
177
- const PANES = ["MailboxPane.tsx", "BriefPane.tsx", "FlaggedPane.tsx"];
178
- /**
179
- * The triage handlers that can be aimed at a selection, each with the wizard
180
- * verb it has to route to. Star is a knowing carve-out: it is not one of the
181
- * five verbs the wizard walks, it is not on `SelectionTopBar`, and it sets a
182
- * flag on mail already in front of the user that the same key unsets — so
183
- * there is nothing for a review screen to name. `LabelApplyTrigger` applies
184
- * labels to the selection directly for the same reason, and because labels
185
- * are out of scope for the epic that put the wizard in front of the rest
186
- * (#477 §6). Mute and block are per-sender and reach no selection at all.
187
- */
188
- const PANE_VERB_ROUTE: Record<string, string> = {
189
- delete: "delete",
190
- toggleRead: "markRead",
191
- markJunk: "junk",
192
- };
193
-
194
- /**
195
- * How a list may answer a verb aimed at the bare cursor: with the shared
196
- * decision, whose rules are in `../../lib/list-verb-request.test.ts`, or
197
- * inline. Either way only Delete is the list's.
198
- */
199
- const CURSOR_DELETE_ONLY = /listVerbRequest\(\{|if \(verb !== "delete"/;
200
-
201
- for (const file of LISTS) {
202
- it(`${file} publishes the one seam and claims a selection`, () => {
203
- const source = read(file);
204
- assert.match(source, /requestVerb,/);
205
- assert.match(
206
- source,
207
- CURSOR_DELETE_ONLY,
208
- "only delete is the list's over a bare cursor",
209
- );
210
- assert.doesNotMatch(
211
- source,
212
- /requestDelete:/,
213
- "one seam, so no verb can have a second one",
214
- );
215
- });
216
-
217
- it(`${file} stands its keyboard down while the wizard is up`, () => {
218
- assert.match(
219
- read(file),
220
- /blocksKeyboard: confirmOpen \|\| wizard/,
221
- "a shortcut must not act behind the screen already asking",
222
- );
223
- });
224
- }
225
-
226
- // Only the handlers a pane really declares get a case, so a pane that never
227
- // wired one cannot pass by skipping — the roll-call below is what notices a
228
- // handler nobody routed.
229
- for (const file of PANES) {
230
- for (const handler of declaredHandlers(read(file))) {
231
- const verb = PANE_VERB_ROUTE[handler];
232
- if (verb === undefined) continue;
233
-
234
- it(`${file}'s ${handler} offers the list ${verb} first`, () => {
235
- const body = paneVerbBody(read(file), handler);
236
- assert.notEqual(body, "", `${handler}'s body was found`);
237
- assert.match(
238
- body,
239
- new RegExp(`requestVerb\\("${verb}"\\)`),
240
- `${handler} must hand ${verb} to the list before running it`,
241
- );
242
- });
243
-
244
- it(`${file}'s ${handler} aims at the cursor once the list declines`, () => {
245
- const body = paneVerbBody(read(file), handler);
246
- assert.notEqual(body, "", `${handler}'s body was found`);
247
- // The list declines only when its commands are gone, which an
248
- // escalated selection outlives — so a fallback that could still see
249
- // a selection would run the verb over a stale snapshot of rows that
250
- // are no longer loaded, with no wizard in front of it.
251
- assert.doesNotMatch(
252
- body,
253
- /selectedIds/i,
254
- `${handler} falls back to the focused row, never to a selection`,
255
- );
256
- });
257
- }
258
- }
259
-
260
- it("checks the routed handler on every pane that declares one", () => {
261
- // The roll-call. Each pair above is generated from what a pane declares, so
262
- // this is what says the generation reached everything it should have — a
263
- // handler that stops being declared, or a body the reader stops finding,
264
- // shows up here as a missing pair rather than as a case that passed by
265
- // doing nothing.
266
- const checked = PANES.flatMap((file) =>
267
- declaredHandlers(read(file))
268
- .filter((handler) => handler in PANE_VERB_ROUTE)
269
- .map((handler) => `${file}:${handler}`),
270
- );
271
- assert.deepEqual(checked.sort(), [
272
- "BriefPane.tsx:delete",
273
- "BriefPane.tsx:toggleRead",
274
- "FlaggedPane.tsx:delete",
275
- "FlaggedPane.tsx:toggleRead",
276
- "MailboxPane.tsx:delete",
277
- "MailboxPane.tsx:markJunk",
278
- "MailboxPane.tsx:toggleRead",
279
- ]);
280
- for (const entry of checked) {
281
- const [file, handler] = entry.split(":");
282
- assert.notEqual(paneVerbBody(read(file), handler), "", entry);
283
- }
284
- });
285
- });