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
const dragService = Cc["@mozilla.org/widget/dragservice;1"].getService(
Ci.nsIDragService
);
let waitTime = 0;
const tabmail = document.getElementById("tabmail");
registerCleanupFunction(() => {
tabmail.closeOtherTabs(tabmail.tabInfo[0]);
Services.prefs.clearUserPref("ui.prefersReducedMotion");
Services.prefs.clearUserPref("mailnews.default_view_flags");
});
async function withMotion(subtest) {
Services.prefs.setIntPref("ui.prefersReducedMotion", 0);
waitTime = 300;
await TestUtils.waitForCondition(
() => !matchMedia("(prefers-reduced-motion)").matches
);
return subtest();
}
async function withoutMotion(subtest) {
Services.prefs.setIntPref("ui.prefersReducedMotion", 1);
waitTime = 0;
await TestUtils.waitForCondition(
() => matchMedia("(prefers-reduced-motion)").matches
);
await subtest();
}
let win, doc, list, dataTransfer;
async function orderWithKeys(key) {
selectHandler.reset();
orderedHandler.reset();
list.addEventListener("select", selectHandler);
list.addEventListener("ordered", orderedHandler);
EventUtils.synthesizeKey(key, { altKey: true }, win);
await new Promise(resolve => win.setTimeout(resolve, waitTime));
list.removeEventListener("select", selectHandler);
list.removeEventListener("ordered", orderedHandler);
await checkNoTransformations();
}
async function waitForTransition(shouldWait) {
if (shouldWait && waitTime) {
await BrowserTestUtils.waitForEvent(list, "transitionend");
}
await new Promise(resolve => win.setTimeout(resolve));
}
async function startDrag(index, shouldWait = false) {
const listRect = list.getBoundingClientRect();
const clientY = listRect.top + index * 32 + 4;
const transitionPromise = waitForTransition(shouldWait);
dragService.startDragSessionForTests(
win,
Ci.nsIDragService.DRAGDROP_ACTION_NONE
);
[, dataTransfer] = EventUtils.synthesizeDragOver(
list.rows[index],
list,
null,
null,
win,
win,
{
clientY,
_domDispatchOnly: true,
}
);
await transitionPromise;
}
async function continueDrag(index, shouldWait = false) {
const listRect = list.getBoundingClientRect();
const destClientX = listRect.left + listRect.width / 2;
const destClientY = listRect.top + index * 32 + 4;
const destScreenX = win.mozInnerScreenX + destClientX;
const destScreenY = win.mozInnerScreenY + destClientY;
const transitionPromise = waitForTransition(shouldWait);
const result = EventUtils.sendDragEvent(
{
type: "dragover",
screenX: destScreenX,
screenY: destScreenY,
clientX: destClientX,
clientY: destClientY,
dataTransfer,
_domDispatchOnly: true,
},
list,
win
);
await transitionPromise;
return result;
}
async function endDrag(index, shouldWait = false) {
const listRect = list.getBoundingClientRect();
const clientY = listRect.top + index * 32 + 4;
const transitionPromise = waitForTransition(shouldWait);
EventUtils.synthesizeDropAfterDragOver(false, dataTransfer, list, win, {
clientY,
_domDispatchOnly: true,
});
list.dispatchEvent(new CustomEvent("dragend", { bubbles: true }));
dragService.getCurrentSession().endDragSession(true);
await transitionPromise;
}
function checkRowOrder(expectedOrder) {
expectedOrder = expectedOrder.split(" ").map(i => `row-${i}`);
Assert.equal(list.rowCount, expectedOrder.length, "rowCount is correct");
Assert.deepEqual(
list.rows.map(row => row.id),
expectedOrder,
"order in DOM is correct"
);
const apparentOrder = list.rows.sort(
(a, b) => a.getBoundingClientRect().top - b.getBoundingClientRect().top
);
Assert.deepEqual(
apparentOrder.map(row => row.id),
expectedOrder,
"order on screen is correct"
);
if (orderedHandler.orderAtEvent) {
Assert.deepEqual(
orderedHandler.orderAtEvent,
expectedOrder,
"order at the last 'ordered' event was correct"
);
}
}
function checkYPositions(...expectedPositions) {
const offset = list.getBoundingClientRect().top;
for (let i = 0; i < 5; i++) {
const id = `row-${i + 1}`;
const row = doc.getElementById(id);
Assert.equal(
row.getBoundingClientRect().top - offset,
expectedPositions[i],
id
);
}
}
async function checkNoTransformations() {
for (const row of list.children) {
await TestUtils.waitForCondition(
() => win.getComputedStyle(row).transform == "none",
`${row.id} has no transforms`
);
Assert.equal(
row
.getAnimations()
.filter(animation => animation.transitionProperty != "opacity").length,
0,
`${row.id} has no animations`
);
}
}
const selectHandler = {
seenEvent: null,
reset() {
this.seenEvent = null;
},
handleEvent(event) {
this.seenEvent = event;
},
};
const orderedHandler = {
seenEvent: null,
orderAtEvent: null,
reset() {
this.seenEvent = null;
this.orderAtEvent = null;
},
handleEvent(event) {
if (this.seenEvent) {
throw new Error("we already have an 'ordered' event");
}
this.seenEvent = event;
this.orderAtEvent = list.rows.map(row => row.id);
},
};
/** Test Alt+Up and Alt+Down. */
async function subtestKeyReorder() {
list.focus();
list.selectedIndex = 0;
// Move row 1 down the list to the bottom.
await orderWithKeys("KEY_ArrowDown");
Assert.ok(orderedHandler.seenEvent);
checkRowOrder("2 2-1 2-2 1 3 3-1 3-2 3-3 4 5 5-1 5-2");
// Some additional checks to prove the right row is selected.
Assert.ok(!selectHandler.seenEvent);
Assert.equal(list.selectedIndex, 3, "correct index is selected");
Assert.equal(
list.querySelector(".selected").id,
"row-1",
"correct row is selected"
);
EventUtils.synthesizeKey("KEY_ArrowUp", {}, win);
Assert.equal(
list.querySelector(".selected").id,
"row-2-2",
"key press moved to the correct row"
);
EventUtils.synthesizeKey("KEY_ArrowDown", {}, win);
await orderWithKeys("KEY_ArrowDown");
Assert.ok(orderedHandler.seenEvent);
checkRowOrder("2 2-1 2-2 3 3-1 3-2 3-3 1 4 5 5-1 5-2");
await orderWithKeys("KEY_ArrowDown");
Assert.ok(orderedHandler.seenEvent);
checkRowOrder("2 2-1 2-2 3 3-1 3-2 3-3 4 1 5 5-1 5-2");
await orderWithKeys("KEY_ArrowDown");
Assert.ok(orderedHandler.seenEvent);
checkRowOrder("2 2-1 2-2 3 3-1 3-2 3-3 4 5 5-1 5-2 1");
// Move row 1 back to the top.
await orderWithKeys("KEY_ArrowUp");
Assert.ok(orderedHandler.seenEvent);
checkRowOrder("2 2-1 2-2 3 3-1 3-2 3-3 4 1 5 5-1 5-2");
await orderWithKeys("KEY_ArrowUp");
Assert.ok(orderedHandler.seenEvent);
checkRowOrder("2 2-1 2-2 3 3-1 3-2 3-3 1 4 5 5-1 5-2");
await orderWithKeys("KEY_ArrowUp");
Assert.ok(orderedHandler.seenEvent);
checkRowOrder("2 2-1 2-2 1 3 3-1 3-2 3-3 4 5 5-1 5-2");
await orderWithKeys("KEY_ArrowUp");
Assert.ok(orderedHandler.seenEvent);
checkRowOrder("1 2 2-1 2-2 3 3-1 3-2 3-3 4 5 5-1 5-2");
// Move row 3 around. Row 3 has children, so we're checking they move with it.
list.selectedIndex = 4;
await orderWithKeys("KEY_ArrowUp");
Assert.ok(orderedHandler.seenEvent);
checkRowOrder("1 3 3-1 3-2 3-3 2 2-1 2-2 4 5 5-1 5-2");
await orderWithKeys("KEY_ArrowUp");
Assert.ok(orderedHandler.seenEvent);
checkRowOrder("3 3-1 3-2 3-3 1 2 2-1 2-2 4 5 5-1 5-2");
await orderWithKeys("KEY_ArrowDown");
Assert.ok(orderedHandler.seenEvent);
checkRowOrder("1 3 3-1 3-2 3-3 2 2-1 2-2 4 5 5-1 5-2");
await orderWithKeys("KEY_ArrowDown");
Assert.ok(orderedHandler.seenEvent);
checkRowOrder("1 2 2-1 2-2 3 3-1 3-2 3-3 4 5 5-1 5-2");
await orderWithKeys("KEY_ArrowDown");
Assert.ok(orderedHandler.seenEvent);
checkRowOrder("1 2 2-1 2-2 4 3 3-1 3-2 3-3 5 5-1 5-2");
await orderWithKeys("KEY_ArrowDown");
Assert.ok(orderedHandler.seenEvent);
checkRowOrder("1 2 2-1 2-2 4 5 5-1 5-2 3 3-1 3-2 3-3");
await orderWithKeys("KEY_ArrowUp");
Assert.ok(orderedHandler.seenEvent);
checkRowOrder("1 2 2-1 2-2 4 3 3-1 3-2 3-3 5 5-1 5-2");
await orderWithKeys("KEY_ArrowUp");
Assert.ok(orderedHandler.seenEvent);
checkRowOrder("1 2 2-1 2-2 3 3-1 3-2 3-3 4 5 5-1 5-2");
await checkNoTransformations();
}
/** Drag the first item to the end. */
async function subtestDragReorder1() {
orderedHandler.reset();
list.addEventListener("ordered", orderedHandler);
checkYPositions(1, 33, 129, 257, 289);
await startDrag(0, true);
checkYPositions(1, 33, 129, 257, 289);
await continueDrag(2, true);
checkYPositions(52, 1, 129, 257, 289);
await continueDrag(3);
checkYPositions(84, 1, 129, 257, 289);
await continueDrag(4);
checkYPositions(116, 1, 129, 257, 289);
await continueDrag(5);
checkYPositions(148, 1, 129, 257, 289);
await continueDrag(6, true);
checkYPositions(180, 1, 97, 257, 289);
await continueDrag(7);
checkYPositions(212, 1, 97, 257, 289);
await continueDrag(8, true);
checkYPositions(244, 1, 97, 225, 289);
await continueDrag(9);
checkYPositions(276, 1, 97, 225, 289);
await continueDrag(10, true);
checkYPositions(308, 1, 97, 225, 257);
await continueDrag(11);
checkYPositions(340, 1, 97, 225, 257);
await continueDrag(12);
checkYPositions(353, 1, 97, 225, 257);
await endDrag(12, true);
list.removeEventListener("ordered", orderedHandler);
Assert.ok(orderedHandler.seenEvent);
checkYPositions(353, 1, 97, 225, 257);
checkRowOrder("2 2-1 2-2 3 3-1 3-2 3-3 4 5 5-1 5-2 1");
await checkNoTransformations();
}
/** Drag the (now) last item back to the start. */
async function subtestDragReorder2() {
orderedHandler.reset();
list.addEventListener("ordered", orderedHandler);
await startDrag(11, true);
checkYPositions(340, 1, 97, 225, 257);
await continueDrag(9, true);
checkYPositions(276, 1, 97, 225, 289);
await continueDrag(7, true);
checkYPositions(212, 1, 97, 257, 289);
await continueDrag(4, true);
checkYPositions(116, 1, 129, 257, 289);
await continueDrag(1, true);
checkYPositions(20, 33, 129, 257, 289);
await endDrag(0, true);
list.removeEventListener("ordered", orderedHandler);
Assert.ok(orderedHandler.seenEvent);
checkYPositions(1, 33, 129, 257, 289);
checkRowOrder("1 2 2-1 2-2 3 3-1 3-2 3-3 4 5 5-1 5-2");
await checkNoTransformations();
}
/**
* Listen for the 'ordering' event and prevent dropping on some rows.
*
* In this test, we'll prevent dragging an item below the last one - row-5 and
* its descendants. Other use cases may be possible but haven't been needed
* yet, so they are untested.
*/
async function subtestDragUndroppable() {
const originalGetter = list.__lookupGetter__("_orderableChildren");
list.__defineGetter__("_orderableChildren", function () {
const rows = [...this.children];
rows.pop();
return rows;
});
orderedHandler.reset();
list.addEventListener("ordered", orderedHandler);
checkYPositions(1, 33, 129, 257, 289);
await startDrag(0, true);
checkYPositions(1, 33, 129, 257, 289);
await continueDrag(8, true);
checkYPositions(244, 1, 97, 225, 289);
await continueDrag(9);
checkYPositions(257, 1, 97, 225, 289);
await continueDrag(10);
checkYPositions(257, 1, 97, 225, 289);
await continueDrag(11);
checkYPositions(257, 1, 97, 225, 289);
await continueDrag(12);
checkYPositions(257, 1, 97, 225, 289);
await endDrag(12, true);
list.removeEventListener("ordered", orderedHandler);
Assert.ok(orderedHandler.seenEvent);
checkYPositions(257, 1, 97, 225, 289);
checkRowOrder("2 2-1 2-2 3 3-1 3-2 3-3 4 1 5 5-1 5-2");
await checkNoTransformations();
// Move row-3 down with the keyboard.
list.selectedIndex = 7;
await orderWithKeys("KEY_ArrowDown");
Assert.ok(orderedHandler.seenEvent);
checkRowOrder("2 2-1 2-2 3 3-1 3-2 3-3 1 4 5 5-1 5-2");
// It should not move further down.
await orderWithKeys("KEY_ArrowDown");
Assert.ok(!orderedHandler.seenEvent);
checkRowOrder("2 2-1 2-2 3 3-1 3-2 3-3 1 4 5 5-1 5-2");
// Reset the order.
await orderWithKeys("KEY_ArrowUp");
Assert.ok(orderedHandler.seenEvent);
checkRowOrder("2 2-1 2-2 3 3-1 3-2 3-3 4 1 5 5-1 5-2");
orderedHandler.reset();
await startDrag(8, true);
await continueDrag(1, true);
await endDrag(1, true);
checkRowOrder("1 2 2-1 2-2 3 3-1 3-2 3-3 4 5 5-1 5-2");
list.__defineGetter__("_orderableChildren", originalGetter);
}
add_setup(async function () {
// Make sure the whole test runs with an unthreaded view in all folders.
Services.prefs.setIntPref("mailnews.default_view_flags", 0);
const tab = tabmail.openTab("contentTab", {
url: "chrome://mochitests/content/browser/comm/mail/base/test/widgets/files/orderableTreeListbox.xhtml",
});
await BrowserTestUtils.browserLoaded(tab.browser);
tab.browser.focus();
win = tab.browser.contentWindow;
doc = win.document;
list = doc.querySelector(`ol[is="orderable-tree-listbox"]`);
Assert.ok(!!list, "the list exists");
checkRowOrder("1 2 2-1 2-2 3 3-1 3-2 3-3 4 5 5-1 5-2");
Assert.equal(list.selectedIndex, 0, "selectedIndex is set to 0");
});
add_task(async function testKeyReorder() {
await withMotion(subtestKeyReorder);
});
add_task(async function testDragReorder1() {
await withMotion(subtestDragReorder1);
});
add_task(async function testDragReorder2() {
await withMotion(subtestDragReorder2);
});
add_task(async function testDragUndroppable() {
await withMotion(subtestDragUndroppable);
});
add_task(async function testKeyReorderReducedMotion() {
await withoutMotion(subtestKeyReorder);
});
add_task(async function testDragReorder1ReducedMotion() {
await withoutMotion(subtestDragReorder1);
});
add_task(async function testDragReorder2ReducedMotion() {
await withoutMotion(subtestDragReorder2);
});
add_task(async function testDragUndroppableReducedMotion() {
await withoutMotion(subtestDragUndroppable);
});