@remit/ui 0.0.66 → 0.0.68

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,5 +1,6 @@
1
1
  /**
2
- * The inline create form: where it opens, what it says the parent is, and how it
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
3
4
  * behaves while the mail server is confirming the folder. Mounted against jsdom
4
5
  * for the anchoring, the field state and the async resolve. React is imported
5
6
  * after the jsdom globals are installed so the controlled value tracker binds to
@@ -11,6 +12,7 @@ import type { JSDOM } from "jsdom";
11
12
  import type {
12
13
  act as reactAct,
13
14
  createElement as reactCreateElement,
15
+ useState as reactUseState,
14
16
  } from "react";
15
17
  import type { Root, createRoot as reactCreateRoot } from "react-dom/client";
16
18
  import type {
@@ -22,14 +24,18 @@ const folders: FolderTreeNode[] = [
22
24
  { id: "inbox", label: "Inbox", path: "INBOX", isCurrent: true },
23
25
  { id: "travel", label: "Travel", path: "Travel" },
24
26
  { id: "hotels", label: "Hotels", path: "Travel/Hotels" },
27
+ { id: "receipts", label: "Receipts", path: "Travel/Hotels/Receipts" },
28
+ { id: "flights", label: "Flights", path: "Travel/Flights" },
25
29
  { id: "archive", label: "Archive", path: "Archive" },
26
30
  ];
27
31
 
28
32
  let dom: JSDOM;
33
+ const scrolledIntoView: Element[] = [];
29
34
  let container: HTMLElement;
30
35
  let root: Root;
31
36
  let act: typeof reactAct;
32
37
  let createElement: typeof reactCreateElement;
38
+ let useState: typeof reactUseState;
33
39
  let createRoot: typeof reactCreateRoot;
34
40
  let FolderTreePicker: typeof FolderTreePickerType;
35
41
 
@@ -51,10 +57,17 @@ before(async () => {
51
57
  (
52
58
  globalThis as { IS_REACT_ACT_ENVIRONMENT?: boolean }
53
59
  ).IS_REACT_ACT_ENVIRONMENT = true;
60
+ Object.defineProperty(dom.window.HTMLElement.prototype, "scrollIntoView", {
61
+ configurable: true,
62
+ value: function scrollIntoView(this: Element) {
63
+ scrolledIntoView.push(this);
64
+ },
65
+ });
54
66
 
55
67
  const react = await import("react");
56
68
  act = react.act;
57
69
  createElement = react.createElement;
70
+ useState = react.useState;
58
71
  ({ createRoot } = await import("react-dom/client"));
59
72
  ({ FolderTreePicker } = await import("./folder-tree-picker.js"));
60
73
  });
@@ -64,6 +77,7 @@ after(() => {
64
77
  });
65
78
 
66
79
  beforeEach(() => {
80
+ scrolledIntoView.length = 0;
67
81
  container = dom.window.document.createElement("div");
68
82
  dom.window.document.body.append(container);
69
83
  root = createRoot(container);
@@ -76,9 +90,9 @@ afterEach(async () => {
76
90
  container.remove();
77
91
  });
78
92
 
79
- const mount = async (
80
- props: Partial<Parameters<typeof FolderTreePickerType>[0]> = {},
81
- ) => {
93
+ type PickerProps = Partial<Parameters<typeof FolderTreePickerType>[0]>;
94
+
95
+ const mount = async (props: PickerProps = {}) => {
82
96
  await act(async () => {
83
97
  root.render(
84
98
  createElement(FolderTreePicker, {
@@ -90,6 +104,22 @@ const mount = async (
90
104
  });
91
105
  };
92
106
 
107
+ /** Holds the chosen destination the way the app does, so a tap can be undone. */
108
+ const mountControlled = async (props: PickerProps = {}) => {
109
+ const Controlled = () => {
110
+ const [selected, setSelected] = useState<string>();
111
+ return createElement(FolderTreePicker, {
112
+ folders,
113
+ selectedId: selected,
114
+ onSelect: setSelected,
115
+ ...props,
116
+ });
117
+ };
118
+ await act(async () => {
119
+ root.render(createElement(Controlled));
120
+ });
121
+ };
122
+
93
123
  const click = async (element: Element | null | undefined) => {
94
124
  assert.ok(element, "control not rendered");
95
125
  await act(async () => {
@@ -97,6 +127,13 @@ const click = async (element: Element | null | undefined) => {
97
127
  });
98
128
  };
99
129
 
130
+ const focus = async (element: Element | null | undefined) => {
131
+ assert.ok(element, "control not rendered");
132
+ await act(async () => {
133
+ (element as HTMLElement).focus();
134
+ });
135
+ };
136
+
100
137
  const byAriaLabel = (label: string): HTMLElement | null =>
101
138
  container.querySelector(`[aria-label="${label}"]`);
102
139
 
@@ -121,6 +158,20 @@ const typeName = async (value: string) => {
121
158
  });
