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
/**
* Regression tests for bugs 2054189 and 2054237. In Classic layout, the
* resize-with-window pane locks must never break the pane geometry: the
* folder pane always spans the full window height, the thread pane, message
* pane splitter and message pane tile their column without gaps, and the
* thread pane can always be dragged down to its minimum height.
*
* The assertions here deliberately check the resulting geometry instead of
* implementation details (locked inline styles), so they should catch
* stale-lock bugs regardless of which code path triggers them. The waits use
* the splitter's "--messagePaneSplitter-height" variable, which is "0px"
* while collapsed and returns to "100%" once the splitter has finished
* asynchronously applying a restored size.
*/
"use strict";
var { be_in_folder, create_folder } = ChromeUtils.importESModule(
);
var { make_message_sets_in_folders } = ChromeUtils.importESModule(
);
var { promise_new_window } = ChromeUtils.importESModule(
);
var { XULStoreUtils } = ChromeUtils.importESModule(
);
// Matches the fuzz used by the pane splitter when restoring sizes.
const FUZZ = 2;
var folder;
add_setup(async function () {
folder = await create_folder("PaneSplitterGaps");
await make_message_sets_in_folders([folder], [{ count: 3 }]);
await be_in_folder(folder);
registerCleanupFunction(() => folder.deleteSelf(null));
});
async function open3PaneWindow(
folderToShow,
features = "chrome,all,dialog=no"
) {
const newWindowPromise = promise_new_window("mail:3pane");
window.openDialog(
"_blank",
features,
folderToShow.URI,
-1
);
const win = await newWindowPromise;
await TestUtils.waitForCondition(() => {
const about3Pane =
win.document.getElementById("tabmail")?.currentAbout3Pane;
return (
about3Pane?.document.readyState == "complete" &&
about3Pane.location.href != "about:blank"
);
}, "the new 3-pane window should load about:3pane");
const about3Pane = win.document.getElementById("tabmail").currentAbout3Pane;
await TestUtils.waitForCondition(
() =>
about3Pane.gFolder == folderToShow &&
!about3Pane.document.body.classList.contains("account-central"),
"the new 3-pane window should display the test folder"
);
return win;
}
function getPanes(about3Pane) {
const doc = about3Pane.document;
return {
body: doc.body,
folderPane: doc.getElementById("folderPane"),
threadPane: doc.getElementById("threadPane"),
splitter: doc.getElementById("messagePaneSplitter"),
messagePane: doc.getElementById("messagePane"),
};
}
/**
* Assert that the classic layout panes exactly tile the window: no gaps or
* overlaps between the thread pane, the message pane splitter and the message
* pane, and a folder pane spanning the full height.
*
* @param {Window} about3Pane - The about:3pane window to check.
* @param {string} when - Description of the scenario being checked.
*/
function assertPaneGeometry(about3Pane, when) {
const { body, folderPane, threadPane, splitter, messagePane } =
getPanes(about3Pane);
const bodyRect = body.getBoundingClientRect();
const folderRect = folderPane.getBoundingClientRect();
const threadRect = threadPane.getBoundingClientRect();
const splitterRect = splitter.getBoundingClientRect();
const messageRect = messagePane.getBoundingClientRect();
Assert.lessOrEqual(
Math.abs(folderRect.height - bodyRect.height),
FUZZ,
`${when}: the folder pane should span the full height ` +
`(folder pane ${folderRect.height}, body ${bodyRect.height})`
);
Assert.lessOrEqual(
Math.abs(threadRect.top - bodyRect.top),
FUZZ,
`${when}: the thread pane should start at the top of the window`
);
// The splitter has negative margins to enlarge its hit area, so measure
// the gap between the panes and compare it with the splitter's margin-box
// extent, which is the only thing that legitimately separates them.
const splitterStyle = about3Pane.getComputedStyle(splitter);
const splitterExtent =
splitterRect.height +
parseFloat(splitterStyle.marginTop) +
parseFloat(splitterStyle.marginBottom);
const paneGap = messageRect.top - threadRect.bottom;
Assert.lessOrEqual(
Math.abs(paneGap - splitterExtent),
FUZZ,
`${when}: only the splitter should separate the thread pane and the ` +
`message pane (gap ${paneGap}, splitter extent ${splitterExtent})`
);
Assert.lessOrEqual(
Math.abs(bodyRect.bottom - messageRect.bottom),
FUZZ,
`${when}: the message pane should end at the bottom of the window`
);
}
/**
* Exercise the ways stale pane locks used to corrupt the classic layout:
* restoring a persisted size on startup, resizing the window in both
* directions, toggling the message pane, and dragging the splitter.
*/
add_task(async function test_classic_layout_has_no_pane_gaps() {
const restoredHeight = 350;
const oldPaneConfig = Services.prefs.getIntPref("mail.pane_config.dynamic");
Services.prefs.setIntPref("mail.pane_config.dynamic", 0);
XULStoreUtils.setValue("messenger", "folderPaneBox", "width", 250);
XULStoreUtils.setValue(
"messenger",
"messagepaneboxwrapper",
"collapsed",
false
);
XULStoreUtils.setValue(
"messenger",
"messagepaneboxwrapper",
"height",
restoredHeight
);
const win = await open3PaneWindow(
folder,
"chrome,all,dialog=no,width=1000,height=800"
);
try {
const about3Pane = win.document.getElementById("tabmail").currentAbout3Pane;
const { body, threadPane, splitter, messagePane } = getPanes(about3Pane);
await TestUtils.waitForCondition(
() =>
Math.abs(messagePane.getBoundingClientRect().height - restoredHeight) <=
FUZZ,
"the message pane should reach the stored height"
);
// Restoring a persisted size completes asynchronously, ending with the
// splitter handing the message pane row back to the grid as "100%".
await TestUtils.waitForCondition(
() =>
splitter.parentNode.style.getPropertyValue(
"--messagePaneSplitter-height"
) == "100%",
"the splitter should finish restoring the message pane size"
);
assertPaneGeometry(about3Pane, "after startup restore");
// Growing the window should give the new space to the message pane and
// leave the thread pane alone.
const threadHeightBeforeGrow = threadPane.getBoundingClientRect().height;
win.resizeTo(win.outerWidth, win.outerHeight + 150);
await TestUtils.waitForCondition(
() =>
messagePane.getBoundingClientRect().height > restoredHeight + 150 - 50,
"the message pane should absorb the window growth"
);
assertPaneGeometry(about3Pane, "after growing the window");
Assert.lessOrEqual(
Math.abs(
threadPane.getBoundingClientRect().height - threadHeightBeforeGrow
),
FUZZ,
"the thread pane should keep its height when the window grows"
);
// Shrinking the window must not leave a gap; a stale full-height lock on
// the folder pane used to inflate the thread pane row here.
const bodyHeightBeforeShrink = body.getBoundingClientRect().height;
win.resizeTo(win.outerWidth, win.outerHeight - 350);
await TestUtils.waitForCondition(
() => body.getBoundingClientRect().height < bodyHeightBeforeShrink - 300,
"the window should shrink"
);
assertPaneGeometry(about3Pane, "after shrinking the window");
// Toggling the message pane (F8) must restore a clean layout.
about3Pane.paneLayout.messagePaneVisible = false;
await TestUtils.waitForCondition(
() =>
splitter.parentNode.style.getPropertyValue(
"--messagePaneSplitter-height"
) == "0px",
"the message pane should collapse"
);
about3Pane.paneLayout.messagePaneVisible = true;
await TestUtils.waitForCondition(
() => messagePane.getBoundingClientRect().height > 50,
"the message pane should become visible again"
);
// Re-expanding restores the persisted size asynchronously, ending with
// the splitter handing the message pane row back to the grid as "100%".
await TestUtils.waitForCondition(
() =>
splitter.parentNode.style.getPropertyValue(
"--messagePaneSplitter-height"
) == "100%",
"the splitter should finish restoring the message pane size"
);
assertPaneGeometry(about3Pane, "after toggling the message pane");
// The thread pane must be draggable down to (near) its minimum height,
// not be propped up by other panes' locks.
const threadRect = threadPane.getBoundingClientRect();
const splitterRect = splitter.getBoundingClientRect();
const dragX = splitterRect.left + splitterRect.width / 2;
const dragStartY = splitterRect.top + splitterRect.height / 2;
const dragEndY = threadRect.top + 20;
EventUtils.synthesizeMouseAtPoint(
dragX,
dragStartY,
{ type: "mousedown", buttons: 1 },
about3Pane
);
Assert.ok(!!splitter._dragStartInfo, "the splitter drag should start");
// Move in two steps so the drag reliably starts.
EventUtils.synthesizeMouseAtPoint(
dragX,
dragStartY - 20,
{ type: "mousemove", buttons: 1 },
about3Pane
);
// The splitter processes at most one mousemove per frame, so a frame must
// pass before the next mousemove or it is ignored.
await new Promise(resolve => about3Pane.requestAnimationFrame(resolve));
EventUtils.synthesizeMouseAtPoint(
dragX,
dragEndY,
{ type: "mousemove", buttons: 1 },
about3Pane
);
EventUtils.synthesizeMouseAtPoint(
dragX,
dragEndY,
{ type: "mouseup" },
about3Pane
);
// On mouseup the splitter re-locks the panes at their new sizes and
// hands the message pane row back to the grid, asynchronously.
await TestUtils.waitForCondition(
() =>
splitter.parentNode.style.getPropertyValue(
"--messagePaneSplitter-height"
) == "100%",
"the splitter should re-enable resize-with-window after the drag"
);
const minHeight = parseFloat(
about3Pane.getComputedStyle(threadPane).minBlockSize
);
Assert.lessOrEqual(
threadPane.getBoundingClientRect().height,
minHeight + FUZZ,
"the thread pane should shrink to its minimum height when the " +
"splitter is dragged to the top"
);
assertPaneGeometry(about3Pane, "after dragging the splitter up");
// And the layout must survive a window resize after the drag.
const bodyHeightBeforeGrow = body.getBoundingClientRect().height;
win.resizeTo(win.outerWidth, win.outerHeight + 100);
await TestUtils.waitForCondition(
() => body.getBoundingClientRect().height > bodyHeightBeforeGrow + 50,
"the window should grow again"
);
assertPaneGeometry(about3Pane, "after resizing following a drag");
} finally {
Services.prefs.setIntPref("mail.pane_config.dynamic", oldPaneConfig);
XULStoreUtils.removeValue("messenger", "folderPaneBox", "width");
XULStoreUtils.removeValue("messenger", "messagepaneboxwrapper", "height");
XULStoreUtils.removeValue(
"messenger",
"messagepaneboxwrapper",
"collapsed"
);
const closePromise = BrowserTestUtils.domWindowClosed(win);
win.close();
await closePromise;
}
});