summaryrefslogtreecommitdiffstats
path: root/comm/mail/test/browser/composition/browser_focus.js
blob: 852f2e99cc6052a0dc769b924fc72e554e03626e (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
/* This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */

/*
 * Test that cycling through the focus of the 3pane's panes works correctly.
 */

"use strict";

var { add_attachments, close_compose_window, open_compose_new_mail } =
  ChromeUtils.import("resource://testing-common/mozmill/ComposeHelpers.jsm");
var { mc } = ChromeUtils.import(
  "resource://testing-common/mozmill/FolderDisplayHelpers.jsm"
);

requestLongerTimeout(3);

/**
 * Test the cycling of focus in the composition window through (Shift+)F6.
 *
 * @param {MozMillController} controller - Controller for the compose window.
 * @param {object} options - Options to set for the test.
 * @param {boolean} options.useTab - Whether to use Ctrl+Tab instead of F6.
 * @param {boolean} options.attachment - Whether to add an attachment.
 * @param {boolean} options.notifications - Whether to show notifications.
 * @param {boolean} options.languageButton - Whether to show the language
 *   menu button.
 * @param {boolean} options.contacts - Whether to show the contacts side pane.
 * @param {string} otherHeader - The name of the custom header to show.
 */
async function checkFocusCycling(controller, options) {
  let win = controller.window;
  let doc = win.document;
  let contactDoc;
  let contactsInput;
  let identityElement = doc.getElementById("msgIdentity");
  let bccButton = doc.getElementById("addr_bccShowAddressRowButton");
  let toInput = doc.getElementById("toAddrInput");
  let bccInput = doc.getElementById("bccAddrInput");
  let subjectInput = doc.getElementById("msgSubject");
  let editorElement = doc.getElementById("messageEditor");
  let attachmentElement = doc.getElementById("attachmentBucket");
  let extraMenuButton = doc.getElementById("extraAddressRowsMenuButton");
  let languageButton = doc.getElementById("languageStatusButton");
  let firstNotification;
  let secondNotification;

  if (Services.ww.activeWindow != win) {
    // Wait for the window to be in focus before beginning.
    await BrowserTestUtils.waitForEvent(win, "activate");
  }

  let key = options.useTab ? "VK_TAB" : "VK_F6";
  let goForward = () =>
    EventUtils.synthesizeKey(key, { ctrlKey: options.useTab }, win);
  let goBackward = () =>
    EventUtils.synthesizeKey(
      key,
      { ctrlKey: options.useTab, shiftKey: true },
      win
    );

  if (options.attachment) {
    add_attachments(controller, "https://www.mozilla.org/");
  }

  if (options.contacts) {
    // Open the contacts sidebar.
    EventUtils.synthesizeKey("VK_F9", {}, win);
    contactsInput = await TestUtils.waitForCondition(() => {
      contactDoc = doc.getElementById("contactsBrowser").contentDocument;
      return contactDoc.getElementById("peopleSearchInput");
    }, "Waiting for the contacts pane to load");
  }

  if (options.languageButton) {
    // languageButton only shows if we have more than one dictionary, but we
    // will show it anyway.
    languageButton.hidden = false;
  }

  // Show the bcc row by clicking the button.
  EventUtils.synthesizeMouseAtCenter(bccButton, {}, win);

  // Show the custom row.
  let otherRow = doc.querySelector(
    `.address-row[data-recipienttype="${options.otherHeader}"]`
  );
  // Show the input.
  let menu = doc.getElementById("extraAddressRowsMenu");
  let promise = BrowserTestUtils.waitForEvent(menu, "popupshown");
  EventUtils.synthesizeMouseAtCenter(extraMenuButton, {}, win);
  await promise;
  promise = BrowserTestUtils.waitForEvent(menu, "popuphidden");
  menu.activateItem(doc.getElementById(otherRow.dataset.showSelfMenuitem));
  await promise;
  let otherHeaderInput = otherRow.querySelector(".address-row-input");

  // Move the initial focus back to the To input.
  toInput.focus();

  if (options.notifications) {
    // Exceed the recipient threshold.
    Assert.equal(
      win.gComposeNotification.allNotifications.length,
      0,
      "Should be no initial notifications"
    );
    let notificationPromise = TestUtils.waitForCondition(
      () => win.gComposeNotification.allNotifications[0],
      "First notification shown"
    );
    EventUtils.sendString("a@b.org,c@d.org", win);
    EventUtils.synthesizeKey("KEY_Enter", {}, win);
    firstNotification = await notificationPromise;
  }

  // We start on the addressing widget and go from there.

  // From To to Subject.
  goForward();
  Assert.ok(bccInput.matches(":focus"), "forward to bcc row");
  goForward();
  Assert.ok(otherHeaderInput.matches(":focus"), "forward to other row");
  goForward();
  Assert.ok(subjectInput.matches(":focus"), "forward to subject");

  if (options.notifications && !options.attachment) {
    // Include an attachment key word in the subject.
    let notificationPromise = TestUtils.waitForCondition(() => {
      let notifications = win.gComposeNotification.allNotifications;
      if (notifications.length != 2) {
        return null;
      }
      return notifications[1];
    }, "Second notification shown");
    EventUtils.sendString("My attached file", win);
    secondNotification = await notificationPromise;
    Assert.notEqual(
      firstNotification,
      secondNotification,
      "New notification shown second"
    );
  }

  // From Subject to Message Body.
  goForward();
  // The editor's body will not match ":focus", even when it has focus, instead,
  // we use the parent window's activeElement.
  Assert.equal(editorElement, doc.activeElement, "forward to message body");

  // From Message Body to Attachment bucket if visible.
  goForward();
  if (options.attachment) {
    Assert.ok(attachmentElement.matches(":focus"), "forward to attachments");
    goForward();
  }

  if (options.notifications) {
    Assert.equal(
      firstNotification,
      doc.activeElement,
      "forward to notification"
    );
    goForward();
  }

  // From Message Body (or Attachment bucket) to Language button.
  if (options.languageButton) {
    Assert.ok(languageButton.matches(":focus"), "forward to status bar");
    goForward();
  }

  // From Language button to contacts pane.
  if (options.contacts) {
    Assert.ok(
      contactsInput.matches(":focus-within"),
      "forward to contacts pane"
    );
    goForward();
  }

  // From contacts pane to identity.
  Assert.ok(identityElement.matches(":focus"), "forward to 'from' row");

  // Back to the To input.
  goForward();
  Assert.ok(toInput.matches(":focus"), "forward to 'to' row");

  // Reverse the direction.

  goBackward();
  Assert.ok(identityElement.matches(":focus"), "backward to 'from' row");

  goBackward();
  if (options.contacts) {
    Assert.ok(
      contactsInput.matches(":focus-within"),
      "backward to contacts pane"
    );
    goBackward();
  }

  if (options.languageButton) {
    Assert.ok(languageButton.matches(":focus"), "backward to status bar");
    goBackward();
  }

  if (options.notifications) {
    Assert.equal(
      firstNotification,
      doc.activeElement,
      "backward to notification"
    );
    goBackward();
  }

  if (options.attachment) {
    Assert.ok(attachmentElement.matches(":focus"), "backward to attachments");
    goBackward();
  }

  Assert.equal(editorElement, doc.activeElement, "backward to message body");
  goBackward();
  Assert.ok(subjectInput.matches(":focus"), "backward to subject");
  goBackward();
  Assert.ok(otherHeaderInput.matches(":focus"), "backward to other row");
  goBackward();
  Assert.ok(bccInput.matches(":focus"), "backward to bcc row");
  goBackward();

  Assert.ok(toInput.matches(":focus"), "backward to 'to' row");

  // Now test some other elements that aren't the main focus point of their
  // areas. I.e. focusable elements that are within an area, but are not
  // focused when the area is *entered* through F6 or Ctrl+Tab. When these
  // elements have focus, we still want F6 or Ctrl+Tab to move the focus to the
  // neighbouring area.

  // Focus the close button.
  let bccCloseButton = doc.querySelector("#addressRowBcc .remove-field-button");
  bccCloseButton.focus();
  goForward();
  Assert.ok(
    otherHeaderInput.matches(":focus"),
    "from close bcc button to other row"
  );
  goBackward();
  // The input is focused on return.
  Assert.ok(bccInput.matches(":focus"), "back to bcc row");
  // Same the other way.
  bccCloseButton.focus();
  goBackward();
  Assert.ok(toInput.matches(":focus"), "from close bcc button to 'to' row");

  if (options.contacts) {
    let addressBookList = contactDoc.getElementById("addressbookList");
    addressBookList.focus();
    goForward();
    Assert.ok(
      identityElement.matches(":focus"),
      "from addressbook selector to 'from' row"
    );
    goBackward();
    // The input is focused on return.
    Assert.ok(contactsInput.matches(":focus-within"), "back to contacts input");
    // Same the other way.
    addressBookList.focus();
    goBackward();
    if (options.languageButton) {
      Assert.ok(
        languageButton.matches(":focus"),
        "from addressbook selector to status bar"
      );
    } else if (options.notifications) {
      Assert.equal(
        firstNotification,
        doc.activeElement,
        "from addressbook selector to notification"
      );
    } else if (options.attachment) {
      Assert.ok(
        attachmentElement.matches(":focus"),
        "from addressbook selector to attachments"
      );
    } else {
      Assert.equal(
        editorElement,
        doc.activeElement,
        "from addressbook selector to message body"
      );
    }
  }

  // Cc button and extra address rows menu button are in the same area as the
  // message identity.
  let ccButton = doc.getElementById("addr_ccShowAddressRowButton");
  ccButton.focus();
  goBackward();
  if (options.contacts) {
    Assert.ok(
      contactsInput.matches(":focus-within"),
      "from Cc button to contacts"
    );
  } else if (options.languageButton) {
    Assert.ok(languageButton.matches(":focus"), "from Cc button to status bar");
  } else if (options.notifications) {
    Assert.equal(
      firstNotification,
      doc.activeElement,
      "from Cc button to notification"
    );
  } else if (options.attachment) {
    Assert.ok(
      attachmentElement.matches(":focus"),
      "from Cc button to attachments"
    );
  } else {
    Assert.equal(
      editorElement,
      doc.activeElement,
      "from Cc button to message body"
    );
  }
  goForward();
  // Return to the input.
  Assert.ok(identityElement.matches(":focus"), "back to 'from' row");

  // Try in the other direction with the extra menu button.
  extraMenuButton.focus();
  goForward();
  Assert.ok(toInput.matches(":focus"), "from extra menu button to 'to' row");
  goBackward();
  // Return to the input.
  Assert.ok(identityElement.matches(":focus"), "back to 'from' row again");

  if (options.attachment) {
    let attachmentArea = doc.getElementById("attachmentArea");
    let attachmentSummary = attachmentArea.querySelector("summary");
    Assert.ok(attachmentArea.open, "Attachment area should be open");
    for (let open of [true, false]) {
      if (open) {
        Assert.ok(attachmentArea.open, "Attachment area should be open");
      } else {
        // Close the attachment bucket. In this case, the focus will move to the
        // summary element (where the bucket can be shown again).
        EventUtils.synthesizeMouseAtCenter(attachmentSummary, {}, win);
        Assert.ok(!attachmentArea.open, "Attachment area should be closed");
      }

      // Focus the attachmentSummary.
      attachmentSummary.focus();
      goBackward();
      Assert.equal(
        editorElement,
        doc.activeElement,
        `backward from attachment summary (open: ${open}) to message body`
      );
      goForward();
      if (open) {
        // Focus returns to the bucket when it is open.
        Assert.ok(
          attachmentElement.matches(":focus"),
          "forward to attachment bucket"
        );
      } else {
        // Otherwise, it returns to the summary.
        Assert.ok(
          attachmentSummary.matches(":focus"),
          "forward to attachment summary"
        );
      }
      // Try reverse.
      attachmentSummary.focus();
      goForward();
      if (options.notifications) {
        Assert.equal(
          firstNotification,
          doc.activeElement,
          `forward from attachment summary (open: ${open}) to notification`
        );
      } else if (options.languageButton) {
        Assert.ok(
          languageButton.matches(":focus"),
          `forward from attachment summary (open: ${open}) to status bar`
        );
      } else if (options.contacts) {
        Assert.ok(
          contactsInput.matches(":focus-within"),
          `forward from attachment summary (open: ${open}) to contacts pane`
        );
      } else {
        Assert.ok(
          identityElement.matches(":focus"),
          `forward from attachment summary (open: ${open}) to 'from' row`
        );
      }
      goBackward();
      if (open) {
        Assert.ok(
          attachmentElement.matches(":focus"),
          "return to attachment bucket"
        );
      } else {
        Assert.ok(
          attachmentSummary.matches(":focus"),
          "return to attachment summary"
        );
        // Open again.
        EventUtils.synthesizeMouseAtCenter(attachmentSummary, {}, win);
        Assert.ok(attachmentArea.open, "Attachment area should be open again");
      }
    }
  }

  if (options.notifications) {
    // Focus inside the notification.
    let closeButton = (secondNotification || firstNotification).closeButton;
    closeButton.focus();

    goBackward();

    if (options.attachment) {
      Assert.ok(
        attachmentElement.matches(":focus"),
        "backward from notification button to attachments"
      );
    } else {
      Assert.equal(
        editorElement,
        doc.activeElement,
        "backward from notification button to message body"
      );
    }
    goForward();
    // Go to the first notification.
    Assert.equal(
      firstNotification,
      doc.activeElement,
      "forward to the first notification"
    );

    // Try reverse.
    closeButton.focus();
    goForward();
    if (options.languageButton) {
      Assert.ok(
        languageButton.matches(":focus"),
        "forward from notification button to status bar"
      );
    } else if (options.contacts) {
      Assert.ok(
        contactsInput.matches(":focus-within"),
        "forward from notification button to contacts pane"
      );
    } else {
      Assert.ok(
        identityElement.matches(":focus"),
        "forward from notification button to 'from' row"
      );
    }
    goBackward();
    Assert.equal(
      firstNotification,
      doc.activeElement,
      "return to the first notification"
    );
  }

  // Contacts pane is persistent, so we close it again.
  if (options.contacts) {
    // Close the contacts sidebar.
    EventUtils.synthesizeKey("VK_F9", {}, win);
  }
}

add_task(async function test_jump_focus() {
  // Make sure the accessibility tabfocus is set to 7 to enable normal Tab
  // focus on non-input field elements. This is necessary only for macOS as
  // the default value is 2 instead of the default 7 used on Windows and Linux.
  Services.prefs.setIntPref("accessibility.tabfocus", 7);
  let prevHeader = Services.prefs.getCharPref("mail.compose.other.header");
  let prevThreshold = Services.prefs.getIntPref(
    "mail.compose.warn_public_recipients.threshold"
  );
  // Set two custom headers, but only one is shown.
  Services.prefs.setCharPref(
    "mail.compose.other.header",
    "X-Header2,X-Header1"
  );
  Services.prefs.setIntPref("mail.compose.warn_public_recipients.threshold", 2);
  for (let useTab of [false, true]) {
    for (let attachment of [false, true]) {
      for (let notifications of [false, true]) {
        for (let languageButton of [false, true]) {
          for (let contacts of [false, true]) {
            let options = {
              useTab,
              attachment,
              notifications,
              languageButton,
              contacts,
              otherHeader: "X-Header1",
            };
            info(`Test run: ${JSON.stringify(options)}`);
            let controller = open_compose_new_mail();
            await checkFocusCycling(controller, options);
            close_compose_window(controller);
          }
        }
      }
    }
  }

  // Reset the preferences.
  Services.prefs.clearUserPref("accessibility.tabfocus");
  Services.prefs.setCharPref("mail.compose.other.header", prevHeader);
  Services.prefs.setIntPref(
    "mail.compose.warn_public_recipients.threshold",
    prevThreshold
  );
});