122
159
  };
123
160
 
161
+ const open = async (...labels: string[]) => {
162
+ for (const label of labels) {
163
+ await click(byAriaLabel(`Move to ${label}`));
164
+ }
165
+ };
166
+
167
+ /**
168
+ * The block a folder's create action lives in — and, while the form is open,
169
+ * the form that took its place. Keyed on the anchor rather than the action, so
170
+ * it still resolves once the action has given way.
171
+ */
172
+ const createBlockOf = (anchor: string): Element | null =>
173
+ container.querySelector(`[data-create-anchor="${anchor}"]`);
174
+
124
175
  const rowOf = (label: string): Element | null =>
125
176
  byAriaLabel(`Move to ${label}`)?.closest('[role="none"]') ?? null;
126
177
 
@@ -130,52 +181,259 @@ const created = (name: string, parentPath: string): FolderTreeNode => ({
130
181
  path: parentPath ? `${parentPath}/${name}` : name,
131
182
  });
132
183
 
184
+ const resolving = (name: string, parentPath: string) =>
185
+ Promise.resolve(created(name, parentPath));
186
+
187
+ describe("opening folders", () => {
188
+ it("starts at the top level with everything closed", async () => {
189
+ await mount({});
190
+ assert.ok(byAriaLabel("Move to Travel"));
191
+ assert.ok(byAriaLabel("Move to Archive"));
192
+ assert.equal(byAriaLabel("Move to Hotels"), null);
193
+ assert.equal(
194
+ byAriaLabel("Move to Travel")?.getAttribute("aria-expanded"),
195
+ "false",
196
+ );
197
+ });
198
+
199
+ it("picks the destination and opens it in one tap", async () => {
200
+ const selected: string[] = [];
201
+ await mount({ onSelect: (id) => selected.push(id) });
202
+ await open("Travel");
203
+ assert.deepEqual(selected, ["travel"]);
204
+ assert.ok(byAriaLabel("Move to Hotels"));
205
+ assert.equal(
206
+ byAriaLabel("Move to Travel")?.getAttribute("aria-expanded"),
207
+ "true",
208
+ );
209
+ assert.equal(
210
+ byAriaLabel("Move to Hotels")?.getAttribute("aria-level"),
211
+ "2",
212
+ );
213
+ });
214
+
215
+ it("closes on a second tap and keeps the destination", async () => {
216
+ await mountControlled();
217
+ await open("Travel");
218
+ assert.equal(
219
+ byAriaLabel("Move to Travel")?.getAttribute("aria-selected"),
220
+ "true",
221
+ );
222
+ await open("Travel");
223
+ assert.equal(byAriaLabel("Move to Hotels"), null);
224
+ assert.equal(
225
+ byAriaLabel("Move to Travel")?.getAttribute("aria-selected"),
226
+ "true",
227
+ );
228
+ assert.equal(
229
+ byAriaLabel("Move to Travel")?.getAttribute("aria-expanded"),
230
+ "false",
231
+ );
232
+ });
233
+
234
+ it("opens the current folder without ever picking it", async () => {
235
+ const selected: string[] = [];
236
+ await mount({ onSelect: (id) => selected.push(id) });
237
+ await click(byAriaLabel("Inbox (current folder)"));
238
+ assert.deepEqual(selected, []);
239
+ assert.equal(
240
+ byAriaLabel("Inbox (current folder)")?.getAttribute("aria-expanded"),
241
+ "true",
242
+ );
243
+ });
244
+ });
245
+
246
+ describe("one open branch", () => {
247
+ it("closes the folder that was open when another opens", async () => {
248
+ await mount({});
249
+ await open("Travel");
250
+ assert.ok(byAriaLabel("Move to Hotels"));
251
+ await open("Archive");
252
+ assert.equal(byAriaLabel("Move to Hotels"), null);
253
+ assert.equal(
254
+ byAriaLabel("Move to Travel")?.getAttribute("aria-expanded"),
255
+ "false",
256
+ );
257
+ assert.equal(
258
+ byAriaLabel("Move to Archive")?.getAttribute("aria-expanded"),
259
+ "true",
260
+ );
261
+ });
262
+
263
+ it("keeps the ancestors of the folder being opened open", async () => {
264
+ await mount({});
265
+ await open("Travel", "Hotels");
266
+ assert.ok(byAriaLabel("Move to Receipts"));
267
+ assert.equal(
268
+ byAriaLabel("Move to Travel")?.getAttribute("aria-expanded"),
269
+ "true",
270
+ );
271
+ assert.equal(
272
+ byAriaLabel("Move to Hotels")?.getAttribute("aria-expanded"),
273
+ "true",
274
+ );
275
+ });
276
+
277
+ it("closes a sibling branch without closing the folder holding both", async () => {
278
+ await mount({});
279
+ await open("Travel", "Hotels");
280
+ await open("Flights");
281
+ assert.equal(byAriaLabel("Move to Receipts"), null);
282
+ assert.equal(
283
+ byAriaLabel("Move to Hotels")?.getAttribute("aria-expanded"),
284
+ "false",
285
+ );
286
+ assert.equal(
287
+ byAriaLabel("Move to Travel")?.getAttribute("aria-expanded"),
288
+ "true",
289
+ );
290
+ });
291
+
292
+ it("leaves one create action inside the branch that stayed open", async () => {
293
+ await mount({ onCreateFolder: resolving });
294
+ await open("Travel", "Hotels");
295
+ await open("Archive");
296
+ assert.equal(byAriaLabel("New folder inside Travel"), null);
297
+ assert.equal(byAriaLabel("New folder inside Hotels"), null);
298
+ assert.ok(byAriaLabel("New folder inside Archive"));
299
+ });
300
+ });
301
+
302
+ describe("create action prominence", () => {
303
+ it("keeps the pinned action prominent and the nested one quiet", async () => {
304
+ await mount({ onCreateFolder: resolving });
305
+ assert.equal(byAriaLabel("New folder")?.dataset.prominence, "prominent");
306
+ await open("Travel");
307
+ assert.equal(
308
+ byAriaLabel("New folder inside Travel")?.dataset.prominence,
309
+ "quiet",
310
+ );
311
+ assert.equal(byAriaLabel("New folder")?.dataset.prominence, "prominent");
312
+ });
313
+
314
+ it("stays the same action either way", async () => {
315
+ await mount({ onCreateFolder: resolving });
316
+ await open("Travel");
317
+ const pinned = byAriaLabel("New folder");
318
+ const nested = byAriaLabel("New folder inside Travel");
319
+ assert.equal(pinned?.tagName, nested?.tagName);
320
+ assert.equal(pinned?.textContent, nested?.textContent);
321
+ });
322
+ });
323
+
133
324
  describe("create form anchoring", () => {
134
- it("opens under the row whose affordance was pressed", async () => {
135
- await mount({ onCreateFolder: (n, p) => Promise.resolve(created(n, p)) });
325
+ it("offers the action at the end of an opened folder's children", async () => {
326
+ await mount({ onCreateFolder: resolving });
327
+ assert.equal(byAriaLabel("New folder inside Travel"), null);
328
+ await open("Travel");
329
+ assert.ok(byAriaLabel("New folder inside Travel"));
330
+ assert.equal(byAriaLabel("New folder inside Hotels"), null);
331
+ });
332
+
333
+ it("opens the form under the action it was pressed from", async () => {
334
+ await mount({ onCreateFolder: resolving });
335
+ await open("Travel");
136
336
  await click(byAriaLabel("New folder inside Travel"));
137
- assert.ok(rowOf("Travel")?.querySelector("input"));
138
- assert.equal(rowOf("Hotels")?.querySelector("input"), null);
337
+ assert.ok(createBlockOf("travel")?.querySelector("input"));
338
+ assert.equal(rowOf("Travel")?.querySelector("input"), null);
139
339
  });
140
340
 
141
341
  it("states the parent as fixed text rather than a second choice", async () => {
142
- await mount({ onCreateFolder: (n, p) => Promise.resolve(created(n, p)) });
342
+ await mount({ onCreateFolder: resolving });
343
+ await open("Travel");
143
344
  await click(byAriaLabel("New folder inside Travel"));
144
- assert.match(rowOf("Travel")?.textContent ?? "", /Inside\s*Travel/);
345
+ assert.match(createBlockOf("travel")?.textContent ?? "", /Inside\s*Travel/);
145
346
  assert.equal(container.querySelector("select"), null);
146
347
  });
147
348
 
148
- it("moves the form when another row opens it", async () => {
149
- await mount({ onCreateFolder: (n, p) => Promise.resolve(created(n, p)) });
349
+ it("moves the form when the action inside another folder opens it", async () => {
350
+ await mount({ onCreateFolder: resolving });
351
+ await open("Travel", "Hotels");
150
352
  await click(byAriaLabel("New folder inside Travel"));
151
353
  await click(byAriaLabel("New folder inside Hotels"));
152
- assert.equal(rowOf("Travel")?.querySelector("input"), null);
153
- assert.ok(rowOf("Hotels")?.querySelector("input"));
354
+ assert.equal(createBlockOf("travel")?.querySelector("input"), null);
355
+ assert.ok(createBlockOf("hotels")?.querySelector("input"));
154
356
  });
155
357
 
156
- it("creates at top level from the row above the tree", async () => {
157
- await mount({ onCreateFolder: (n, p) => Promise.resolve(created(n, p)) });
158
- await click(byText("New folder"));
159
- assert.equal(rowOf("Travel")?.querySelector("input"), null);
358
+ it("creates at top level from the action pinned above the tree", async () => {
359
+ await mount({ onCreateFolder: resolving });
360
+ await click(byAriaLabel("New folder"));
160
361
  assert.ok(nameField());
161
362
  assert.match(container.textContent ?? "", /Inside\s*Top level/);
162
363
  });
163
364
 
164
365
  it("offers a subfolder inside the current folder", async () => {
165
- await mount({ onCreateFolder: (n, p) => Promise.resolve(created(n, p)) });
366
+ await mount({ onCreateFolder: resolving });
367
+ await click(byAriaLabel("Inbox (current folder)"));
166
368
  assert.ok(byAriaLabel("New folder inside Inbox"));
167
369
  });
168
370
 
169
371
  it("closes on Cancel", async () => {
170
- await mount({ onCreateFolder: (n, p) => Promise.resolve(created(n, p)) });
372
+ await mount({ onCreateFolder: resolving });
373
+ await open("Travel");
171
374
  await click(byAriaLabel("New folder inside Travel"));
172
375
  await click(byText("Cancel"));
173
376
  assert.equal(nameField(), null);
174
377
  });
175
378
  });
176
379
 
380
+ describe("the form takes the place of the action that opened it", () => {
381
+ it("replaces the pinned action", async () => {
382
+ await mount({ onCreateFolder: resolving });
383
+ await click(byAriaLabel("New folder"));
384
+ assert.equal(byAriaLabel("New folder"), null);
385
+ assert.ok(createBlockOf("top")?.querySelector("input"));
386
+ });
387
+
388
+ it("replaces a nested action and leaves the others alone", async () => {
389
+ await mount({ onCreateFolder: resolving });
390
+ await open("Travel", "Hotels");
391
+ await click(byAriaLabel("New folder inside Hotels"));
392
+ assert.equal(byAriaLabel("New folder inside Hotels"), null);
393
+ assert.ok(byAriaLabel("New folder inside Travel"));
394
+ assert.ok(byAriaLabel("New folder"));
395
+ });
396
+
397
+ it("puts the action back once the form closes", async () => {
398
+ await mount({ onCreateFolder: resolving });
399
+ await open("Travel");
400
+ await click(byAriaLabel("New folder inside Travel"));
401
+ await click(byText("Cancel"));
402
+ assert.ok(byAriaLabel("New folder inside Travel"));
403
+ });
404
+ });
405
+
406
+ describe("an opened form is reachable", () => {
407
+ it("brings the whole form into view, buttons included", async () => {
408
+ await mount({ onCreateFolder: resolving });
409
+ await open("Archive");
410
+ await click(byAriaLabel("New folder inside Archive"));
411
+ const scrolled = scrolledIntoView.at(-1);
412
+ assert.ok(scrolled, "nothing was scrolled into view");
413
+ assert.ok(scrolled.contains(nameField()));
414
+ assert.ok(scrolled.contains(byText("Create folder") ?? null));
415
+ assert.ok(scrolled.contains(byText("Cancel") ?? null));
416
+ });
417
+
418
+ it("scrolls the list rather than the page", async () => {
419
+ await mount({ onCreateFolder: resolving });
420
+ await open("Archive");
421
+ await click(byAriaLabel("New folder inside Archive"));
422
+ const tree = container.querySelector('[role="tree"]');
423
+ assert.ok(tree?.contains(scrolledIntoView.at(-1) ?? null));
424
+ });
425
+
426
+ it("keeps room below the last row for a form opened from it", async () => {
427
+ await mount({ onCreateFolder: resolving });
428
+ assert.match(
429
+ container.querySelector('[role="tree"]')?.className ?? "",
430
+ /\bpb-\d/,
431
+ );
432
+ });
433
+ });
434
+
177
435
  describe("create wait", () => {
178
- it("passes the row's path as the parent and selects what comes back", async () => {
436
+ it("passes the opened folder's path as the parent and selects what comes back", async () => {
179
437
  const calls: Array<[string, string]> = [];
180
438
  const selected: string[] = [];
181
439
  await mount({
@@ -185,11 +443,12 @@ describe("create wait", () => {
185
443
  return Promise.resolve(created(name, parentPath));
186
444
  },
187
445
  });
446
+ await open("Travel");
188
447
  await click(byAriaLabel("New folder inside Travel"));
189
448
  await typeName("Hotels 2");
190
449
  await click(byText("Create folder"));
191
450
  assert.deepEqual(calls, [["Hotels 2", "Travel"]]);
192
- assert.deepEqual(selected, ["made"]);
451
+ assert.deepEqual(selected, ["travel", "made"]);
193
452
  assert.equal(nameField(), null);
194
453
  });
195
454
 
@@ -201,6 +460,7 @@ describe("create wait", () => {
201
460
  return new Promise<FolderTreeNode>(() => undefined);
202
461
  },
203
462
  });
463
+ await open("Travel");
204
464
  await click(byAriaLabel("New folder inside Travel"));
205
465
  await typeName("Hotels 2");
206
466
  await click(byText("Create folder"));
@@ -216,12 +476,13 @@ describe("create wait", () => {
216
476
  onCreateFolder: () =>
217
477
  Promise.reject(new Error("The mail server refused that name.")),
218
478
  });
479
+ await open("Travel");
219
480
  await click(byAriaLabel("New folder inside Travel"));
220
481
  await typeName("Hotels 2");
221
482
  await click(byText("Create folder"));
222
483
  const alert = container.querySelector('[role="alert"]');
223
484
  assert.equal(alert?.textContent, "The mail server refused that name.");
224
- assert.ok(rowOf("Travel")?.querySelector("input"));
485
+ assert.ok(createBlockOf("travel")?.querySelector("input"));
225
486
  });
226
487
 
227
488
  it("says what is missing instead of going dead on an empty name", async () => {
@@ -232,6 +493,7 @@ describe("create wait", () => {
232
493
  return Promise.resolve(created(n, p));
233
494
  },
234
495
  });
496
+ await open("Travel");
235
497
  await click(byAriaLabel("New folder inside Travel"));
236
498
  await click(byText("Create folder"));
237
499
  assert.equal(attempts, 0);
@@ -255,8 +517,8 @@ describe("create wait", () => {
255
517
  });
256
518
  },
257
519
  });
258
- await click(byAriaLabel("New folder inside Travel"));
259
- await typeName("Hotels 2");
520
+ await click(byAriaLabel("New folder"));
521
+ await typeName("Insurance");
260
522
  await click(byText("Create folder"));
261
523
  await act(async () => {
262
524
  root.unmount();
@@ -284,7 +546,8 @@ describe("filter and keyboard", () => {
284
546
  });
285
547
  };
286
548
 
287
- const press = async (target: Element, key: string) => {
549
+ const press = async (target: Element | null | undefined, key: string) => {
550
+ assert.ok(target, "control not rendered");
288
551
  await act(async () => {
289
552
  target.dispatchEvent(
290
553
  new dom.window.KeyboardEvent("keydown", { key, bubbles: true }),
@@ -292,71 +555,86 @@ describe("filter and keyboard", () => {
292
555
  });
293
556
  };
294
557
 
295
- it("narrows to the match and keeps its parent as context, not a target", async () => {
558
+ const focused = (): string | null =>
559
+ dom.window.document.activeElement?.getAttribute("aria-label") ?? null;
560
+
561
+ it("opens the ancestors a match hides behind", async () => {
296
562
  await mount({});
563
+ assert.equal(byAriaLabel("Move to Hotels"), null);
297
564
  await typeFilter("hotels");
298
565
  assert.ok(byAriaLabel("Move to Hotels"));
299
566
  assert.equal(byAriaLabel("Move to Travel"), null);
300
567
  assert.ok(byAriaLabel("Travel (containing folder)"));
568
+ assert.equal(
569
+ byAriaLabel("Travel (containing folder)")?.getAttribute("aria-expanded"),
570
+ "true",
571
+ );
301
572
  assert.equal(byAriaLabel("Move to Archive"), null);
302
573
  });
303
574
 
575
+ it("puts the list back the way it was left when the filter clears", async () => {
576
+ await mount({});
577
+ await open("Travel");
578
+ await typeFilter("archive");
579
+ assert.equal(byAriaLabel("Move to Hotels"), null);
580
+ await typeFilter("");
581
+ assert.ok(byAriaLabel("Move to Hotels"));
582
+ });
583
+
304
584
  it("says so when nothing matches", async () => {
305
585
  await mount({});
306
586
  await typeFilter("zzz");
307
587
  assert.match(container.textContent ?? "", /No folders match "zzz"/);
308
588
  });
309
589
 
310
- it("selects the focused row on Enter", async () => {
590
+ it("picks and opens the focused row on Enter", async () => {
311
591
  const selected: string[] = [];
312
592
  await mount({ onSelect: (id) => selected.push(id) });
313
- const first = byAriaLabel("Move to Travel");
314
- assert.ok(first);
315
- await press(first, "Enter");
593
+ await focus(byAriaLabel("Move to Travel"));
594
+ await press(byAriaLabel("Move to Travel"), "Enter");
316
595
  assert.deepEqual(selected, ["travel"]);
596
+ assert.ok(byAriaLabel("Move to Hotels"));
597
+ });
598
+
599
+ it("opens on Right and closes on Left without picking anything", async () => {
600
+ const selected: string[] = [];
601
+ await mount({ onSelect: (id) => selected.push(id) });
602
+ await focus(byAriaLabel("Move to Travel"));
603
+ await press(byAriaLabel("Move to Travel"), "ArrowRight");
604
+ assert.ok(byAriaLabel("Move to Hotels"));
605
+ await press(byAriaLabel("Move to Travel"), "ArrowRight");
606
+ assert.equal(focused(), "Move to Hotels");
607
+ await press(byAriaLabel("Move to Hotels"), "ArrowLeft");
608
+ assert.equal(focused(), "Move to Travel");
609
+ await press(byAriaLabel("Move to Travel"), "ArrowLeft");
610
+ assert.equal(byAriaLabel("Move to Hotels"), null);
611
+ assert.deepEqual(selected, []);
317
612
  });
318
613
 
319
- it("walks the tree with the arrow keys", async () => {
614
+ it("walks only the rows on screen with the arrow keys", async () => {
320
615
  await mount({});
321
- const first = byAriaLabel("Move to Travel");
322
- assert.ok(first);
323
- await press(first, "ArrowDown");
324
- assert.equal(
325
- dom.window.document.activeElement?.getAttribute("aria-label"),
326
- "Move to Hotels",
327
- );
328
- await press(first, "End");
329
- assert.equal(
330
- dom.window.document.activeElement?.getAttribute("aria-label"),
331
- "Move to Archive",
332
- );
333
- await press(first, "Home");
334
- assert.equal(
335
- dom.window.document.activeElement?.getAttribute("aria-label"),
336
- "Move to Travel",
337
- );
616
+ await focus(byAriaLabel("Inbox (current folder)"));
617
+ await press(byAriaLabel("Inbox (current folder)"), "ArrowDown");
618
+ assert.equal(focused(), "Move to Travel");
619
+ await press(byAriaLabel("Move to Travel"), "ArrowDown");
620
+ assert.equal(focused(), "Move to Archive");
621
+ await press(byAriaLabel("Move to Archive"), "Home");
622
+ assert.equal(focused(), "Inbox (current folder)");
623
+ await press(byAriaLabel("Inbox (current folder)"), "End");
624
+ assert.equal(focused(), "Move to Archive");
338
625
  });
339
626
 
340
627
  it("cancels on Escape from the tree and from the filter", async () => {
341
628
  let cancelled = 0;
342
629
  await mount({ onCancel: () => (cancelled += 1) });
343
- const first = byAriaLabel("Move to Travel");
344
- assert.ok(first);
345
- await press(first, "Escape");
346
- const filter = filterField();
347
- assert.ok(filter);
348
- await press(filter, "Escape");
630
+ await press(byAriaLabel("Move to Travel"), "Escape");
631
+ await press(filterField(), "Escape");
349
632
  assert.equal(cancelled, 2);
350
633
  });
351
634
 
352
- it("hands focus from the filter to the first destination on ArrowDown", async () => {
635
+ it("hands focus from the filter to the first row on ArrowDown", async () => {
353
636
  await mount({});
354
- const filter = filterField();
355
- assert.ok(filter);
356
- await press(filter, "ArrowDown");
357
- assert.equal(
358
- dom.window.document.activeElement?.getAttribute("aria-label"),
359
- "Move to Travel",
360
- );
637
+ await press(filterField(), "ArrowDown");
638
+ assert.equal(focused(), "Inbox (current folder)");
361
639
  });
362
640
  });