Revision control

Copy as Markdown

Other Tools

/* 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/. */
"use strict";
const { close_compose_window, open_compose_new_mail } =
ChromeUtils.importESModule(
);
add_task(async function test_tabBetweenSubjectAndBody() {
const identity = MailServices.accounts.defaultAccount.defaultIdentity;
for (const html of [true, false]) {
await SpecialPowers.pushPrefEnv({
set: [[`mail.identity.${identity.key}.compose_html`, html]],
});
const win = await open_compose_new_mail();
registerCleanupFunction(async () => {
if (!win.closed) {
await close_compose_window(win);
}
});
const doc = win.document;
const subject = doc.getElementById("msgSubject");
const editor = doc.getElementById("messageEditor");
subject.focus();
EventUtils.synthesizeKey("KEY_Tab", {}, win);
Assert.equal(
doc.activeElement,
editor,
`Tab from Subject should focus the editor (HTML: ${html})`
);
EventUtils.synthesizeKey("KEY_Tab", { shiftKey: true }, win);
Assert.equal(
doc.activeElement,
subject,
`Shift+Tab from the editor should focus Subject (HTML: ${html})`
);
EventUtils.synthesizeKey("KEY_F6", {}, win);
Assert.equal(
doc.activeElement,
editor,
"F6 from Subject should still focus the editor"
);
EventUtils.synthesizeKey("KEY_F6", { shiftKey: true }, win);
Assert.equal(
doc.activeElement,
subject,
"Shift+F6 from the editor should still focus Subject"
);
await close_compose_window(win);
await SpecialPowers.popPrefEnv();
}
});
add_task(async function test_resizeHeaderArea() {
const win = await open_compose_new_mail();
registerCleanupFunction(async () => {
if (!win.closed) {
await close_compose_window(win);
}
});
const doc = win.document;
const subject = doc.getElementById("msgSubject");
const editor = doc.getElementById("messageEditor");
const headers = doc.getElementById("MsgHeadersToolbar");
const splitter = doc.getElementById("headersSplitter");
const resizeItem = doc.getElementById("menu_resizeHeaderArea");
await doc.l10n.translateElements([splitter]);
Assert.equal(
splitter.getAttribute("role"),
"separator",
"The header splitter should retain separator semantics"
);
Assert.equal(
doc.l10n.getAttributes(splitter).id,
"compose-header-area-splitter",
"The header splitter should have a localized name and resize instructions"
);
Assert.deepEqual(
splitter.ariaControlsElements,
[headers],
"The header splitter should identify the region it resizes"
);
Assert.equal(
doc.l10n.getAttributes(resizeItem).id,
"menuitem-resize-header-area",
"The View command should have a localized label and access key"
);
Assert.ok(
splitter.ariaLabel && splitter.ariaDescription,
"The header splitter should expose its name and resize instructions"
);
for (const previousFocus of [subject, editor]) {
if (previousFocus == editor) {
win.focusMsgBody();
} else {
previousFocus.focus();
}
resizeItem.doCommand();
Assert.equal(
doc.activeElement,
splitter,
"The View command should focus the header splitter"
);
const style = win.getComputedStyle(splitter);
Assert.notEqual(
style.outlineStyle,
"none",
"The focused header splitter should have a visible focus outline"
);
Assert.greater(
parseFloat(style.outlineWidth),
0,
"The focused header splitter's outline should have a positive width"
);
const height = headers.getBoundingClientRect().height;
for (const [key, options, delta] of [
["KEY_ArrowDown", {}, 10],
["KEY_ArrowDown", { shiftKey: true }, 60],
["KEY_ArrowUp", {}, 50],
["KEY_ArrowUp", { shiftKey: true }, 0],
]) {
const resized = BrowserTestUtils.waitForEvent(
splitter,
"splitter-resized"
);
EventUtils.synthesizeKey(key, options, win);
await resized;
Assert.equal(
headers.getBoundingClientRect().height,
height + delta,
"Arrow keys should resize the headers, with larger steps for Shift"
);
Assert.equal(
Number(splitter.getAttribute("aria-valuenow")),
Math.round(height + delta),
"The accessible value should reflect the header height"
);
Assert.equal(
splitter.tabIndex,
-1,
"Resizing should keep the header splitter out of the tab order"
);
}
EventUtils.synthesizeKey("KEY_Tab", {}, win);
Assert.equal(
doc.activeElement,
editor,
"Tab should leave the header splitter and focus the editor"
);
}
subject.focus();
resizeItem.doCommand();
EventUtils.synthesizeKey("KEY_Tab", { shiftKey: true }, win);
Assert.equal(
doc.activeElement,
subject,
"Shift+Tab should leave the header splitter and focus Subject"
);
resizeItem.doCommand();
resizeItem.doCommand();
Assert.equal(
doc.activeElement,
splitter,
"Repeated resize commands should keep focus on the splitter"
);
await close_compose_window(win);
});
add_task(async function test_mouseResizeWithoutMenu() {
const win = await open_compose_new_mail();
registerCleanupFunction(async () => {
if (!win.closed) {
await close_compose_window(win);
}
});
const doc = win.document;
const headers = doc.getElementById("MsgHeadersToolbar");
const splitter = doc.getElementById("headersSplitter");
const height = headers.getBoundingClientRect().height;
const rect = splitter.getBoundingClientRect();
const x = rect.x + rect.width / 2;
const y = rect.y + rect.height / 2;
const resized = BrowserTestUtils.waitForEvent(splitter, "splitter-resized");
EventUtils.synthesizeMouseAtCenter(
splitter,
{ type: "mousedown", buttons: 1 },
win
);
EventUtils.synthesizeMouse(
doc.documentElement,
x,
y + 30,
{ type: "mousemove", buttons: 1 },
win
);
EventUtils.synthesizeMouse(
doc.documentElement,
x,
y + 30,
{ type: "mouseup" },
win
);
await resized;
Assert.equal(
headers.getBoundingClientRect().height,
height + 30,
"Dragging should resize the headers without invoking the menu command"
);
Assert.equal(
splitter.tabIndex,
-1,
"Mouse resizing should keep the splitter out of the tab order"
);
await close_compose_window(win);
});
add_task(async function test_resizeHeaderAreaCommand() {
const win = await open_compose_new_mail();
registerCleanupFunction(async () => {
if (!win.closed) {
await close_compose_window(win);
}
});
const doc = win.document;
const subject = doc.getElementById("msgSubject");
const editor = doc.getElementById("messageEditor");
const headers = doc.getElementById("MsgHeadersToolbar");
const splitter = doc.getElementById("headersSplitter");
const viewMenu = doc.getElementById("menu_View");
const popup = doc.getElementById("menu_View_Popup");
const resizeItem = doc.getElementById("menu_resizeHeaderArea");
const menuItems = Array.from(popup.children).filter(item =>
item.matches("menu, menuitem")
);
await doc.l10n.translateElements(
menuItems.filter(item => doc.l10n.getAttributes(item).id)
);
const resizeAccessKey = resizeItem.getAttribute("accesskey")?.toLowerCase();
Assert.ok(resizeAccessKey, "The resize command should have an access key");
Assert.deepEqual(
menuItems.filter(
item => item.getAttribute("accesskey")?.toLowerCase() == resizeAccessKey
),
[resizeItem],
`The resize access key (${resizeAccessKey}) should be unique in the View menu`
);
if (AppConstants.platform != "macosx") {
// Exclude the existing Toolbars/To Field access key conflict from this test.
doc
.getElementById("menu_showToField")
.setAttribute("intended-duplicate-accesskey", "menu_ToolbarsNew");
}
win.focusMsgBody();
win.GetCurrentEditor().insertText("Resize from the View menu");
const selection = win.GetCurrentEditor().selection;
selection.collapse(selection.anchorNode, 3);
const bodyContent = editor.contentDocument.body.innerHTML;
for (const previousFocus of [subject, editor]) {
for (const shiftKey of [false, true]) {
if (previousFocus == editor) {
win.focusMsgBody();
} else {
subject.focus();
}
const anchorNode = selection.anchorNode;
const anchorOffset = selection.anchorOffset;
const initialHeight = headers.getBoundingClientRect().height;
if (AppConstants.platform == "macosx") {
// Native macOS menus do not support Alt access keys.
resizeItem.doCommand();
} else {
const shown = BrowserTestUtils.waitForPopupEvent(popup, "shown");
EventUtils.synthesizeKey(
viewMenu.getAttribute("accesskey"),
{ altKey: true },
win
);
await shown;
const hidden = BrowserTestUtils.waitForPopupEvent(popup, "hidden");
EventUtils.synthesizeKey(resizeAccessKey, {}, win);
await hidden;
// Let queued menu dismissal finish before checking the splitter's focus.
await new Promise(resolve => win.requestAnimationFrame(resolve));
}
Assert.equal(
doc.activeElement,
splitter,
"The resize command should keep focus on the splitter after activation"
);
EventUtils.synthesizeKey("KEY_ArrowDown", {}, win);
EventUtils.synthesizeKey("KEY_Tab", { shiftKey }, win);
Assert.equal(
doc.activeElement,
shiftKey ? subject : editor,
"Tab or Shift+Tab should return to composing after using the menu"
);
Assert.greater(
headers.getBoundingClientRect().height,
initialHeight,
"Leaving the splitter should keep the new height"
);
Assert.equal(
editor.contentDocument.body.innerHTML,
bodyContent,
"The resize command should leave the message body unchanged"
);
if (previousFocus == editor) {
Assert.equal(
selection.anchorNode,
anchorNode,
"Leaving the splitter should preserve the editor caret node"
);
Assert.equal(
selection.anchorOffset,
anchorOffset,
"Leaving the splitter should preserve the editor caret offset"
);
}
}
}
await close_compose_window(win);
});
add_task(async function test_resizeWithoutCompletionKeys() {
const win = await open_compose_new_mail();
registerCleanupFunction(async () => {
if (!win.closed) {
await close_compose_window(win);
}
});
const doc = win.document;
const subject = doc.getElementById("msgSubject");
const editor = doc.getElementById("messageEditor");
const headers = doc.getElementById("MsgHeadersToolbar");
const splitter = doc.getElementById("headersSplitter");
win.focusMsgBody();
win.GetCurrentEditor().insertText("Header resize test");
const bodyContent = editor.contentDocument.body.innerHTML;
subject.focus();
win.goDoCommand("cmd_resizeHeaderArea");
EventUtils.synthesizeKey("KEY_ArrowDown", {}, win);
const height = headers.getBoundingClientRect().height;
for (const key of ["KEY_Enter", "KEY_Escape"]) {
EventUtils.synthesizeKey(key, {}, win);
Assert.equal(
doc.activeElement,
splitter,
`${key} should leave focus on the splitter`
);
Assert.equal(
headers.getBoundingClientRect().height,
height,
`${key} should leave the resized height unchanged`
);
Assert.equal(
editor.contentDocument.body.innerHTML,
bodyContent,
`${key} should leave the message body unchanged`
);
Assert.equal(subject.value, "", `${key} should leave Subject unchanged`);
}
await close_compose_window(win);
});
add_task(async function test_resizeOnFocusDeparture() {
const win = await open_compose_new_mail();
registerCleanupFunction(async () => {
if (!win.closed) {
await close_compose_window(win);
}
});
const doc = win.document;
const subject = doc.getElementById("msgSubject");
const editor = doc.getElementById("messageEditor");
const headers = doc.getElementById("MsgHeadersToolbar");
const splitter = doc.getElementById("headersSplitter");
const resizeItem = doc.getElementById("menu_resizeHeaderArea");
for (const departure of ["Tab", "Shift+Tab", "click"]) {
win.focusMsgBody();
resizeItem.doCommand();
const resized = BrowserTestUtils.waitForEvent(splitter, "splitter-resized");
EventUtils.synthesizeKey("KEY_ArrowDown", { type: "keydown" }, win);
const height = headers.getBoundingClientRect().height;
if (departure == "click") {
EventUtils.synthesizeMouseAtCenter(subject, {}, win);
} else {
EventUtils.synthesizeKey(
"KEY_Tab",
{ shiftKey: departure == "Shift+Tab" },
win
);
}
Assert.equal(
doc.activeElement,
departure == "Tab" ? editor : subject,
`${departure} should move focus normally after resizing`
);
Assert.equal(
headers.getBoundingClientRect().height,
height,
"Leaving the splitter should keep its size"
);
await resized;
EventUtils.synthesizeKey("KEY_ArrowDown", { type: "keyup" }, win);
Assert.equal(
splitter.tabIndex,
-1,
"Leaving the splitter should keep it out of the tab order"
);
}
win.gWindowLocked = true;
win.updateViewItems();
Assert.ok(
doc.getElementById("cmd_resizeHeaderArea").hasAttribute("disabled"),
"The resize command should be disabled while composing is locked"
);
win.goDoCommand("cmd_resizeHeaderArea");
Assert.equal(
doc.activeElement,
subject,
"A disabled resize command should leave focus unchanged"
);
win.gWindowLocked = false;
win.updateViewItems();
await close_compose_window(win);
});