@remit/ui 0.0.65 → 0.0.67
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
|
@@ -0,0 +1,493 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Opening folders, and the inline create form that sits at the end of an opened
|
|
3
|
+
* folder's children: where it opens, what it says the parent is, and how it
|
|
4
|
+
* behaves while the mail server is confirming the folder. Mounted against jsdom
|
|
5
|
+
* for the anchoring, the field state and the async resolve. React is imported
|
|
6
|
+
* after the jsdom globals are installed so the controlled value tracker binds to
|
|
7
|
+
* jsdom's prototypes.
|
|
8
|
+
*/
|
|
9
|
+
import assert from "node:assert/strict";
|
|
10
|
+
import { after, afterEach, before, beforeEach, describe, it } from "node:test";
|
|
11
|
+
import type { JSDOM } from "jsdom";
|
|
12
|
+
import type {
|
|
13
|
+
act as reactAct,
|
|
14
|
+
createElement as reactCreateElement,
|
|
15
|
+
useState as reactUseState,
|
|
16
|
+
} from "react";
|
|
17
|
+
import type { Root, createRoot as reactCreateRoot } from "react-dom/client";
|
|
18
|
+
import type {
|
|
19
|
+
FolderTreeNode,
|
|
20
|
+
FolderTreePicker as FolderTreePickerType,
|
|
21
|
+
} from "./folder-tree-picker.js";
|
|
22
|
+
|
|
23
|
+
const folders: FolderTreeNode[] = [
|
|
24
|
+
{ id: "inbox", label: "Inbox", path: "INBOX", isCurrent: true },
|
|
25
|
+
{ id: "travel", label: "Travel", path: "Travel" },
|
|
26
|
+
{ id: "hotels", label: "Hotels", path: "Travel/Hotels" },
|
|
27
|
+
{ id: "archive", label: "Archive", path: "Archive" },
|
|
28
|
+
];
|
|
29
|
+
|
|
30
|
+
let dom: JSDOM;
|
|
31
|
+
let container: HTMLElement;
|
|
32
|
+
let root: Root;
|
|
33
|
+
let act: typeof reactAct;
|
|
34
|
+
let createElement: typeof reactCreateElement;
|
|
35
|
+
let useState: typeof reactUseState;
|
|
36
|
+
let createRoot: typeof reactCreateRoot;
|
|
37
|
+
let FolderTreePicker: typeof FolderTreePickerType;
|
|
38
|
+
|
|
39
|
+
before(async () => {
|
|
40
|
+
const { JSDOM: JSDOMCtor } = await import("jsdom");
|
|
41
|
+
dom = new JSDOMCtor(
|
|
42
|
+
"<!doctype html><html><body><div id=root></div></body></html>",
|
|
43
|
+
{ url: "http://localhost/", pretendToBeVisual: true },
|
|
44
|
+
);
|
|
45
|
+
globalThis.window = dom.window as unknown as typeof globalThis.window;
|
|
46
|
+
globalThis.document = dom.window.document;
|
|
47
|
+
globalThis.HTMLElement = dom.window.HTMLElement;
|
|
48
|
+
globalThis.Element = dom.window.Element;
|
|
49
|
+
globalThis.Event = dom.window.Event;
|
|
50
|
+
Object.defineProperty(globalThis, "navigator", {
|
|
51
|
+
value: dom.window.navigator,
|
|
52
|
+
configurable: true,
|
|
53
|
+
});
|
|
54
|
+
(
|
|
55
|
+
globalThis as { IS_REACT_ACT_ENVIRONMENT?: boolean }
|
|
56
|
+
).IS_REACT_ACT_ENVIRONMENT = true;
|
|
57
|
+
|
|
58
|
+
const react = await import("react");
|
|
59
|
+
act = react.act;
|
|
60
|
+
createElement = react.createElement;
|
|
61
|
+
useState = react.useState;
|
|
62
|
+
({ createRoot } = await import("react-dom/client"));
|
|
63
|
+
({ FolderTreePicker } = await import("./folder-tree-picker.js"));
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
after(() => {
|
|
67
|
+
dom.window.close();
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
beforeEach(() => {
|
|
71
|
+
container = dom.window.document.createElement("div");
|
|
72
|
+
dom.window.document.body.append(container);
|
|
73
|
+
root = createRoot(container);
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
afterEach(async () => {
|
|
77
|
+
await act(async () => {
|
|
78
|
+
root.unmount();
|
|
79
|
+
});
|
|
80
|
+
container.remove();
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
type PickerProps = Partial<Parameters<typeof FolderTreePickerType>[0]>;
|
|
84
|
+
|
|
85
|
+
const mount = async (props: PickerProps = {}) => {
|
|
86
|
+
await act(async () => {
|
|
87
|
+
root.render(
|
|
88
|
+
createElement(FolderTreePicker, {
|
|
89
|
+
folders,
|
|
90
|
+
onSelect: () => {},
|
|
91
|
+
...props,
|
|
92
|
+
}),
|
|
93
|
+
);
|
|
94
|
+
});
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
/** Holds the chosen destination the way the app does, so a tap can be undone. */
|
|
98
|
+
const mountControlled = async (props: PickerProps = {}) => {
|
|
99
|
+
const Controlled = () => {
|
|
100
|
+
const [selected, setSelected] = useState<string>();
|
|
101
|
+
return createElement(FolderTreePicker, {
|
|
102
|
+
folders,
|
|
103
|
+
selectedId: selected,
|
|
104
|
+
onSelect: setSelected,
|
|
105
|
+
...props,
|
|
106
|
+
});
|
|
107
|
+
};
|
|
108
|
+
await act(async () => {
|
|
109
|
+
root.render(createElement(Controlled));
|
|
110
|
+
});
|
|
111
|
+
};
|
|
112
|
+
|
|
113
|
+
const click = async (element: Element | null | undefined) => {
|
|
114
|
+
assert.ok(element, "control not rendered");
|
|
115
|
+
await act(async () => {
|
|
116
|
+
(element as HTMLElement).click();
|
|
117
|
+
});
|
|
118
|
+
};
|
|
119
|
+
|
|
120
|
+
const focus = async (element: Element | null | undefined) => {
|
|
121
|
+
assert.ok(element, "control not rendered");
|
|
122
|
+
await act(async () => {
|
|
123
|
+
(element as HTMLElement).focus();
|
|
124
|
+
});
|
|
125
|
+
};
|
|
126
|
+
|
|
127
|
+
const byAriaLabel = (label: string): HTMLElement | null =>
|
|
128
|
+
container.querySelector(`[aria-label="${label}"]`);
|
|
129
|
+
|
|
130
|
+
const byText = (label: string): HTMLButtonElement | undefined =>
|
|
131
|
+
Array.from(container.querySelectorAll("button")).find(
|
|
132
|
+
(button) => button.textContent?.trim() === label,
|
|
133
|
+
);
|
|
134
|
+
|
|
135
|
+
const nameField = (): HTMLInputElement | null =>
|
|
136
|
+
container.querySelector('input:not([type="search"])');
|
|
137
|
+
|
|
138
|
+
const typeName = async (value: string) => {
|
|
139
|
+
const input = nameField();
|
|
140
|
+
assert.ok(input, "name field not rendered");
|
|
141
|
+
const setter = Object.getOwnPropertyDescriptor(
|
|
142
|
+
dom.window.HTMLInputElement.prototype,
|
|
143
|
+
"value",
|
|
144
|
+
)?.set;
|
|
145
|
+
await act(async () => {
|
|
146
|
+
setter?.call(input, value);
|
|
147
|
+
input.dispatchEvent(new dom.window.Event("input", { bubbles: true }));
|
|
148
|
+
});
|
|
149
|
+
};
|
|
150
|
+
|
|
151
|
+
const open = async (...labels: string[]) => {
|
|
152
|
+
for (const label of labels) {
|
|
153
|
+
await click(byAriaLabel(`Move to ${label}`));
|
|
154
|
+
}
|
|
155
|
+
};
|
|
156
|
+
|
|
157
|
+
/** The block holding a folder's create action and any form opened from it. */
|
|
158
|
+
const createBlockOf = (label: string): Element | null =>
|
|
159
|
+
byAriaLabel(`New folder inside ${label}`)?.closest('[role="none"]') ?? null;
|
|
160
|
+
|
|
161
|
+
const rowOf = (label: string): Element | null =>
|
|
162
|
+
byAriaLabel(`Move to ${label}`)?.closest('[role="none"]') ?? null;
|
|
163
|
+
|
|
164
|
+
const created = (name: string, parentPath: string): FolderTreeNode => ({
|
|
165
|
+
id: "made",
|
|
166
|
+
label: name,
|
|
167
|
+
path: parentPath ? `${parentPath}/${name}` : name,
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
const resolving = (name: string, parentPath: string) =>
|
|
171
|
+
Promise.resolve(created(name, parentPath));
|
|
172
|
+
|
|
173
|
+
describe("opening folders", () => {
|
|
174
|
+
it("starts at the top level with everything closed", async () => {
|
|
175
|
+
await mount({});
|
|
176
|
+
assert.ok(byAriaLabel("Move to Travel"));
|
|
177
|
+
assert.ok(byAriaLabel("Move to Archive"));
|
|
178
|
+
assert.equal(byAriaLabel("Move to Hotels"), null);
|
|
179
|
+
assert.equal(
|
|
180
|
+
byAriaLabel("Move to Travel")?.getAttribute("aria-expanded"),
|
|
181
|
+
"false",
|
|
182
|
+
);
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
it("picks the destination and opens it in one tap", async () => {
|
|
186
|
+
const selected: string[] = [];
|
|
187
|
+
await mount({ onSelect: (id) => selected.push(id) });
|
|
188
|
+
await open("Travel");
|
|
189
|
+
assert.deepEqual(selected, ["travel"]);
|
|
190
|
+
assert.ok(byAriaLabel("Move to Hotels"));
|
|
191
|
+
assert.equal(
|
|
192
|
+
byAriaLabel("Move to Travel")?.getAttribute("aria-expanded"),
|
|
193
|
+
"true",
|
|
194
|
+
);
|
|
195
|
+
assert.equal(
|
|
196
|
+
byAriaLabel("Move to Hotels")?.getAttribute("aria-level"),
|
|
197
|
+
"2",
|
|
198
|
+
);
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
it("closes on a second tap and keeps the destination", async () => {
|
|
202
|
+
await mountControlled();
|
|
203
|
+
await open("Travel");
|
|
204
|
+
assert.equal(
|
|
205
|
+
byAriaLabel("Move to Travel")?.getAttribute("aria-selected"),
|
|
206
|
+
"true",
|
|
207
|
+
);
|
|
208
|
+
await open("Travel");
|
|
209
|
+
assert.equal(byAriaLabel("Move to Hotels"), null);
|
|
210
|
+
assert.equal(
|
|
211
|
+
byAriaLabel("Move to Travel")?.getAttribute("aria-selected"),
|
|
212
|
+
"true",
|
|
213
|
+
);
|
|
214
|
+
assert.equal(
|
|
215
|
+
byAriaLabel("Move to Travel")?.getAttribute("aria-expanded"),
|
|
216
|
+
"false",
|
|
217
|
+
);
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
it("opens the current folder without ever picking it", async () => {
|
|
221
|
+
const selected: string[] = [];
|
|
222
|
+
await mount({ onSelect: (id) => selected.push(id) });
|
|
223
|
+
await click(byAriaLabel("Inbox (current folder)"));
|
|
224
|
+
assert.deepEqual(selected, []);
|
|
225
|
+
assert.equal(
|
|
226
|
+
byAriaLabel("Inbox (current folder)")?.getAttribute("aria-expanded"),
|
|
227
|
+
"true",
|
|
228
|
+
);
|
|
229
|
+
});
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
describe("create form anchoring", () => {
|
|
233
|
+
it("offers the action at the end of an opened folder's children", async () => {
|
|
234
|
+
await mount({ onCreateFolder: resolving });
|
|
235
|
+
assert.equal(byAriaLabel("New folder inside Travel"), null);
|
|
236
|
+
await open("Travel");
|
|
237
|
+
assert.ok(byAriaLabel("New folder inside Travel"));
|
|
238
|
+
assert.equal(byAriaLabel("New folder inside Hotels"), null);
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
it("opens the form under the action it was pressed from", async () => {
|
|
242
|
+
await mount({ onCreateFolder: resolving });
|
|
243
|
+
await open("Travel");
|
|
244
|
+
await click(byAriaLabel("New folder inside Travel"));
|
|
245
|
+
assert.ok(createBlockOf("Travel")?.querySelector("input"));
|
|
246
|
+
assert.equal(rowOf("Travel")?.querySelector("input"), null);
|
|
247
|
+
});
|
|
248
|
+
|
|
249
|
+
it("states the parent as fixed text rather than a second choice", async () => {
|
|
250
|
+
await mount({ onCreateFolder: resolving });
|
|
251
|
+
await open("Travel");
|
|
252
|
+
await click(byAriaLabel("New folder inside Travel"));
|
|
253
|
+
assert.match(createBlockOf("Travel")?.textContent ?? "", /Inside\s*Travel/);
|
|
254
|
+
assert.equal(container.querySelector("select"), null);
|
|
255
|
+
});
|
|
256
|
+
|
|
257
|
+
it("moves the form when the action inside another folder opens it", async () => {
|
|
258
|
+
await mount({ onCreateFolder: resolving });
|
|
259
|
+
await open("Travel", "Hotels");
|
|
260
|
+
await click(byAriaLabel("New folder inside Travel"));
|
|
261
|
+
await click(byAriaLabel("New folder inside Hotels"));
|
|
262
|
+
assert.equal(createBlockOf("Travel")?.querySelector("input"), null);
|
|
263
|
+
assert.ok(createBlockOf("Hotels")?.querySelector("input"));
|
|
264
|
+
});
|
|
265
|
+
|
|
266
|
+
it("creates at top level from the action pinned above the tree", async () => {
|
|
267
|
+
await mount({ onCreateFolder: resolving });
|
|
268
|
+
await click(byAriaLabel("New folder"));
|
|
269
|
+
assert.ok(nameField());
|
|
270
|
+
assert.match(container.textContent ?? "", /Inside\s*Top level/);
|
|
271
|
+
});
|
|
272
|
+
|
|
273
|
+
it("offers a subfolder inside the current folder", async () => {
|
|
274
|
+
await mount({ onCreateFolder: resolving });
|
|
275
|
+
await click(byAriaLabel("Inbox (current folder)"));
|
|
276
|
+
assert.ok(byAriaLabel("New folder inside Inbox"));
|
|
277
|
+
});
|
|
278
|
+
|
|
279
|
+
it("closes on Cancel", async () => {
|
|
280
|
+
await mount({ onCreateFolder: resolving });
|
|
281
|
+
await open("Travel");
|
|
282
|
+
await click(byAriaLabel("New folder inside Travel"));
|
|
283
|
+
await click(byText("Cancel"));
|
|
284
|
+
assert.equal(nameField(), null);
|
|
285
|
+
});
|
|
286
|
+
});
|
|
287
|
+
|
|
288
|
+
describe("create wait", () => {
|
|
289
|
+
it("passes the opened folder's path as the parent and selects what comes back", async () => {
|
|
290
|
+
const calls: Array<[string, string]> = [];
|
|
291
|
+
const selected: string[] = [];
|
|
292
|
+
await mount({
|
|
293
|
+
onSelect: (id) => selected.push(id),
|
|
294
|
+
onCreateFolder: (name, parentPath) => {
|
|
295
|
+
calls.push([name, parentPath]);
|
|
296
|
+
return Promise.resolve(created(name, parentPath));
|
|
297
|
+
},
|
|
298
|
+
});
|
|
299
|
+
await open("Travel");
|
|
300
|
+
await click(byAriaLabel("New folder inside Travel"));
|
|
301
|
+
await typeName("Hotels 2");
|
|
302
|
+
await click(byText("Create folder"));
|
|
303
|
+
assert.deepEqual(calls, [["Hotels 2", "Travel"]]);
|
|
304
|
+
assert.deepEqual(selected, ["travel", "made"]);
|
|
305
|
+
assert.equal(nameField(), null);
|
|
306
|
+
});
|
|
307
|
+
|
|
308
|
+
it("shows the wait and refuses a second submit while it runs", async () => {
|
|
309
|
+
let attempts = 0;
|
|
310
|
+
await mount({
|
|
311
|
+
onCreateFolder: () => {
|
|
312
|
+
attempts += 1;
|
|
313
|
+
return new Promise<FolderTreeNode>(() => undefined);
|
|
314
|
+
},
|
|
315
|
+
});
|
|
316
|
+
await open("Travel");
|
|
317
|
+
await click(byAriaLabel("New folder inside Travel"));
|
|
318
|
+
await typeName("Hotels 2");
|
|
319
|
+
await click(byText("Create folder"));
|
|
320
|
+
const pending = byText("Creating folder…");
|
|
321
|
+
assert.ok(pending);
|
|
322
|
+
assert.equal(pending.disabled, true);
|
|
323
|
+
await click(pending);
|
|
324
|
+
assert.equal(attempts, 1);
|
|
325
|
+
});
|
|
326
|
+
|
|
327
|
+
it("states a failure where it happened and keeps the form open", async () => {
|
|
328
|
+
await mount({
|
|
329
|
+
onCreateFolder: () =>
|
|
330
|
+
Promise.reject(new Error("The mail server refused that name.")),
|
|
331
|
+
});
|
|
332
|
+
await open("Travel");
|
|
333
|
+
await click(byAriaLabel("New folder inside Travel"));
|
|
334
|
+
await typeName("Hotels 2");
|
|
335
|
+
await click(byText("Create folder"));
|
|
336
|
+
const alert = container.querySelector('[role="alert"]');
|
|
337
|
+
assert.equal(alert?.textContent, "The mail server refused that name.");
|
|
338
|
+
assert.ok(createBlockOf("Travel")?.querySelector("input"));
|
|
339
|
+
});
|
|
340
|
+
|
|
341
|
+
it("says what is missing instead of going dead on an empty name", async () => {
|
|
342
|
+
let attempts = 0;
|
|
343
|
+
await mount({
|
|
344
|
+
onCreateFolder: (n, p) => {
|
|
345
|
+
attempts += 1;
|
|
346
|
+
return Promise.resolve(created(n, p));
|
|
347
|
+
},
|
|
348
|
+
});
|
|
349
|
+
await open("Travel");
|
|
350
|
+
await click(byAriaLabel("New folder inside Travel"));
|
|
351
|
+
await click(byText("Create folder"));
|
|
352
|
+
assert.equal(attempts, 0);
|
|
353
|
+
assert.match(
|
|
354
|
+
container.querySelector('[role="alert"]')?.textContent ?? "",
|
|
355
|
+
/Give the folder a name/,
|
|
356
|
+
);
|
|
357
|
+
});
|
|
358
|
+
|
|
359
|
+
it("aborts the wait on unmount so a late confirmation selects nothing", async () => {
|
|
360
|
+
let signal: AbortSignal | undefined;
|
|
361
|
+
const selected: string[] = [];
|
|
362
|
+
await mount({
|
|
363
|
+
onSelect: (id) => selected.push(id),
|
|
364
|
+
onCreateFolder: (_name, _parentPath, abortSignal) => {
|
|
365
|
+
signal = abortSignal;
|
|
366
|
+
return new Promise<FolderTreeNode>((_resolve, reject) => {
|
|
367
|
+
abortSignal?.addEventListener("abort", () =>
|
|
368
|
+
reject(new DOMException("Aborted", "AbortError")),
|
|
369
|
+
);
|
|
370
|
+
});
|
|
371
|
+
},
|
|
372
|
+
});
|
|
373
|
+
await click(byAriaLabel("New folder"));
|
|
374
|
+
await typeName("Insurance");
|
|
375
|
+
await click(byText("Create folder"));
|
|
376
|
+
await act(async () => {
|
|
377
|
+
root.unmount();
|
|
378
|
+
});
|
|
379
|
+
assert.equal(signal?.aborted, true);
|
|
380
|
+
assert.deepEqual(selected, []);
|
|
381
|
+
root = createRoot(container);
|
|
382
|
+
});
|
|
383
|
+
});
|
|
384
|
+
|
|
385
|
+
describe("filter and keyboard", () => {
|
|
386
|
+
const filterField = (): HTMLInputElement | null =>
|
|
387
|
+
container.querySelector('input[type="search"]');
|
|
388
|
+
|
|
389
|
+
const typeFilter = async (value: string) => {
|
|
390
|
+
const input = filterField();
|
|
391
|
+
assert.ok(input);
|
|
392
|
+
const setter = Object.getOwnPropertyDescriptor(
|
|
393
|
+
dom.window.HTMLInputElement.prototype,
|
|
394
|
+
"value",
|
|
395
|
+
)?.set;
|
|
396
|
+
await act(async () => {
|
|
397
|
+
setter?.call(input, value);
|
|
398
|
+
input.dispatchEvent(new dom.window.Event("input", { bubbles: true }));
|
|
399
|
+
});
|
|
400
|
+
};
|
|
401
|
+
|
|
402
|
+
const press = async (target: Element | null | undefined, key: string) => {
|
|
403
|
+
assert.ok(target, "control not rendered");
|
|
404
|
+
await act(async () => {
|
|
405
|
+
target.dispatchEvent(
|
|
406
|
+
new dom.window.KeyboardEvent("keydown", { key, bubbles: true }),
|
|
407
|
+
);
|
|
408
|
+
});
|
|
409
|
+
};
|
|
410
|
+
|
|
411
|
+
const focused = (): string | null =>
|
|
412
|
+
dom.window.document.activeElement?.getAttribute("aria-label") ?? null;
|
|
413
|
+
|
|
414
|
+
it("opens the ancestors a match hides behind", async () => {
|
|
415
|
+
await mount({});
|
|
416
|
+
assert.equal(byAriaLabel("Move to Hotels"), null);
|
|
417
|
+
await typeFilter("hotels");
|
|
418
|
+
assert.ok(byAriaLabel("Move to Hotels"));
|
|
419
|
+
assert.equal(byAriaLabel("Move to Travel"), null);
|
|
420
|
+
assert.ok(byAriaLabel("Travel (containing folder)"));
|
|
421
|
+
assert.equal(
|
|
422
|
+
byAriaLabel("Travel (containing folder)")?.getAttribute("aria-expanded"),
|
|
423
|
+
"true",
|
|
424
|
+
);
|
|
425
|
+
assert.equal(byAriaLabel("Move to Archive"), null);
|
|
426
|
+
});
|
|
427
|
+
|
|
428
|
+
it("puts the list back the way it was left when the filter clears", async () => {
|
|
429
|
+
await mount({});
|
|
430
|
+
await open("Travel");
|
|
431
|
+
await typeFilter("archive");
|
|
432
|
+
assert.equal(byAriaLabel("Move to Hotels"), null);
|
|
433
|
+
await typeFilter("");
|
|
434
|
+
assert.ok(byAriaLabel("Move to Hotels"));
|
|
435
|
+
});
|
|
436
|
+
|
|
437
|
+
it("says so when nothing matches", async () => {
|
|
438
|
+
await mount({});
|
|
439
|
+
await typeFilter("zzz");
|
|
440
|
+
assert.match(container.textContent ?? "", /No folders match "zzz"/);
|
|
441
|
+
});
|
|
442
|
+
|
|
443
|
+
it("picks and opens the focused row on Enter", async () => {
|
|
444
|
+
const selected: string[] = [];
|
|
445
|
+
await mount({ onSelect: (id) => selected.push(id) });
|
|
446
|
+
await focus(byAriaLabel("Move to Travel"));
|
|
447
|
+
await press(byAriaLabel("Move to Travel"), "Enter");
|
|
448
|
+
assert.deepEqual(selected, ["travel"]);
|
|
449
|
+
assert.ok(byAriaLabel("Move to Hotels"));
|
|
450
|
+
});
|
|
451
|
+
|
|
452
|
+
it("opens on Right and closes on Left without picking anything", async () => {
|
|
453
|
+
const selected: string[] = [];
|
|
454
|
+
await mount({ onSelect: (id) => selected.push(id) });
|
|
455
|
+
await focus(byAriaLabel("Move to Travel"));
|
|
456
|
+
await press(byAriaLabel("Move to Travel"), "ArrowRight");
|
|
457
|
+
assert.ok(byAriaLabel("Move to Hotels"));
|
|
458
|
+
await press(byAriaLabel("Move to Travel"), "ArrowRight");
|
|
459
|
+
assert.equal(focused(), "Move to Hotels");
|
|
460
|
+
await press(byAriaLabel("Move to Hotels"), "ArrowLeft");
|
|
461
|
+
assert.equal(focused(), "Move to Travel");
|
|
462
|
+
await press(byAriaLabel("Move to Travel"), "ArrowLeft");
|
|
463
|
+
assert.equal(byAriaLabel("Move to Hotels"), null);
|
|
464
|
+
assert.deepEqual(selected, []);
|
|
465
|
+
});
|
|
466
|
+
|
|
467
|
+
it("walks only the rows on screen with the arrow keys", async () => {
|
|
468
|
+
await mount({});
|
|
469
|
+
await focus(byAriaLabel("Inbox (current folder)"));
|
|
470
|
+
await press(byAriaLabel("Inbox (current folder)"), "ArrowDown");
|
|
471
|
+
assert.equal(focused(), "Move to Travel");
|
|
472
|
+
await press(byAriaLabel("Move to Travel"), "ArrowDown");
|
|
473
|
+
assert.equal(focused(), "Move to Archive");
|
|
474
|
+
await press(byAriaLabel("Move to Archive"), "Home");
|
|
475
|
+
assert.equal(focused(), "Inbox (current folder)");
|
|
476
|
+
await press(byAriaLabel("Inbox (current folder)"), "End");
|
|
477
|
+
assert.equal(focused(), "Move to Archive");
|
|
478
|
+
});
|
|
479
|
+
|
|
480
|
+
it("cancels on Escape from the tree and from the filter", async () => {
|
|
481
|
+
let cancelled = 0;
|
|
482
|
+
await mount({ onCancel: () => (cancelled += 1) });
|
|
483
|
+
await press(byAriaLabel("Move to Travel"), "Escape");
|
|
484
|
+
await press(filterField(), "Escape");
|
|
485
|
+
assert.equal(cancelled, 2);
|
|
486
|
+
});
|
|
487
|
+
|
|
488
|
+
it("hands focus from the filter to the first row on ArrowDown", async () => {
|
|
489
|
+
await mount({});
|
|
490
|
+
await press(filterField(), "ArrowDown");
|
|
491
|
+
assert.equal(focused(), "Inbox (current folder)");
|
|
492
|
+
});
|
|
493
|
+
});
|