summaryrefslogtreecommitdiffstats
path: root/comm/mail/base/test/browser/browser_selectionWidgetController.js
diff options
context:
space:
mode:
Diffstat (limited to 'comm/mail/base/test/browser/browser_selectionWidgetController.js')
-rw-r--r--comm/mail/base/test/browser/browser_selectionWidgetController.js6196
1 files changed, 6196 insertions, 0 deletions
diff --git a/comm/mail/base/test/browser/browser_selectionWidgetController.js b/comm/mail/base/test/browser/browser_selectionWidgetController.js
new file mode 100644
index 0000000000..8e57c64bdc
--- /dev/null
+++ b/comm/mail/base/test/browser/browser_selectionWidgetController.js
@@ -0,0 +1,6196 @@
+/* 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/. */
+
+var tabInfo;
+var win;
+
+add_setup(async () => {
+ tabInfo = window.openContentTab(
+ "chrome://mochitests/content/browser/comm/mail/base/test/browser/files/selectionWidget.xhtml"
+ );
+ await BrowserTestUtils.browserLoaded(tabInfo.browser);
+
+ tabInfo.browser.focus();
+ win = tabInfo.browser.contentWindow;
+});
+
+registerCleanupFunction(() => {
+ window.tabmail.closeTab(tabInfo);
+});
+
+var selectionModels = ["focus", "browse", "browse-multi"];
+
+/**
+ * The selection widget.
+ *
+ * @type {HTMLElement}
+ */
+var widget;
+/**
+ * A focusable item before the widget.
+ *
+ * @type {HTMLElement}
+ */
+var before;
+/**
+ * A focusable item after the widget.
+ *
+ * @type {HTMLElement}
+ */
+var after;
+
+/**
+ * Reset the page and create a new widget.
+ *
+ * The "widget", "before" and "after" variables will be reset to the new
+ * elements.
+ *
+ * @param {object} options - Options to set.
+ * @param {string} options.model - The selection model to use.
+ * @param {string} [options.direction="right-to-left"] - The direction of the
+ * widget. Choosing "top-to-bottom" will layout items from top to bottom.
+ * Choosing "right-to-left" or "left-to-right" will set the page's direction
+ * to "rtl" or "ltr", respectively, and will layout items in the writing
+ * direction.
+ * @param {boolean} [options.draggable=false] - Whether to make the items
+ * draggable.
+ */
+function reset(options) {
+ function createTabStop(text) {
+ let el = win.document.createElement("span");
+ el.tabIndex = 0;
+ el.id = text;
+ el.textContent = text;
+ return el;
+ }
+ before = createTabStop("before");
+ after = createTabStop("after");
+
+ let { model, direction } = options;
+ if (!direction) {
+ // Default to a less-common format.
+ direction = "right-to-left";
+ }
+ info(`Creating ${direction} widget with "${model}" model`);
+
+ widget = win.document.createElement("test-selection-widget");
+ widget.id = "widget";
+ widget.setAttribute("selection-model", model);
+ widget.setAttribute(
+ "layout-direction",
+ direction == "top-to-bottom" ? "vertical" : "horizontal"
+ );
+ widget.toggleAttribute("items-draggable", options.draggable);
+
+ win.document.body.replaceChildren(before, widget, after);
+
+ win.document.dir = direction == "left-to-right" ? "ltr" : "rtl";
+
+ before.focus();
+}
+
+/**
+ * Create an array of sequential integers.
+ *
+ * @param {number} start - The starting integer.
+ * @param {number} num - The number of integers.
+ *
+ * @returns {number[]} - Array of integers between start and (start + num - 1).
+ */
+function range(start, num) {
+ return Array.from({ length: num }, (_, i) => start + i);
+}
+
+/**
+ * Assert that the specified items are selected in the widget, and nothing else.
+ *
+ * @param {number[]} indices - The indices of the selected items.
+ * @param {string} msg - A message to use for the assertion.
+ */
+function assertSelection(indices, msg) {
+ let selected = widget.selectedIndices();
+ Assert.deepEqual(selected, indices, `Selected indices should match: ${msg}`);
+ // Test that the return of getSelectionRanges is as expected.
+ let expectRanges = [];
+ let lastIndex = -2;
+ let rangeIndex = -1;
+ for (let index of indices) {
+ if (index == lastIndex + 1) {
+ expectRanges[rangeIndex].end++;
+ } else {
+ rangeIndex++;
+ expectRanges.push({ start: index, end: index + 1 });
+ }
+ lastIndex = index;
+ }
+ Assert.deepEqual(
+ widget.getSelectionRanges(),
+ expectRanges,
+ `Selection ranges should match expected: ${msg}`
+ );
+}
+
+/**
+ * Assert that the given element is focused.
+ *
+ * @param {object} expect - The expected focused element.
+ * @param {HTMLElement} [expect.element] - The expected element that will
+ * have focus.
+ * @param {number} [expect.index] - If the `element` property is not given, this
+ * specifies the index of the item widget we expect to have focus.
+ * @param {string} [expect.text] - Optionally test that the element also has the
+ * given text content.
+ * @param {string} msg - A message to use for the assertion.
+ */
+function assertFocus(expect, msg) {
+ let expectElement;
+ let name;
+ if (expect.element != undefined) {
+ expectElement = expect.element;
+ name = `Element #${expectElement.id}`;
+ } else {
+ expectElement = widget.items[expect.index].element;
+ name = `Item ${expect.index}`;
+ }
+ let active = win.document.activeElement;
+ let activeIndex = widget.items.findIndex(i => i.element == active);
+ if (activeIndex >= 0) {
+ active = `"${active.textContent}", index: ${activeIndex}`;
+ } else if (active.id) {
+ active = `#${active.id}`;
+ } else {
+ active = `<${active.localName}>`;
+ }
+ Assert.ok(
+ expectElement.matches(":focus"),
+ `${name} should have focus (active: ${active}): ${msg}`
+ );
+}
+
+/**
+ * Shift the focus by one step by pressing Tab and assert the new focused
+ * element.
+ *
+ * @param {boolean} forward - Whether to move the focus forward.
+ * @param {object} expect - The expected focused element after pressing tab.
+ * Same as passed to {@link assertFocus}.
+ * @param {string} msg - A message to use for the assertion.
+ */
+function stepFocus(forward, expect, msg) {
+ EventUtils.synthesizeKey("KEY_Tab", { shiftKey: !forward }, win);
+ assertFocus(
+ expect,
+ `After moving ${forward ? "forward" : "backward"}: ${msg}`
+ );
+}
+
+/**
+ * @typedef {object} ItemState
+ * @property {string} text - The text content of the item.
+ * @property {boolean} [selected=false] - Whether the item is selected.
+ * @property {boolean} [focused=false] - Whether the item is focused.
+ */
+
+/**
+ * Assert the text order, selection state and focus of the widget items.
+ *
+ * @param {ItemState[]} expected - The expected state of the widget items, in
+ * the expected order of the items.
+ * @param {string} msg - A message to use for the assertion.
+ */
+function assertState(expected, msg) {
+ let textOrder = [];
+ let focusIndex;
+ let selectedIndices = [];
+ for (let [index, state] of expected.entries()) {
+ textOrder.push(state.text);
+ if (state.selected) {
+ selectedIndices.push(index);
+ }
+ if (state.focused) {
+ if (focusIndex != undefined) {
+ throw new Error("More than one item specified as having focus");
+ }
+ focusIndex = index;
+ }
+ }
+ Assert.deepEqual(
+ Array.from(widget.items, i => i.element.textContent),
+ textOrder,
+ `Text order should match: ${msg}`
+ );
+ assertSelection(selectedIndices, msg);
+ if (focusIndex != undefined) {
+ assertFocus({ index: focusIndex }, msg);
+ } else {
+ Assert.ok(
+ !widget.querySelector(":focus"),
+ `Widget should not contain any focus: ${msg}`
+ );
+ }
+}
+
+/**
+ * Click the empty space of the widget.
+ *
+ * @param {object} mouseEvent - Properties for the click event.
+ */
+function clickWidgetEmptySpace(mouseEvent) {
+ let widgetRect = widget.getBoundingClientRect();
+ if (widget.getAttribute("layout-direction") == "vertical") {
+ // Try click end, which we assume is empty.
+ EventUtils.synthesizeMouse(
+ widget,
+ widgetRect.width / 2,
+ widgetRect.height - 5,
+ mouseEvent,
+ win
+ );
+ } else if (widget.matches(":dir(rtl)")) {
+ // Try click the left, which we assume is empty.
+ EventUtils.synthesizeMouse(
+ widget,
+ 5,
+ widgetRect.height / 2,
+ mouseEvent,
+ win
+ );
+ } else {
+ // Try click the right, which we assume is empty.
+ EventUtils.synthesizeMouse(
+ widget,
+ widgetRect.width - 5,
+ widgetRect.height / 2,
+ mouseEvent,
+ win
+ );
+ }
+}
+
+/**
+ * Click the specified widget item.
+ *
+ * @param {number} index - The index of the item to click.
+ * @param {object} mouseEvent - Properties for the click event.
+ */
+function clickWidgetItem(index, mouseEvent) {
+ EventUtils.synthesizeMouseAtCenter(
+ widget.items[index].element,
+ mouseEvent,
+ win
+ );
+}
+
+/**
+ * Trigger the select-all shortcut.
+ */
+function selectAllShortcut() {
+ EventUtils.synthesizeKey(
+ "a",
+ AppConstants.platform == "macosx" ? { metaKey: true } : { ctrlKey: true },
+ win
+ );
+}
+
+// If the widget is empty, it receives focus on itself.
+add_task(function test_empty_widget_focus() {
+ for (let model of selectionModels) {
+ reset({ model });
+
+ assertFocus({ element: before }, "Initial");
+
+ // Move focus forward.
+ stepFocus(true, { element: widget }, "Move into widget");
+ stepFocus(true, { element: after }, "Move out of widget");
+
+ // Move focus backward.
+ stepFocus(false, { element: widget }, "Move back to widget");
+ stepFocus(false, { element: before }, "Move back out of widget");
+
+ // Clicking also gives focus.
+ for (let shiftKey of [false, true]) {
+ for (let ctrlKey of [false, true]) {
+ info(
+ `Clicking empty widget: ctrlKey: ${ctrlKey}, shiftKey: ${shiftKey}`
+ );
+ clickWidgetEmptySpace({ shiftKey, ctrlKey });
+ assertFocus({ element: widget }, "Widget receives focus after click");
+ // Move focus for the next loop.
+ stepFocus(true, { element: after }, "Move back out");
+ }
+ }
+ }
+});
+
+/**
+ * Test that the initial focus is as expected.
+ *
+ * @param {string} model - The selection model to use.
+ * @param {Function} setup - A callback to set up the widget.
+ * @param {number} clickIndex - The index of an item to click.
+ * @param {object} expect - The expected states.
+ * @param {number} expect.focusIndex - The expected focus index.
+ * @param {number[]} expect.selection - The expected initial selection.
+ * @param {boolean} expect.selectFocus - Whether we expect the focused item to
+ * become selected.
+ */
+function subtest_initial_focus(model, setup, expect) {
+ let { focusIndex: index, selection, selectFocus } = expect;
+
+ reset({ model });
+ setup();
+
+ assertFocus({ element: before }, "Forward start");
+ assertSelection(selection, "Initial selection");
+
+ stepFocus(true, { index }, "Move onto selected item");
+ if (selectFocus) {
+ assertSelection([index], "Focus becomes selected");
+ } else {
+ assertSelection(selection, "Selection remains when focussing");
+ }
+ stepFocus(true, { element: after }, "Move out of widget");
+
+ // Reverse.
+ reset({ model });
+ after.focus();
+ setup();
+
+ assertFocus({ element: after }, "Reverse start");
+ assertSelection(selection, "Reverse start");
+
+ stepFocus(false, { index }, "Move backward to selected item");
+ if (selectFocus) {
+ assertSelection([index], "Focus becomes selected");
+ } else {
+ assertSelection(selection, "Selection remains when focussing");
+ }
+ stepFocus(false, { element: before }, "Move out of widget");
+
+ // With mouse click.
+ for (let shiftKey of [false, true]) {
+ for (let ctrlKey of [false, true]) {
+ info(`Clicking widget: ctrlKey: ${ctrlKey}, shiftKey: ${shiftKey}`);
+
+ reset({ model });
+ setup();
+
+ assertFocus({ element: before }, "Click empty start");
+ assertSelection(selection, "Click empty start");
+ clickWidgetEmptySpace({ ctrlKey, shiftKey });
+ assertFocus(
+ { index },
+ "Selected item becomes focused with click on empty"
+ );
+ if (selectFocus) {
+ assertSelection([index], "Focus becomes selected on click on empty");
+ } else {
+ assertSelection(selection, "Selection remains when click on empty");
+ }
+
+ // With mouse click on item focus moves to the clicked item instead.
+ for (let clickIndex of [
+ (index || widget.items.length) - 1,
+ index,
+ index + 1,
+ ]) {
+ reset({ model });
+ setup();
+
+ assertFocus({ element: before }, "Click first item start");
+ assertSelection(selection, "Click first item start");
+
+ clickWidgetItem(clickIndex, { shiftKey, ctrlKey });
+
+ if (
+ (shiftKey && ctrlKey) ||
+ ((shiftKey || ctrlKey) && (model == "focus" || model == "browse"))
+ ) {
+ // Both modifiers, or multi-selection not supported, so acts the
+ // same as clicking empty.
+ assertFocus(
+ { index },
+ "Selected item becomes focused with click on item"
+ );
+ if (selectFocus) {
+ assertSelection([index], "Focus becomes selected on click on item");
+ } else {
+ assertSelection(selection, "Selection remains when click on item");
+ }
+ } else {
+ assertFocus(
+ { index: clickIndex },
+ "Clicked item becomes focused with click on item"
+ );
+ let clickSelection;
+ if (ctrlKey) {
+ if (selection.includes(clickIndex)) {
+ // Toggle off clicked item.
+ clickSelection = selection.filter(index => index != clickIndex);
+ } else {
+ clickSelection = selection.concat([clickIndex]).sort();
+ }
+ } else if (shiftKey) {
+ // Range selection is always from 0, regardless of the selection
+ // before the click.
+ clickSelection = range(0, clickIndex + 1);
+ } else {
+ clickSelection = [clickIndex];
+ }
+ assertSelection(clickSelection, "Selection after click on item");
+ }
+ }
+ }
+ }
+}
+
+// If the widget has a selection when we move into it, the selected item is
+// focused.
+add_task(function test_initial_focus() {
+ for (let model of selectionModels) {
+ // With no initial selection.
+ subtest_initial_focus(
+ model,
+ () => {
+ widget.addItems(0, ["First", "Second", "Third", "Fourth"]);
+ },
+ { focusIndex: 0, selection: [], selectFocus: true }
+ );
+ // With call to selectSingleItem
+ subtest_initial_focus(
+ model,
+ () => {
+ widget.addItems(0, ["First", "Second", "Third", "Fourth"]);
+ widget.selectSingleItem(2);
+ },
+ { focusIndex: 2, selection: [2], selectFocus: false }
+ );
+
+ // Using the setItemSelected API
+ if (model == "focus" || model == "browse") {
+ continue;
+ }
+
+ subtest_initial_focus(
+ model,
+ () => {
+ widget.addItems(0, ["First", "Second", "Third", "Fourth"]);
+ widget.setItemSelected(2, true);
+ },
+ { focusIndex: 2, selection: [2], selectFocus: false }
+ );
+
+ // With multiple selected, we move focus to the first selected.
+ subtest_initial_focus(
+ model,
+ () => {
+ widget.addItems(0, ["First", "Second", "Third", "Fourth"]);
+ widget.setItemSelected(2, true);
+ widget.setItemSelected(1, true);
+ },
+ { focusIndex: 1, selection: [1, 2], selectFocus: false }
+ );
+
+ // If we use both methods.
+ subtest_initial_focus(
+ model,
+ () => {
+ widget.addItems(0, ["First", "Second", "Third", "Fourth"]);
+ widget.selectSingleItem(2, true);
+ widget.setItemSelected(1, true);
+ },
+ { focusIndex: 1, selection: [1, 2], selectFocus: false }
+ );
+
+ // If we call selectSingleItem and then unselect it, we act same as the
+ // default case.
+ subtest_initial_focus(
+ model,
+ () => {
+ widget.addItems(0, ["First", "Second", "Third", "Fourth"]);
+ widget.selectSingleItem(2, true);
+ widget.setItemSelected(2, false);
+ },
+ { focusIndex: 0, selection: [], selectFocus: true }
+ );
+ }
+});
+
+// If selectSingleItem API method is called, we select an item and make it the
+// focus.
+add_task(function test_select_single_item_method() {
+ function subTestSelectSingleItem(outside, index) {
+ if (outside) {
+ stepFocus(true, { element: after }, "Moving focus to outside widget");
+ }
+
+ widget.selectSingleItem(index);
+ assertSelection([index], "Item becomes selected after call");
+
+ if (outside) {
+ assertFocus({ element: after }, "Focus remains outside the widget");
+ // Return.
+ stepFocus(false, { index }, "Focus moves to selected item on return");
+ assertSelection([index], "Item remains selected on return");
+ } else {
+ assertFocus({ index }, "Focus force moved to selected item");
+ }
+ }
+
+ for (let model of selectionModels) {
+ reset({ model });
+ widget.addItems(0, ["First", "Second", "Third", "Fourth"]);
+
+ stepFocus(true, { index: 0 }, "Move onto first item");
+
+ for (let outside of [false, true]) {
+ info(`Testing selecting item${outside ? " with focus outside" : ""}`);
+
+ EventUtils.synthesizeKey("KEY_Home", {}, win);
+ assertFocus({ index: 0 }, "Focus initially on first item");
+ assertSelection([0], "Initial selection on first item");
+
+ subTestSelectSingleItem(outside, 1);
+ // Selecting again.
+ subTestSelectSingleItem(outside, 1);
+
+ if (model == "focus") {
+ continue;
+ }
+
+ // Split focus from selection
+ EventUtils.synthesizeKey("KEY_ArrowLeft", { ctrlKey: true }, win);
+ assertFocus({ index: 2 }, "Third item has focus");
+ assertSelection([1], "Second item remains selected");
+
+ // Select focused item.
+ subTestSelectSingleItem(outside, 2);
+
+ // Split again.
+ EventUtils.synthesizeKey("KEY_ArrowRight", { ctrlKey: true }, win);
+ assertFocus({ index: 1 }, "Second item has focus");
+ assertSelection([2], "Third item remains selected");
+
+ // Selecting selected item will still move focus.
+ subTestSelectSingleItem(outside, 2);
+
+ // Split again.
+ EventUtils.synthesizeKey("KEY_ArrowRight", { ctrlKey: true }, win);
+ EventUtils.synthesizeKey("KEY_ArrowRight", { ctrlKey: true }, win);
+ assertFocus({ index: 0 }, "First item has focus");
+ assertSelection([2], "Third item remains selected");
+
+ // Select neither focused nor selected.
+ subTestSelectSingleItem(outside, 1);
+ }
+
+ // With mouse click to focus.
+ for (let shiftKey of [false, true]) {
+ for (let ctrlKey of [false, true]) {
+ info(`Clicking widget: ctrlKey: ${ctrlKey}, shiftKey: ${shiftKey}`);
+
+ reset({ model });
+ widget.addItems(0, ["First", "Second", "Third"]);
+ stepFocus(true, { index: 0 }, "Move onto first item");
+ assertSelection([0], "First item becomes selected");
+
+ // Move focus outside widget.
+ stepFocus(true, { element: after }, "Move focus outside");
+
+ // Select an item.
+ widget.selectSingleItem(1);
+
+ // Click empty space will focus the selected item.
+ clickWidgetEmptySpace({});
+ assertFocus(
+ { index: 1 },
+ "Selected item becomes focused with click on empty"
+ );
+ assertSelection(
+ [1],
+ "Second item remains selected with click on empty"
+ );
+
+ // With mouse click on selected item.
+ stepFocus(false, { element: before }, "Move focus outside");
+ widget.selectSingleItem(2);
+
+ clickWidgetItem(2, { shiftKey, ctrlKey });
+ assertFocus(
+ { index: 2 },
+ "Selected item becomes focused with click on selected"
+ );
+ if (ctrlKey && !shiftKey && model == "browse-multi") {
+ assertSelection(
+ [],
+ "Item becomes unselected with Ctrl+click on selected"
+ );
+ } else {
+ // NOTE: Shift+Click will select from the item to itself.
+ assertSelection(
+ [2],
+ "Selected item remains selected with click on selected"
+ );
+ }
+
+ // With mouse click on non-selected item.
+ stepFocus(false, { element: before }, "Move focus outside");
+ widget.selectSingleItem(1);
+
+ clickWidgetItem(2, { shiftKey, ctrlKey });
+ if (
+ (shiftKey && ctrlKey) ||
+ ((shiftKey || ctrlKey) && (model == "focus" || model == "browse"))
+ ) {
+ // Both modifiers, or multi-selection not supported, so acts the
+ // same as clicking empty.
+ assertFocus(
+ { index: 1 },
+ "Selected item becomes focused with click on item"
+ );
+ assertSelection(
+ [1],
+ "Selected item remains selected with click on item"
+ );
+ } else {
+ assertFocus(
+ { index: 2 },
+ "Third item becomes focused with click on item"
+ );
+ if (ctrlKey) {
+ assertSelection(
+ [1, 2],
+ "Third item becomes selected with Ctrl+click"
+ );
+ } else if (shiftKey) {
+ assertSelection(
+ [1, 2],
+ "Second to third item become selected with Shift+click"
+ );
+ } else {
+ assertSelection(
+ [2],
+ "Third item becomes selected with click on item"
+ );
+ }
+ }
+ }
+ }
+ }
+});
+
+// If setItemSelected API method is called, we set the selection state of an
+// item but do not change anything else.
+add_task(function test_set_item_selected_method() {
+ for (let model of selectionModels) {
+ reset({ model });
+ widget.addItems(0, ["First", "Second", "Third", "Fourth", "Fifth"]);
+ stepFocus(true, { index: 0 }, "Initial focus on first item");
+ assertSelection([0], "Initial selection on first item");
+
+ if (model == "focus" || model == "browse") {
+ // This method always throws.
+ Assert.throws(
+ () => widget.setItemSelected(2, true),
+ /Widget does not support multi-selection/
+ );
+ // Even if it would not change the single selection state.
+ Assert.throws(
+ () => widget.setItemSelected(2, false),
+ /Widget does not support multi-selection/
+ );
+ Assert.throws(
+ () => widget.setItemSelected(0, true),
+ /Widget does not support multi-selection/
+ );
+ continue;
+ }
+
+ // Can select.
+ widget.setItemSelected(2, true);
+ assertFocus({ index: 0 }, "Same focus");
+ assertSelection([0, 2], "Item 2 becomes selected");
+
+ // And unselect.
+ widget.setItemSelected(0, false);
+ assertFocus({ index: 0 }, "Same focus");
+ assertSelection([2], "Item 0 is unselected");
+
+ // Does nothing extra if already selected/unselected.
+ widget.setItemSelected(2, true);
+ assertFocus({ index: 0 }, "Same focus");
+ assertSelection([2], "Same selected");
+
+ widget.setItemSelected(0, false);
+ assertFocus({ index: 0 }, "Same focus");
+ assertSelection([2], "Same selected");
+
+ EventUtils.synthesizeKey("KEY_ArrowLeft", { ctrlKey: true }, win);
+ EventUtils.synthesizeKey("KEY_ArrowLeft", { ctrlKey: true }, win);
+ EventUtils.synthesizeKey("KEY_ArrowLeft", { ctrlKey: true }, win);
+
+ // Select the focused item.
+ assertFocus({ index: 3 }, "Focus on item 3");
+ assertSelection([2], "Same selected");
+
+ widget.setItemSelected(3, true);
+ assertFocus({ index: 3 }, "Same focus");
+ assertSelection([2, 3], "Item 3 selected");
+
+ widget.setItemSelected(2, false);
+ assertFocus({ index: 3 }, "Same focus");
+ assertSelection([3], "Item 2 unselected");
+
+ // Can select none this way.
+ widget.setItemSelected(3, false);
+ assertFocus({ index: 3 }, "Same focus");
+ assertSelection([], "None selected");
+ }
+});
+
+/**
+ * Test navigation for the given direction.
+ *
+ * @param {string} model - The selection model to use.
+ * @param {string} direction - The layout direction of the widget.
+ * @param {object} keys - Navigation keys.
+ * @param {string} keys.forward - The key to move forward.
+ * @param {string} keys.backward - The key to move backward.
+ */
+function subtest_keyboard_navigation(model, direction, keys) {
+ let { forward: forwardKey, backward: backwardKey } = keys;
+ reset({ model, direction });
+ widget.addItems(0, ["First", "Second", "Third"]);
+
+ stepFocus(true, { index: 0 }, "Initially on first item");
+
+ // Without Ctrl, selection follows focus.
+
+ // Forward.
+ EventUtils.synthesizeKey(forwardKey, {}, win);
+ assertFocus({ index: 1 }, "Forward to second item");
+ assertSelection([1], "Second item becomes selected on focus");
+ EventUtils.synthesizeKey(forwardKey, {}, win);
+ assertFocus({ index: 2 }, "Forward to third item");
+ assertSelection([2], "Third item becomes selected on focus");
+ EventUtils.synthesizeKey(forwardKey, {}, win);
+ assertFocus({ index: 2 }, "Forward at end remains on third item");
+ assertSelection([2], "Third item remains selected");
+
+ // Backward.
+ EventUtils.synthesizeKey(backwardKey, {}, win);
+ assertFocus({ index: 1 }, "Backward to second item");
+ assertSelection([1], "Second item becomes selected on focus");
+ EventUtils.synthesizeKey(backwardKey, {}, win);
+ assertFocus({ index: 0 }, "Backward to first item");
+ assertSelection([0], "First item becomes selected on focus");
+ EventUtils.synthesizeKey(backwardKey, {}, win);
+ assertFocus({ index: 0 }, "Backward at end remains on first item");
+ assertSelection([0], "First item remains selected");
+
+ // End.
+ EventUtils.synthesizeKey("KEY_End", {}, win);
+ assertFocus({ index: 2 }, "Third becomes focused on End");
+ assertSelection([2], "Third becomes selected on End");
+ // Move to middle.
+ EventUtils.synthesizeKey(backwardKey, {}, win);
+ EventUtils.synthesizeKey("KEY_End", {}, win);
+ assertFocus({ index: 2 }, "Third becomes focused on End from second");
+ assertSelection([2], "Third becomes selected on End from second");
+ EventUtils.synthesizeKey("KEY_End", {}, win);
+ assertFocus({ index: 2 }, "Third remains focused on End from third");
+ assertSelection([2], "Third becomes selected on End from third");
+
+ // Home.
+ EventUtils.synthesizeKey("KEY_Home", {}, win);
+ assertFocus({ index: 0 }, "First becomes focused on Home");
+ assertSelection([0], "First becomes selected on Home");
+ // Move to middle.
+ EventUtils.synthesizeKey(forwardKey, {}, win);
+ EventUtils.synthesizeKey("KEY_Home", {}, win);
+ assertFocus({ index: 0 }, "First becomes focused on Home from second");
+ assertSelection([0], "First becomes selected on Home from second");
+ EventUtils.synthesizeKey("KEY_Home", {}, win);
+ assertFocus({ index: 0 }, "First remains focused on Home from first");
+ assertSelection([0], "First becomes selected on Home from first");
+
+ // With Ctrl key, selection does not follow focus.
+ if (model == "focus") {
+ // Disabled in "focus" model.
+ // Move to middle item.
+ EventUtils.synthesizeKey(forwardKey, {}, win);
+ assertFocus({ index: 1 }, "Second item is focused");
+ assertFocus({ index: 1 }, "Second item is selected");
+
+ for (let key of [backwardKey, forwardKey, "KEY_Home", "KEY_End"]) {
+ for (let shiftKey of [false, true]) {
+ info(
+ `Pressing Ctrl+${
+ shiftKey ? "Shift+" : ""
+ }${key} on "focus" model widget`
+ );
+ EventUtils.synthesizeKey(key, { ctrlKey: true, shiftKey }, win);
+ assertFocus({ index: 1 }, "Second item is still focused");
+ assertSelection([1], "Second item is still selected");
+ }
+ }
+ } else {
+ EventUtils.synthesizeKey(forwardKey, { ctrlKey: true }, win);
+ assertFocus({ index: 1 }, "Ctrl+Forward to second item");
+ assertSelection([0], "First item remains selected on Ctrl+Forward");
+
+ EventUtils.synthesizeKey(forwardKey, { ctrlKey: true }, win);
+ assertFocus({ index: 2 }, "Ctrl+Forward to third item");
+ assertSelection([0], "First item remains selected on Ctrl+Forward");
+
+ EventUtils.synthesizeKey(backwardKey, { ctrlKey: true }, win);
+ assertFocus({ index: 1 }, "Ctrl+Backward to second item");
+ assertSelection([0], "First item remains selected on Ctrl+Backward");
+
+ EventUtils.synthesizeKey(backwardKey, { ctrlKey: true }, win);
+ assertFocus({ index: 0 }, "Ctrl+Backward to first item");
+ assertSelection([0], "First item remains selected on Ctrl+Backward");
+
+ EventUtils.synthesizeKey("KEY_End", { ctrlKey: true }, win);
+ assertFocus({ index: 2 }, "Ctrl+End to third item");
+ assertSelection([0], "First item remains selected on Ctrl+End");
+
+ EventUtils.synthesizeKey(backwardKey, {}, win);
+ assertFocus({ index: 1 }, "Backward to second item");
+ assertSelection([1], "Selection moves with focus when not pressing Ctrl");
+
+ EventUtils.synthesizeKey("KEY_Home", { ctrlKey: true }, win);
+ assertFocus({ index: 0 }, "Ctrl+Home to first item");
+ assertSelection([1], "Second item remains selected on Ctrl+Home");
+
+ // Does nothing if combined with Shift.
+ for (let key of [backwardKey, forwardKey, "KEY_Home", "KEY_End"]) {
+ info(`Pressing Ctrl+Shift+${key} on "${model}" model widget`);
+ EventUtils.synthesizeKey(key, { ctrlKey: true, shiftKey: true }, win);
+ assertFocus({ index: 0 }, "First item is still focused");
+ assertSelection([1], "Second item is still selected");
+ }
+
+ // Even if focus remains the same, the selection is still updated if we
+ // don't press Ctrl.
+ EventUtils.synthesizeKey(backwardKey, {}, win);
+ assertFocus({ index: 0 }, "Focus remains on first item");
+ assertSelection(
+ [0],
+ "Selection moves to the first item since Ctrl was not pressed"
+ );
+ }
+}
+
+// Navigating with keyboard will move focus, and possibly selection.
+add_task(function test_keyboard_navigation() {
+ for (let model of selectionModels) {
+ subtest_keyboard_navigation(model, "top-to-bottom", {
+ forward: "KEY_ArrowDown",
+ backward: "KEY_ArrowUp",
+ });
+ subtest_keyboard_navigation(model, "right-to-left", {
+ forward: "KEY_ArrowLeft",
+ backward: "KEY_ArrowRight",
+ });
+ subtest_keyboard_navigation(model, "left-to-right", {
+ forward: "KEY_ArrowRight",
+ backward: "KEY_ArrowLeft",
+ });
+ }
+});
+
+/**
+ * A method to scroll the widget.
+ *
+ * @callback ScrollMethod
+ * @param {number} pos - The position/offset to scroll to.
+ */
+/**
+ * The position of an element, relative to the layout of the widget.
+ *
+ * @typedef {object} StartEndPositions
+ * @property {number} start - The starting position of the element in the
+ * direction of the widget's layout. The value should be a pixel offset
+ * from some fixed point, such that a higher value indicates an element
+ * further from the start of the widget.
+ * @property {number} end - The ending position of the element in the
+ * direction of the widget's layout. This should use the same fixed point
+ * as the start.
+ * @property {number} xStart - An X position in the client coordinates that
+ * points to the inside of the element, close to the starting corner. I.e.
+ * the block-start and inline-start.
+ * @property {number} yStart - A Y position in the client coordinates that
+ * points to the inside of the element, close to the starting corner.
+ * @property {number} xEnd - An X position in the client coordinates that
+ * points to the inside of the element, close to the ending corner. I.e.
+ * the block-end and inline-end.
+ * @property {number} yEnd - A Y position in the client coordinates that
+ * points to the inside of the element, close to the ending corner.
+ */
+/**
+ * A method to return the starting and ending positions of the bounding
+ * client rectangle of an element.
+ *
+ * @callback GetStartEndMethod
+ * @param {DOMRect} rect - The rectangle to get the positions of.
+ * @returns {object} positions
+/**
+ * Test page navigation for the given direction.
+ *
+ * @param {string} model - The selection model to use on the widget.
+ * @param {string} direction - The direction of the widget layout.
+ * @param {object} details - Details about the direction.
+ * @param {string} details.sizeName - The CSS style name that controls the
+ * widget size in the direction of widget layout.
+ * @param {string} details.forwardKey - The key to press to move forward one
+ * item.
+ * @param {string} details.backwardKey - The key to press to move backward
+ * one item.
+ * @param {ScrollMethod} details.scrollTo - A method to call to scroll the
+ * widget.
+ * @param {GetStartEndMethod} details.getStartEnd - A method to get the
+ * positioning of an element.
+ */
+function subtest_page_navigation(model, direction, details) {
+ let { sizeName, forwardKey, backwardKey, scrollTo, getStartEnd } = details;
+ function getStartEndBoundary(element) {
+ return getStartEnd(element.getBoundingClientRect());
+ }
+ function assertInView(expect, msg) {
+ let { first, firstClipped, last, lastClipped } = expect;
+ if (!firstClipped) {
+ firstClipped = 0;
+ }
+ if (!lastClipped) {
+ lastClipped = 0;
+ }
+ let { start: viewStart, end: viewEnd } = getStartEndBoundary(widget);
+ // The widget has a 1px border that should not contribute to the view
+ // size.
+ viewStart += 1;
+ viewEnd -= 1;
+ let firstStart = getStartEndBoundary(
+ widget.items[expect.first].element
+ ).start;
+ Assert.equal(
+ firstStart,
+ viewStart - firstClipped,
+ `Item ${first} should be at the start of the view (${viewStart}) clipped by ${firstClipped}: ${msg}`
+ );
+ if (expect.first > 0) {
+ Assert.lessOrEqual(
+ getStartEndBoundary(widget.items[expect.first - 1].element).end,
+ viewStart,
+ `Item ${expect.first - 1} should be out of view: ${msg}`
+ );
+ }
+ let lastEnd = getStartEndBoundary(widget.items[expect.last].element).end;
+ Assert.equal(
+ lastEnd,
+ viewEnd + lastClipped,
+ `Item ${last} should be at the end of the view (${viewEnd}) clipped by ${lastClipped}: ${msg}`
+ );
+ if (expect.last < widget.items.length - 1) {
+ Assert.greaterOrEqual(
+ getStartEndBoundary(widget.items[expect.last + 1].element).start,
+ viewEnd,
+ `Item ${expect.last + 1} should be out of view: ${msg}`
+ );
+ }
+ }
+ reset({ model, direction });
+ widget.addItems(
+ 0,
+ range(0, 70).map(i => `add-${i}`)
+ );
+ let { start: itemStart, end: itemEnd } = getStartEndBoundary(
+ widget.items[0].element
+ );
+ Assert.equal(itemEnd - itemStart, 30, "Expected item size");
+
+ assertInView({ first: 0, last: 19 }, "First 20 items in view");
+ stepFocus(true, { index: 0 }, "Move into widget");
+ assertSelection([0], "Fist item selected");
+ assertInView({ first: 0, last: 19 }, "First 20 items still in view");
+
+ // PageDown goes to the end of the current page.
+ EventUtils.synthesizeKey("KEY_PageDown", {}, win);
+ assertInView({ first: 0, last: 19 }, "First 20 item still in view");
+ assertFocus({ index: 19 }, "Focus moves to end of the page");
+ assertSelection([19], "Selection at end of the page");
+
+ // Pressing forward key will scroll the next item into view.
+ EventUtils.synthesizeKey(forwardKey, {}, win);
+ assertInView({ first: 1, last: 20 }, "Items 1 to 20 in view");
+ assertFocus({ index: 20 }, "Focus at end of the page");
+ assertSelection([20], "Selection at end of the page");
+
+ // Pressing backward will not change the view.
+ EventUtils.synthesizeKey(backwardKey, {}, win);
+ assertInView({ first: 1, last: 20 }, "Items 1 to 20 still in view");
+ assertFocus({ index: 19 }, "Focus moves up to 19");
+ assertSelection([19], "Selection moves up to 19");
+
+ // PageDown goes to the end of the current page.
+ EventUtils.synthesizeKey("KEY_PageDown", {}, win);
+ assertInView({ first: 1, last: 20 }, "Items 1 to 20 still in view");
+ assertFocus({ index: 20 }, "Focus moves to end of page");
+ assertSelection([20], "Selection moves to end of page");
+
+ // PageDown when already at the end of the page will move to the next
+ // page.
+ // The last index from the previous page (20) should still be visible at
+ // the top.
+ EventUtils.synthesizeKey("KEY_PageDown", {}, win);
+ assertInView({ first: 20, last: 39 }, "Items 20 to 39 in view");
+ assertFocus({ index: 39 }, "Focus moves to end of new page");
+ assertSelection([39], "Selection moves to end of new page");
+
+ // Another PageDown will do the same.
+ EventUtils.synthesizeKey("KEY_PageDown", {}, win);
+ assertInView({ first: 39, last: 58 }, "Items 39 to 58 in view");
+ assertFocus({ index: 58 }, "Focus moves to end of new page");
+ assertSelection([58], "Selection moves to end of new page");
+
+ // Last PageDown will take us to the end.
+ EventUtils.synthesizeKey("KEY_PageDown", {}, win);
+ assertInView({ first: 50, last: 69 }, "Last 20 items in view");
+ assertFocus({ index: 69 }, "Focus moves to end");
+ assertSelection([69], "Selection moves to end");
+
+ // Same thing in reverse with PageUp.
+ // PageUp goes to the start of the current page.
+ EventUtils.synthesizeKey("KEY_PageUp", {}, win);
+ assertInView({ first: 50, last: 69 }, "Last 20 item still in view");
+ assertFocus({ index: 50 }, "Focus moves to start of the page");
+ assertSelection([50], "Selection at end of the page");
+
+ // Pressing backward will scroll the previous item into view.
+ EventUtils.synthesizeKey(backwardKey, {}, win);
+ assertInView({ first: 49, last: 68 }, "Items 49 to 68 in view");
+ assertFocus({ index: 49 }, "Focus at start of the page");
+ assertSelection([49], "Selection at start of the page");
+
+ // Pressing forward will not change the view.
+ EventUtils.synthesizeKey(forwardKey, {}, win);
+ assertInView({ first: 49, last: 68 }, "Items 49 to 68 still in view");
+ assertFocus({ index: 50 }, "Focus moves up to 50");
+ assertSelection([50], "Selection moves up to 50");
+
+ // PageUp goes to the start of the current page.
+ EventUtils.synthesizeKey("KEY_PageUp", {}, win);
+ assertInView({ first: 49, last: 68 }, "Items 49 to 68 still in view");
+ assertFocus({ index: 49 }, "Focus moves to start of page");
+ assertSelection([49], "Selection moves to start of page");
+
+ // PageUp when already at the start of the page will move one page up.
+ // The first index from the previously shown page (49) should still be
+ // visible at the bottom.
+ EventUtils.synthesizeKey("KEY_PageUp", {}, win);
+ assertInView({ first: 30, last: 49 }, "Items 30 to 49 in view");
+ assertFocus({ index: 30 }, "Focus moves to start of new page");
+ assertSelection([30], "Selection moves to start of new page");
+
+ // Another PageUp will do the same.
+ EventUtils.synthesizeKey("KEY_PageUp", {}, win);
+ assertInView({ first: 11, last: 30 }, "Items 11 to 30 in view");
+ assertFocus({ index: 11 }, "Focus moves to start of new page");
+ assertSelection([11], "Selection moves to start of new page");
+
+ // Last PageUp will take us to the start.
+ EventUtils.synthesizeKey("KEY_PageUp", {}, win);
+ assertInView({ first: 0, last: 19 }, "Items 0 to 19 in view");
+ assertFocus({ index: 0 }, "Focus moves to start");
+ assertSelection([0], "Selection moves to start");
+
+ // PageDown with focus above the view. Focus should move to the end of the
+ // visible page.
+ scrollTo(120);
+ assertInView({ first: 4, last: 23 }, "Items 4 to 23 in view");
+ assertFocus({ index: 0 }, "Focus remains above the view");
+ assertSelection([0], "Selection remains above the view");
+
+ EventUtils.synthesizeKey("KEY_PageDown", {}, win);
+ assertInView({ first: 4, last: 23 }, "Same items in view");
+ assertFocus({ index: 23 }, "Focus moves to the end of the visible page");
+ assertSelection([23], "Selection moves to the end of the visible page");
+
+ // PageDown with focus below the view. Focus should shift by one page,
+ // with the previous focus at the top of the page.
+ scrollTo(60);
+ assertInView({ first: 2, last: 21 }, "Items 2 to 21 in view");
+ assertFocus({ index: 23 }, "Focus remains below the view");
+ assertSelection([23], "Selection remains below the view");
+
+ EventUtils.synthesizeKey("KEY_PageDown", {}, win);
+ assertInView(
+ { first: 23, last: 42 },
+ "View shifts by a page relative to focus"
+ );
+ assertFocus({ index: 42 }, "Focus moves to end of new page");
+ assertSelection([42], "Selection moves to end of new page");
+
+ // PageUp with focus below the view. Focus should move to the start of the
+ // visible page.
+ scrollTo(630);
+ assertInView({ first: 21, last: 40 }, "Items 21 to 40 in view");
+ assertFocus({ index: 42 }, "Focus remains below the view");
+ assertSelection([42], "Selection remains below the view");
+
+ EventUtils.synthesizeKey("KEY_PageUp", {}, win);
+ assertInView({ first: 21, last: 40 }, "Same items in view");
+ assertFocus({ index: 21 }, "Focus moves to the start of the visible page");
+ assertSelection([21], "Selection moves to the start of the visible page");
+
+ // PageUp with focus above the view. Focus should shift by one page, with
+ // the previous focus at the bottom of the page.
+ scrollTo(750);
+ assertInView({ first: 25, last: 44 }, "Items 25 to 44 in view");
+ assertFocus({ index: 21 }, "Focus remains above the view");
+ assertSelection([21], "Selection remains above the view");
+
+ EventUtils.synthesizeKey("KEY_PageUp", {}, win);
+ assertInView(
+ { first: 2, last: 21 },
+ "View shifts by a page relative to focus"
+ );
+ assertFocus({ index: 2 }, "Focus moves to start of new page");
+ assertSelection([2], "Selection moves to start of new page");
+
+ // Test when view does not exactly fit items.
+ for (let sizeDiff of [0, 10, 15, 20]) {
+ info(`Reducing widget size by ${sizeDiff}px`);
+ widget.style[sizeName] = `${600 - sizeDiff}px`;
+
+ // When we reduce the size of the view by half an item or more, we
+ // reduce the page size from 20 to 19.
+ // NOTE: At each sizeDiff still fits strictly more than 19 items in its
+ // view.
+ let pageSize = sizeDiff < 15 ? 20 : 19;
+
+ // Make sure that Home and End keys scroll the view and clip the items
+ // as expected.
+ EventUtils.synthesizeKey("KEY_Home", {}, win);
+ assertInView(
+ { first: 0, last: 19, lastClipped: sizeDiff },
+ `Start of view with last item clipped by ${sizeDiff}px`
+ );
+ assertFocus({ index: 0 }, "First item has focus");
+ assertSelection([0], "First item is selected");
+
+ EventUtils.synthesizeKey("KEY_End", {}, win);
+ assertInView(
+ { first: 50, firstClipped: sizeDiff, last: 69 },
+ `End of view with first item clipped by ${sizeDiff}px`
+ );
+ assertFocus({ index: 69 }, "Last item has focus");
+ assertSelection([69], "Last item is selected");
+
+ for (let lastClipped of [0, 10, 15, 20]) {
+ info(`Testing PageDown with last item clipped by ${lastClipped}px`);
+ // Across all sizeDiff and lastClipped values we still want the last
+ // item to be index 21 clipped by lastClipped.
+ // E.g. when sizeDiff is 10 and lastClipped is 10, then the scroll
+ // will be 60px and the first item will be index 2 with no clipping.
+ // But when the sizeDiff is 10 and the lastClipped is 20, then the
+ // scroll will be 50px and the first item will be index 1 with 20px
+ // clipping.
+ let scroll = 60 + sizeDiff - lastClipped;
+ scrollTo(scroll);
+ let first = Math.floor(scroll / 30);
+ let firstClipped = scroll % 30;
+ clickWidgetItem(3, {});
+ assertInView(
+ { first, firstClipped, last: 21, lastClipped },
+ `Last item 21 in view clipped by ${lastClipped}px`
+ );
+ assertFocus({ index: 3 }, "Focus on item 3");
+ assertSelection([3], "Selection on item 3");
+
+ EventUtils.synthesizeKey("KEY_PageDown", {}, win);
+ let pageEnd;
+ if (lastClipped < 15) {
+ // The last item is more than half in view, so counts as part of the
+ // page.
+ // NOTE: Index of the first item is always "2", even if it was "1"
+ // before the scroll, because the view fits (19, 20] items.
+ assertInView(
+ { first: 2, firstClipped: sizeDiff, last: 21 },
+ "Scrolls down to fully include the last item 21"
+ );
+ pageEnd = 21;
+ } else {
+ // The last item is half or less in view, so only the one before it
+ // counts as being part of the page.
+ assertInView(
+ { first, firstClipped, last: 21, lastClipped },
+ "Same view"
+ );
+ pageEnd = 20;
+ }
+ assertFocus({ index: pageEnd }, "Focus moves to pageEnd");
+ assertSelection([pageEnd], "Selection moves to pageEnd");
+
+ // Reset scroll to test scrolling when the focus is already at the
+ // pageEnd.
+ scrollTo(scroll);
+ assertInView(
+ { first, firstClipped, last: 21, lastClipped },
+ `Last item 21 in view clipped by ${lastClipped}px`
+ );
+
+ // PageDown again will move by a page. The new end of the page will be
+ // scrolled just into view at the bottom.
+ EventUtils.synthesizeKey("KEY_PageDown", {}, win);
+ let newPageEnd = pageEnd + pageSize - 1;
+ // NOTE: If the previous pageEnd would fit mostly in view, then we
+ // expect the first item in the view to be this item. Otherwise, we
+ // expect it to be the one before, which will ensure the previous
+ // pageEnd is fully visible.
+ firstClipped = sizeDiff;
+ first = sizeDiff < 15 ? pageEnd : pageEnd - 1;
+ assertInView(
+ { first, firstClipped, last: newPageEnd },
+ "New page end scrolled into view, previous page end mostly visible"
+ );
+ assertFocus({ index: newPageEnd }, "Focus moves to end of new page");
+ assertSelection([newPageEnd], "Selection moves to end of new page");
+
+ // PageUp reverses the focus.
+ // We don't test the the view since that is handled lower down.
+ EventUtils.synthesizeKey("KEY_PageUp", {}, win);
+ assertFocus({ index: pageEnd }, "Focus returns to pageEnd");
+ assertSelection([pageEnd], "Selection returns to pageEnd");
+ }
+
+ for (let firstClipped of [0, 10, 15, 20]) {
+ // Across all sizeDiff and firstClipped values we still want the first
+ // item to be index 24 clipped by firstClipped.
+ // E.g. when sizeDiff is 10 and firstClipped is 10, then the scroll
+ // will be 730px and the last item will be index 44 with no clipping.
+ // But when the sizeDiff is 10 and the firstClipped is 0, then the
+ // scroll will be 720px and the last item will be index 43 with 10px
+ // clipping.
+ info(`Testing PageUp with first item clipped by ${firstClipped}px`);
+ scrollTo(720 + firstClipped);
+ let viewEnd = 720 + firstClipped + 600 - sizeDiff;
+ let last = Math.floor(viewEnd / 30);
+ let lastClipped = 30 - (viewEnd % 30);
+ clickWidgetItem(42, {});
+ assertInView(
+ { first: 24, firstClipped, last, lastClipped },
+ `First item 24 in view clipped by ${firstClipped}px`
+ );
+ assertFocus({ index: 42 }, "Focus on item 42");
+ assertSelection([42], "Selection on item 42");
+
+ EventUtils.synthesizeKey("KEY_PageUp", {}, win);
+ let pageStart;
+ if (firstClipped < 15) {
+ // The first item is more than half in view, so counts as part of
+ // the page.
+ // NOTE: Index of the last item is always "43", even if it was "44"
+ // before the scroll, because the view fits (19, 20] items.
+ assertInView(
+ { first: 24, last: 43, lastClipped: sizeDiff },
+ "Scrolls up to fully include the first item 24"
+ );
+ pageStart = 24;
+ } else {
+ // The first item is half or less in view, so only the one after it
+ // counts as being part of the page.
+ assertInView(
+ { first: 24, firstClipped, last, lastClipped },
+ "Same view"
+ );
+ pageStart = 25;
+ }
+ assertFocus({ index: pageStart }, "Focus moves to pageStart");
+ assertSelection([pageStart], "Selection moves to pageStart");
+
+ // Reset scroll.
+ scrollTo(720 + firstClipped);
+ assertInView(
+ { first: 24, firstClipped, last, lastClipped },
+ `First item 24 in view clipped by ${firstClipped}px`
+ );
+
+ // PageUp again will move by a page. The new start of the page will be
+ // scrolled just into view at the top.
+ EventUtils.synthesizeKey("KEY_PageUp", {}, win);
+ let newPageStart = pageStart - pageSize + 1;
+ // NOTE: If the previous pageStart would fit mostly in view, then we
+ // expect the last item in the view to be this item. Otherwise, we
+ // expect it to be the one after, which will ensure the previous
+ // pageStart is fully visible.
+ lastClipped = sizeDiff;
+ last = sizeDiff < 15 ? pageStart : pageStart + 1;
+ assertInView(
+ { first: newPageStart, last, lastClipped },
+ "New page end scrolled into view, previous page end mostly visible"
+ );
+ assertFocus({ index: newPageStart }, "Focus moves to start of new page");
+ assertSelection([newPageStart], "Selection moves to start of new page");
+
+ // PageDown reverses the focus.
+ // We don't test the the view since that is handled further up.
+ EventUtils.synthesizeKey("KEY_PageDown", {}, win);
+ assertFocus({ index: pageStart }, "Focus returns to pageStart");
+ assertSelection([pageStart], "Selection returns to pageStart");
+ }
+ }
+
+ // When widget only fits 1 visible item or less.
+ for (let size of [10, 20, 30, 45, 50]) {
+ info(`Resizing widget to ${size}px`);
+ widget.style[sizeName] = `${size}px`;
+
+ scrollTo(600);
+ // When the view size is less than the size of an item, we cannot always
+ // click the center of the item, so we need to click the start instead.
+ let { xStart, yStart } = getStartEndBoundary(widget.items[20].element);
+ EventUtils.synthesizeMouseAtPoint(xStart, yStart, {}, win);
+ let last = size > 30 ? 21 : 20;
+ let lastClipped = size > 30 ? 60 - size : 30 - size;
+ assertInView({ first: 20, last, lastClipped }, "Small number of items");
+ assertFocus({ index: 20 }, "Focus on item 20");
+ assertSelection([20], "Item 20 selected");
+
+ EventUtils.synthesizeKey("KEY_PageDown", {}, win);
+ if (size <= 45) {
+ // Only 1 or 0 items fit on the page, so does nothing.
+ assertInView({ first: 20, last, lastClipped }, "Same view");
+ assertFocus({ index: 20 }, "Same focus");
+ assertSelection([20], "Same selected");
+ } else {
+ // 2 items fit visibly on the page, so acts as normal.
+ assertInView(
+ { first: 20, firstClipped: lastClipped, last: 21 },
+ "Last item scrolled into view"
+ );
+ assertFocus({ index: 21 }, "Focus increases by one");
+ assertSelection([21], "Selected moves to focus");
+ }
+
+ scrollTo(660 - size);
+ let { xEnd, yEnd } = getStartEndBoundary(widget.items[21].element);
+ EventUtils.synthesizeMouseAtPoint(xEnd, yEnd, {}, win);
+ let first = size > 30 ? 20 : 21;
+ let firstClipped = size > 30 ? 60 - size : 30 - size;
+ assertInView({ first, firstClipped, last: 21 }, "Small number of items");
+ assertFocus({ index: 21 }, "Focus on item 21");
+ assertSelection([21], "Item 21 selected");
+
+ EventUtils.synthesizeKey("KEY_PageUp", {}, win);
+ if (size <= 45) {
+ // Only 1 or 0 items fit on the page, so does nothing.
+ assertInView({ first, firstClipped, last: 21 }, "Same view");
+ assertFocus({ index: 21 }, "Same focus");
+ assertSelection([21], "Same selected");
+ } else {
+ // 2 items fit visibly on the page, so acts as normal.
+ assertInView(
+ { first: 20, last: 21, lastClipped: firstClipped },
+ "First item scrolled into view"
+ );
+ assertFocus({ index: 20 }, "Focus decreases by one");
+ assertSelection([20], "Selected moves to focus");
+ }
+ }
+ widget.style[sizeName] = null;
+
+ // Disable page navigation.
+ // This would be used when the item sizes or the page layout do not allow
+ // for page navigation, or if PageUp and PageDown should be used for something
+ // else.
+ widget.toggleAttribute("no-pages", true);
+
+ let gotKeys = [];
+ let keydownListener = event => {
+ gotKeys.push(event.key);
+ };
+ win.document.body.addEventListener("keydown", keydownListener);
+ scrollTo(600);
+ clickWidgetItem(20, {});
+ assertInView({ first: 20, last: 39 }, "Items 20 to 39 in view");
+ assertFocus({ index: 20 }, "First item focused");
+ assertSelection([20], "First item selected");
+
+ EventUtils.synthesizeKey("KEY_PageUp", {}, win);
+ assertInView({ first: 20, last: 39 }, "Same view");
+ assertFocus({ index: 20 }, "Same focus");
+ assertSelection([20], "Same selected");
+ Assert.deepEqual(gotKeys, ["PageUp"], "PageUp reaches document body");
+ gotKeys = [];
+ EventUtils.synthesizeKey("KEY_PageDown", {}, win);
+ assertInView({ first: 20, last: 39 }, "Same view");
+ assertFocus({ index: 20 }, "Same focus");
+ assertSelection([20], "Same selected");
+ Assert.deepEqual(gotKeys, ["PageDown"], "PageDown reaches document body");
+ gotKeys = [];
+
+ clickWidgetItem(39, {});
+ assertInView({ first: 20, last: 39 }, "Items 20 to 39 in view");
+ assertFocus({ index: 39 }, "Last item focused");
+ assertSelection([39], "Last item selected");
+
+ EventUtils.synthesizeKey("KEY_PageUp", {}, win);
+ assertInView({ first: 20, last: 39 }, "Same view");
+ assertFocus({ index: 39 }, "Same focus");
+ assertSelection([39], "Same selected");
+ Assert.deepEqual(gotKeys, ["PageUp"], "PageUp reaches document body");
+ gotKeys = [];
+ EventUtils.synthesizeKey("KEY_PageDown", {}, win);
+ assertInView({ first: 20, last: 39 }, "Same view");
+ assertFocus({ index: 39 }, "Same focus");
+ assertSelection([39], "Same selected");
+ Assert.deepEqual(gotKeys, ["PageDown"], "PageDown reaches document body");
+ gotKeys = [];
+
+ widget.removeAttribute("no-pages");
+
+ // With page navigation enabled key-presses do not reach the document body.
+ EventUtils.synthesizeKey("KEY_PageUp", {}, win);
+ Assert.deepEqual(gotKeys, [], "No key reaches document body");
+ EventUtils.synthesizeKey("KEY_PageDown", {}, win);
+ Assert.deepEqual(gotKeys, [], "No key reaches document body");
+
+ win.document.body.removeEventListener("keydown", keydownListener);
+
+ // Test with modifiers.
+ for (let { shiftKey, ctrlKey } of [
+ { shiftKey: true, ctrlKey: true },
+ { shiftKey: false, ctrlKey: true },
+ { shiftKey: true, ctrlKey: false },
+ ]) {
+ info(
+ `Pressing ${ctrlKey ? "Ctrl+" : ""}${shiftKey ? "Shift+" : ""}PageUp/Down`
+ );
+ EventUtils.synthesizeKey("KEY_Home", {}, win);
+ EventUtils.synthesizeKey(forwardKey, {}, win);
+ assertInView({ first: 0, last: 19 }, "First 20 items in view");
+ assertFocus({ index: 1 }, "Item 1 has focus");
+ assertSelection([1], "Item 1 is selected");
+
+ EventUtils.synthesizeKey("KEY_PageDown", { ctrlKey, shiftKey }, win);
+ assertInView({ first: 0, last: 19 }, "Same view");
+ if (
+ (ctrlKey && shiftKey) ||
+ model == "focus" ||
+ (model == "browse" && shiftKey)
+ ) {
+ // Does nothing.
+ assertFocus({ index: 1 }, "Same focus");
+ assertSelection([1], "Same selected");
+ // Move focus to the end of the view.
+ clickWidgetItem(19, {});
+ assertFocus({ index: 19 }, "Focus at end of page");
+ assertSelection([19], "Selected at end of page");
+ } else {
+ assertFocus({ index: 19 }, "Focus moves to end of page");
+ if (ctrlKey) {
+ // Splits focus from selected.
+ assertSelection([1], "Same selected");
+ } else {
+ assertSelection(range(1, 19), "Range selection from 1 to 19");
+ }
+ }
+ // And again, with focus at the end of the page.
+ EventUtils.synthesizeKey("KEY_PageDown", { ctrlKey, shiftKey }, win);
+ if (
+ (ctrlKey && shiftKey) ||
+ model == "focus" ||
+ (model == "browse" && shiftKey)
+ ) {
+ // Does nothing.
+ assertInView({ first: 0, last: 19 }, "Same view");
+ assertFocus({ index: 19 }, "Same focus");
+ assertSelection([19], "Same selected");
+ } else {
+ assertInView({ first: 19, last: 38 }, "View scrolls to focus");
+ assertFocus({ index: 38 }, "Focus moves to end of new page");
+ if (ctrlKey) {
+ // Splits focus from selected.
+ assertSelection([1], "Same selected");
+ } else {
+ assertSelection(range(1, 38), "Range selection from 1 to 38");
+ }
+ }
+
+ EventUtils.synthesizeKey("KEY_End", {}, win);
+ EventUtils.synthesizeKey(backwardKey, {}, win);
+ assertInView({ first: 50, last: 69 }, "Last 20 items in view");
+ assertFocus({ index: 68 }, "Item 68 has focus");
+ assertSelection([68], "Item 68 is selected");
+
+ EventUtils.synthesizeKey("KEY_PageUp", { ctrlKey, shiftKey }, win);
+ assertInView({ first: 50, last: 69 }, "Same view");
+ if (
+ (ctrlKey && shiftKey) ||
+ model == "focus" ||
+ (model == "browse" && shiftKey)
+ ) {
+ // Does nothing.
+ assertFocus({ index: 68 }, "Same focus");
+ assertSelection([68], "Same selected");
+ // Move focus to the end of the view.
+ clickWidgetItem(50, {});
+ assertFocus({ index: 50 }, "Focus at start of page");
+ assertSelection([50], "Selected at start of page");
+ } else {
+ assertFocus({ index: 50 }, "Focus moves to start of page");
+ if (ctrlKey) {
+ // Splits focus from selected.
+ assertSelection([68], "Same selected");
+ } else {
+ assertSelection(range(50, 19), "Range selection from 50 to 68");
+ }
+ }
+ // And again, with focus at the start of the page.
+ EventUtils.synthesizeKey("KEY_PageUp", { ctrlKey, shiftKey }, win);
+ if (
+ (ctrlKey && shiftKey) ||
+ model == "focus" ||
+ (model == "browse" && shiftKey)
+ ) {
+ // Does nothing.
+ assertInView({ first: 50, last: 69 }, "Same view");
+ assertFocus({ index: 50 }, "Same focus");
+ assertSelection([50], "Same selected");
+ } else {
+ assertInView({ first: 31, last: 50 }, "View scrolls to focus");
+ assertFocus({ index: 31 }, "Focus moves to start of new page");
+ if (ctrlKey) {
+ // Splits focus from selected.
+ assertSelection([68], "Same selected");
+ } else {
+ assertSelection(range(31, 38), "Range selection from 31 to 68");
+ }
+ }
+ }
+
+ // Does nothing with an empty widget.
+ reset({ model, direction });
+ stepFocus(true, { element: widget }, "Focus on empty widget");
+ assertState([], "Empty");
+ EventUtils.synthesizeKey("KEY_PageDown", {}, win);
+ assertFocus({ element: widget }, "No change in focus");
+ assertState([], "Empty");
+ EventUtils.synthesizeKey("KEY_PageUp", {}, win);
+ assertFocus({ element: widget }, "No change in focus");
+ assertState([], "Empty");
+}
+
+// Test that pressing PageUp or PageDown shifts the view according to the
+// visible items.
+add_task(function test_page_navigation() {
+ for (let model of selectionModels) {
+ subtest_page_navigation(model, "top-to-bottom", {
+ sizeName: "height",
+ forwardKey: "KEY_ArrowDown",
+ backwardKey: "KEY_ArrowUp",
+ scrollTo: pos => {
+ widget.scrollTop = pos;
+ },
+ getStartEnd: rect => {
+ return {
+ start: rect.top,
+ end: rect.bottom,
+ xStart: rect.right - 1,
+ xEnd: rect.left + 1,
+ yStart: rect.top + 1,
+ yEnd: rect.bottom - 1,
+ };
+ },
+ });
+ subtest_page_navigation(model, "right-to-left", {
+ sizeName: "width",
+ forwardKey: "KEY_ArrowLeft",
+ backwardKey: "KEY_ArrowRight",
+ scrollTo: pos => {
+ widget.scrollLeft = -pos;
+ },
+ getStartEnd: rect => {
+ return {
+ start: -rect.right,
+ end: -rect.left,
+ xStart: rect.right - 1,
+ xEnd: rect.left + 1,
+ yStart: rect.top + 1,
+ yEnd: rect.bottom - 1,
+ };
+ },
+ });
+ subtest_page_navigation(model, "left-to-right", {
+ sizeName: "width",
+ forwardKey: "KEY_ArrowRight",
+ backwardKey: "KEY_ArrowLeft",
+ scrollTo: pos => {
+ widget.scrollLeft = pos;
+ },
+ getStartEnd: rect => {
+ return {
+ start: rect.left,
+ end: rect.right,
+ xStart: rect.left + 1,
+ xEnd: rect.right - 1,
+ yStart: rect.top + 1,
+ yEnd: rect.bottom - 1,
+ };
+ },
+ });
+ }
+});
+
+// Using Space to select items.
+add_task(function test_space_selection() {
+ for (let model of selectionModels) {
+ reset({ model, direction: "right-to-left" });
+ widget.addItems(0, ["First", "Second", "Third", "Fourth"]);
+
+ stepFocus(true, { index: 0 }, "Move focus to first item");
+ assertSelection([0], "First item is selected");
+
+ // Selecting an already selected item does nothing.
+ EventUtils.synthesizeKey(" ", {}, win);
+ assertFocus({ index: 0 }, "First item still has focus");
+ assertSelection([0], "First item is still selected");
+
+ if (model == "focus") {
+ // Just move to second item as set up for the loop.
+ EventUtils.synthesizeKey("KEY_ArrowLeft", {}, win);
+ } else {
+ // Selecting a non-selected item will move selection to it.
+ EventUtils.synthesizeKey("KEY_ArrowLeft", { ctrlKey: true }, win);
+ assertFocus({ index: 1 }, "Second item has focus");
+ assertSelection([0], "First item is still selected");
+ EventUtils.synthesizeKey(" ", {}, win);
+ assertFocus({ index: 1 }, "Second item still has focus");
+ assertSelection([1], "Second item becomes selected");
+ }
+
+ // Ctrl + Space will toggle the selection if multi-selection is supported.
+ EventUtils.synthesizeKey(" ", { ctrlKey: true }, win);
+ if (model == "focus") {
+ // Did nothing.
+ assertFocus({ index: 1 }, "Second item still has focus");
+ assertSelection([1], "Second item is still selected");
+ } else if (model == "browse") {
+ // Did nothing.
+ assertFocus({ index: 1 }, "Second item still has focus");
+ assertSelection([1], "Second item is still selected");
+ // Make sure nothing happens when on a non-selected item as well.
+ EventUtils.synthesizeKey("KEY_ArrowLeft", { ctrlKey: true }, win);
+ assertFocus({ index: 2 }, "Third item has focus");
+ assertSelection([1], "Second item is still selected");
+ EventUtils.synthesizeKey(" ", { ctrlKey: true }, win);
+ assertFocus({ index: 2 }, "Third item still has focus");
+ assertSelection([1], "Second item is still selected");
+ // Restore the previous state.
+ EventUtils.synthesizeKey("KEY_ArrowRight", {}, win);
+ } else {
+ // Unselected the item.
+ assertFocus({ index: 1 }, "Second item still has focus");
+ assertSelection([], "Second item was un-selected");
+ // Toggle again.
+ EventUtils.synthesizeKey(" ", { ctrlKey: true }, win);
+ assertFocus({ index: 1 }, "Second item still has focus");
+ assertSelection([1], "Second item was re-selected");
+
+ // Do on another index.
+ EventUtils.synthesizeKey("KEY_ArrowLeft", { ctrlKey: true }, win);
+ EventUtils.synthesizeKey("KEY_ArrowLeft", { ctrlKey: true }, win);
+ assertFocus({ index: 3 }, "Fourth item has focus");
+ assertSelection([1], "Second item is still selected");
+ EventUtils.synthesizeKey(" ", { ctrlKey: true }, win);
+ assertFocus({ index: 3 }, "Fourth item still has focus");
+ assertSelection([1, 3], "Fourth item becomes selected as well");
+
+ // Move to third without clearing.
+ EventUtils.synthesizeKey("KEY_ArrowRight", { ctrlKey: true }, win);
+ assertFocus({ index: 2 }, "Third item has focus");
+ assertSelection([1, 3], "Fourth and second item remain selected");
+
+ // Merge the two ranges together.
+ EventUtils.synthesizeKey(" ", { ctrlKey: true }, win);
+ assertFocus({ index: 2 }, "Third item still has focus");
+ assertSelection([1, 2, 3], "Third item becomes selected");
+
+ // Shrink the range at the end.
+ EventUtils.synthesizeKey("KEY_ArrowLeft", { ctrlKey: true }, win);
+ assertFocus({ index: 3 }, "Fourth item has focus");
+ assertSelection([1, 2, 3], "Same selection");
+ EventUtils.synthesizeKey(" ", { ctrlKey: true }, win);
+ assertFocus({ index: 3 }, "Fourth item still has focus");
+ assertSelection([1, 2], "Fourth item unselected");
+
+ // Shrink the range at the start.
+ EventUtils.synthesizeKey("KEY_ArrowRight", { ctrlKey: true }, win);
+ EventUtils.synthesizeKey("KEY_ArrowRight", { ctrlKey: true }, win);
+ assertFocus({ index: 1 }, "Second item has focus");
+ assertSelection([1, 2], "Same selection");
+ EventUtils.synthesizeKey(" ", { ctrlKey: true }, win);
+ assertFocus({ index: 1 }, "Second item still has focus");
+ assertSelection([2], "Second item unselected");
+
+ // No selection.
+ EventUtils.synthesizeKey("KEY_ArrowLeft", { ctrlKey: true }, win);
+ assertFocus({ index: 2 }, "Third item has focus");
+ assertSelection([2], "Same selection");
+ EventUtils.synthesizeKey(" ", { ctrlKey: true }, win);
+ assertFocus({ index: 2 }, "Third item still has focus");
+ assertSelection([], "Third item unselected");
+
+ // Using arrow keys without modifier will re-introduce a single selection.
+ EventUtils.synthesizeKey("KEY_ArrowRight");
+ assertFocus({ index: 1 }, "Second item has focus");
+ assertSelection([1], "Second item becomes selected");
+
+ // Grow range at the start.
+ EventUtils.synthesizeKey("KEY_ArrowRight", { ctrlKey: true }, win);
+ assertFocus({ index: 0 }, "First item has focus");
+ assertSelection([1], "Same selection");
+ EventUtils.synthesizeKey(" ", { ctrlKey: true }, win);
+ assertFocus({ index: 0 }, "First item still has focus");
+ assertSelection([0, 1], "First item becomes selected");
+
+ // Grow range at the end.
+ EventUtils.synthesizeKey("KEY_End", { ctrlKey: true }, win);
+ EventUtils.synthesizeKey("KEY_ArrowRight", { ctrlKey: true }, win);
+ assertFocus({ index: 2 }, "Third item has focus");
+ assertSelection([0, 1], "Same selection");
+ EventUtils.synthesizeKey(" ", { ctrlKey: true }, win);
+ assertFocus({ index: 2 }, "Third item still has focus");
+ assertSelection([0, 1, 2], "Third item becomes selected");
+
+ // Split the range in half.
+ EventUtils.synthesizeKey("KEY_ArrowRight", { ctrlKey: true }, win);
+ assertFocus({ index: 1 }, "Second item has focus");
+ assertSelection([0, 1, 2], "Same selection");
+ EventUtils.synthesizeKey(" ", { ctrlKey: true }, win);
+ assertFocus({ index: 1 }, "Second item still has focus");
+ assertSelection([0, 2], "Second item unselected");
+
+ // Pressing Space without a modifier clears the multi-selection.
+ EventUtils.synthesizeKey(" ", {}, win);
+ }
+
+ // Make sure we are in the expected shared state between models.
+ assertFocus({ index: 1 }, "Second item has focus");
+ assertSelection([1], "Second item is selected");
+
+ // Shift + Space will do nothing.
+ for (let ctrlKey of [false, true]) {
+ info(`Pressing ${ctrlKey ? "Ctrl+" : ""}Shift+space on item`);
+ // On selected item.
+ EventUtils.synthesizeKey(" ", { ctrlKey, shiftKey: true }, win);
+ assertFocus({ index: 1 }, "Second item still has focus");
+ assertSelection([1], "Second item is still selected");
+
+ if (model == "focus") {
+ continue;
+ }
+
+ // On non-selected item.
+ EventUtils.synthesizeKey("KEY_ArrowLeft", { ctrlKey: true }, win);
+ assertFocus({ index: 2 }, "Third item has focus");
+ assertSelection([1], "Second item is still selected");
+ EventUtils.synthesizeKey(" ", { ctrlKey, shiftKey: true }, win);
+ assertFocus({ index: 2 }, "Third item still has focus");
+ assertSelection([1], "Second item is still selected");
+
+ // Restore for next loop.
+ EventUtils.synthesizeKey("KEY_ArrowRight", {}, win);
+ assertFocus({ index: 1 }, "Second item has focus");
+ assertSelection([1], "Second item is selected");
+ }
+ }
+});
+
+// Clicking an item will focus and select it.
+add_task(function test_clicking_items() {
+ for (let model of selectionModels) {
+ reset({ model, direction: "right-to-left" });
+ widget.addItems(0, [
+ "First",
+ "Second",
+ "Third",
+ "Fourth",
+ "Fifth",
+ "Sixth",
+ "Seventh",
+ "Eighth",
+ ]);
+
+ assertFocus({ element: before }, "Focus initially outside widget");
+ assertSelection([], "No initial selection");
+
+ // Focus moves into widget, onto the clicked item.
+ clickWidgetItem(1, {});
+ assertFocus({ index: 1 }, "Focus clicked second item");
+ assertSelection([1], "Selected clicked second item");
+
+ // Focus moves to different item.
+ clickWidgetItem(2, {});
+ assertFocus({ index: 2 }, "Focus clicked third item");
+ assertSelection([2], "Selected clicked third item");
+
+ // Click same item.
+ clickWidgetItem(2, {});
+ assertFocus({ index: 2 }, "Focus remains on third item");
+ assertSelection([2], "Selected remains on third item");
+
+ // Focus outside widget, focus moves but selection remains.
+ before.focus();
+ assertFocus({ element: before }, "Focus outside widget");
+ assertSelection([2], "Selected remains on third item");
+
+ // Clicking same item will return focus to it.
+ clickWidgetItem(2, {});
+ assertFocus({ index: 2 }, "Focus returns to third item");
+ assertSelection([2], "Selected remains on third item");
+
+ // Do the same, but return to a different item.
+ before.focus();
+ assertFocus({ element: before }, "Focus outside widget");
+ assertSelection([2], "Selected remains on third item");
+
+ // Clicking same item will return focus to it.
+ clickWidgetItem(1, {});
+ assertFocus({ index: 1 }, "Focus moves to second item");
+ assertSelection([1], "Selected moves to second item");
+
+ // Switching to keyboard works.
+ EventUtils.synthesizeKey("KEY_ArrowRight", {}, win);
+ assertFocus({ index: 0 }, "Focus moves to first item");
+ assertSelection([0], "Selected moves to first item");
+
+ // Returning to mouse works.
+ clickWidgetItem(1, {});
+ assertFocus({ index: 1 }, "Focus moves to second item");
+ assertSelection([1], "Selected moves to second item");
+
+ // Toggle selection with Ctrl+Click.
+ clickWidgetItem(3, { ctrlKey: true });
+ if (model == "browse-multi") {
+ assertFocus({ index: 3 }, "Focus moves to fourth item");
+ assertSelection([1, 3], "Fourth item is selected");
+
+ clickWidgetItem(7, { ctrlKey: true });
+ assertFocus({ index: 7 }, "Focus moves to eighth item");
+ assertSelection([1, 3, 7], "Eighth item selected");
+
+ // Extend selection range by one after.
+ clickWidgetItem(4, { ctrlKey: true });
+ assertFocus({ index: 4 }, "Focus moves to fifth item");
+ assertSelection([1, 3, 4, 7], "Fifth item is selected");
+
+ // Extend selection range by one before.
+ clickWidgetItem(6, { ctrlKey: true });
+ assertFocus({ index: 6 }, "Focus moves to seventh item");
+ assertSelection([1, 3, 4, 6, 7], "Seventh item is selected");
+
+ // Merge the two ranges together.
+ clickWidgetItem(5, { ctrlKey: true });
+ assertFocus({ index: 5 }, "Focus moves to sixth item");
+ assertSelection([1, 3, 4, 5, 6, 7], "Sixth item is selected");
+
+ // Reverse by unselecting.
+ clickWidgetItem(7, { ctrlKey: true });
+ assertFocus({ index: 7 }, "Focus moves to eight item");
+ assertSelection([1, 3, 4, 5, 6], "Eight item is unselected");
+
+ clickWidgetItem(3, { ctrlKey: true });
+ assertFocus({ index: 3 }, "Focus moves to fourth item");
+ assertSelection([1, 4, 5, 6], "Fourth item is unselected");
+
+ // Split a range.
+ clickWidgetItem(5, { ctrlKey: true });
+ assertFocus({ index: 5 }, "Focus moves to sixth item");
+ assertSelection([1, 4, 6], "Sixth item is unselected");
+
+ clickWidgetItem(1, { ctrlKey: true });
+ assertFocus({ index: 1 }, "Focus moves to second item");
+ assertSelection([4, 6], "Second item is unselected");
+
+ clickWidgetItem(6, { ctrlKey: true });
+ assertFocus({ index: 6 }, "Focus moves to seventh item");
+ assertSelection([4], "Seventh item is unselected");
+
+ // Can get zero-selection.
+ clickWidgetItem(4, { ctrlKey: true });
+ assertFocus({ index: 4 }, "Focus moves to fifth item");
+ assertSelection([], "None selected");
+
+ // Get into the same state as the other case.
+ clickWidgetItem(1, { ctrlKey: true });
+ assertFocus({ index: 1 }, "Focus moves to second item");
+ assertSelection([1], "Second item is selected");
+ } else {
+ // No multi-selection, so does nothing.
+ assertFocus({ index: 1 }, "Focus remains on second item");
+ assertSelection([1], "Second item remains selected");
+ }
+
+ // Ctrl+Shift+Click does nothing in all models.
+ clickWidgetItem(2, { ctrlKey: true, shiftKey: true });
+ assertFocus({ index: 1 }, "Focus remains on second item");
+ assertSelection([1], "Second item remains selected");
+ }
+});
+
+add_task(function test_select_all() {
+ for (let model of selectionModels) {
+ reset({ model, direction: "right-to-left" });
+ widget.addItems(0, ["First", "Second", "Third", "Fourth", "Fifth"]);
+
+ stepFocus(true, { index: 0 }, "Move focus to first item");
+ assertSelection([0], "First item is selected");
+
+ EventUtils.synthesizeKey("KEY_ArrowLeft", {}, win);
+ assertFocus({ index: 1 }, "Focus on second item");
+ assertSelection([1], "Second item is selected");
+
+ selectAllShortcut();
+
+ assertFocus({ index: 1 }, "Focus remains on second item");
+ if (model == "browse-multi") {
+ assertSelection([0, 1, 2, 3, 4], "All items are selected");
+ // Can insert a hole.
+ EventUtils.synthesizeKey("KEY_ArrowLeft", { ctrlKey: true }, win);
+ assertFocus({ index: 2 }, "Focus moves to third item");
+ assertSelection([0, 1, 2, 3, 4], "All items are still selected");
+ EventUtils.synthesizeKey(" ", { ctrlKey: true }, win);
+ assertFocus({ index: 2 }, "Focus remains on third item");
+ assertSelection([0, 1, 3, 4], "Third item was unselected");
+ EventUtils.synthesizeKey("KEY_ArrowRight", { ctrlKey: true }, win);
+ assertFocus({ index: 1 }, "Focus moves to the second item");
+ assertSelection([0, 1, 3, 4], "Selection remains the same");
+ EventUtils.synthesizeKey(" ", {}, win);
+ assertFocus({ index: 1 }, "Focus remains on the second item");
+ assertSelection([1], "Only the second item is selected");
+ } else {
+ // Did nothing.
+ assertSelection([1], "Second item is still selected");
+ }
+
+ // Wrong platform modifier does nothing.
+ EventUtils.synthesizeKey(
+ "a",
+ AppConstants.platform == "macosx" ? { ctrlKey: true } : { metaKey: true },
+ win
+ );
+ assertFocus({ index: 1 }, "Focus remains on second item");
+ assertSelection([1], "Second item still selected");
+ }
+});
+
+// Holding the shift key should perform a range selection if multi-selection is
+// supported by the model.
+add_task(function test_range_selection() {
+ for (let model of selectionModels) {
+ reset({ model, direction: "right-to-left" });
+ widget.addItems(0, ["First", "Second", "Third", "Fourth", "Fifth"]);
+
+ stepFocus(true, { index: 0 }, "Move focus to first item");
+ assertSelection([0], "First item is selected");
+
+ EventUtils.synthesizeKey("KEY_ArrowLeft", {}, win);
+ EventUtils.synthesizeKey("KEY_ArrowLeft", {}, win);
+
+ assertFocus({ index: 2 }, "Focus on third item");
+ assertSelection([2], "Third item is selected");
+
+ // Nothing happens with Ctrl+Shift in any model.
+ EventUtils.synthesizeKey(
+ "KEY_ArrowLeft",
+ { shiftKey: true, ctrlKey: true },
+ win
+ );
+ assertFocus({ index: 2 }, "Focus remains on third item");
+ assertSelection([2], "Only second item is selected");
+ EventUtils.synthesizeKey(
+ "KEY_ArrowRight",
+ { shiftKey: true, ctrlKey: true },
+ win
+ );
+ assertFocus({ index: 2 }, "Focus remains on third item");
+ assertSelection([2], "Only second item is selected");
+
+ // With just Shift modifier.
+ if (model == "focus" || model == "browse") {
+ // No range selection.
+ EventUtils.synthesizeKey("KEY_ArrowLeft", { shiftKey: true }, win);
+ assertFocus({ index: 2 }, "Focus remains on third item");
+ assertSelection([2], "Only second item is selected");
+
+ EventUtils.synthesizeKey("KEY_ArrowRight", { shiftKey: true }, win);
+ assertFocus({ index: 2 }, "Focus remains on third item");
+ assertSelection([2], "Only second item is selected");
+
+ clickWidgetItem(3, { shiftKey: true });
+ assertFocus({ index: 2 }, "Focus remains on third item");
+ assertSelection([2], "Only second item is selected");
+
+ clickWidgetItem(1, { shiftKey: true });
+ assertFocus({ index: 2 }, "Focus remains on third item");
+ assertSelection([2], "Only second item is selected");
+ continue;
+ }
+
+ // Range selection with shift key.
+ EventUtils.synthesizeKey("KEY_ArrowLeft", { shiftKey: true }, win);
+ assertFocus({ index: 3 }, "Focus on fourth item");
+ assertSelection([2, 3], "Select from third to fourth item");
+
+ // Reverse
+ EventUtils.synthesizeKey("KEY_ArrowRight", { shiftKey: true }, win);
+ assertFocus({ index: 2 }, "Focus on third item");
+ assertSelection([2], "Select from third to same item");
+
+ // Go back another step.
+ EventUtils.synthesizeKey("KEY_ArrowRight", { shiftKey: true }, win);
+ assertFocus({ index: 1 }, "Focus on second item");
+ assertSelection([1, 2], "Third to second items are selected");
+
+ // Split focus from selection.
+ EventUtils.synthesizeKey("KEY_ArrowLeft", { ctrlKey: true }, win);
+ assertFocus({ index: 2 }, "Focus on third item");
+ assertSelection([1, 2], "Third to second items are still selected");
+
+ // Back to range selection.
+ EventUtils.synthesizeKey("KEY_ArrowLeft", { shiftKey: true }, win);
+ assertFocus({ index: 3 }, "Focus on fourth item");
+ assertSelection([2, 3], "Third to fourth items are selected");
+
+ EventUtils.synthesizeKey("KEY_ArrowLeft", { shiftKey: true }, win);
+ assertFocus({ index: 4 }, "Focus on fifth item");
+ assertSelection([2, 3, 4], "Third to fifth items are selected");
+
+ // Moving without a modifier breaks the range.
+ EventUtils.synthesizeKey("KEY_ArrowLeft", {}, win);
+ assertFocus({ index: 4 }, "Focus remains on final fifth item");
+ assertSelection([4], "Fifth item is selected");
+
+ // Again at the middle.
+ EventUtils.synthesizeKey("KEY_ArrowRight", { shiftKey: true }, win);
+ assertFocus({ index: 3 }, "Focus moves to fourth item");
+ assertSelection([3, 4], "Fifth to fourth items are selected");
+ EventUtils.synthesizeKey("KEY_ArrowRight", {}, win);
+ assertFocus({ index: 2 }, "Focus moves to third item");
+ assertSelection([2], "Only third item is selected");
+
+ // Home and End also work.
+ EventUtils.synthesizeKey("KEY_Home", { shiftKey: true }, win);
+ assertFocus({ index: 0 }, "Focus moves to first item");
+ assertSelection([0, 1, 2], "Up to third item is selected");
+
+ EventUtils.synthesizeKey("KEY_End", { shiftKey: true }, win);
+ assertFocus({ index: 4 }, "Focus moves to last item");
+ assertSelection([2, 3, 4], "Third item and above is selected");
+
+ // Ctrl+A breaks range selection sequence, so we no longer select around the
+ // third item when we go back to using Shift+Arrow.
+ selectAllShortcut();
+ assertFocus({ index: 4 }, "Focus remains on last item");
+ assertSelection([0, 1, 2, 3, 4], "All items are selected");
+ // The new shift+range will be from the focus index (the fifth item) rather
+ // than the third item used for the previous range.
+ EventUtils.synthesizeKey("KEY_ArrowRight", { shiftKey: true }, win);
+ assertFocus({ index: 3 }, "Focus moves to fourth item");
+ assertSelection([3, 4], "Fifth to fourth item are selected");
+
+ // Ctrl+Space also breaks range selection sequence.
+ EventUtils.synthesizeKey("KEY_Home", { ctrlKey: true }, win);
+ assertFocus({ index: 0 }, "Focus moves to first item");
+ assertSelection([3, 4], "Range selection remains");
+ EventUtils.synthesizeKey(" ", { ctrlKey: true }, win);
+ assertFocus({ index: 0 }, "Focus still on first item");
+ assertSelection([0, 3, 4], "First item added to selection");
+ EventUtils.synthesizeKey("KEY_ArrowLeft", { shiftKey: true }, win);
+ assertFocus({ index: 1 }, "Focus moves to second item");
+ assertSelection([0, 1], "First to second item are selected");
+
+ // Same when unselecting.
+ EventUtils.synthesizeKey(" ", { ctrlKey: true }, win);
+ assertFocus({ index: 1 }, "Focus remains on second item");
+ assertSelection([0], "Second item is no longer selected");
+
+ EventUtils.synthesizeKey("KEY_ArrowLeft", { shiftKey: true }, win);
+ assertFocus({ index: 2 }, "Focus moves to third item");
+ assertSelection([1, 2], "Second to third item are selected");
+
+ // Same when using setItemSelected API
+ widget.setItemSelected(4, true);
+ assertFocus({ index: 2 }, "Focus remains on third item");
+ assertSelection([1, 2, 4], "Fifth item becomes selected");
+
+ EventUtils.synthesizeKey("KEY_ArrowLeft", { shiftKey: true }, win);
+ assertFocus({ index: 3 }, "Focus moves to fourth item");
+ assertSelection([2, 3], "Third to fourth item are selected");
+ EventUtils.synthesizeKey("KEY_ArrowLeft", { shiftKey: true }, win);
+ assertFocus({ index: 4 }, "Focus moves to fifth item");
+ assertSelection([2, 3, 4], "Third to fifth item are selected");
+
+ widget.setItemSelected(3, false);
+ assertFocus({ index: 4 }, "Focus remains on fifth item");
+ assertSelection([2, 4], "Fourth item becomes unselected");
+
+ EventUtils.synthesizeKey("KEY_ArrowRight", { shiftKey: true }, win);
+ assertFocus({ index: 3 }, "Focus moves to fourth item");
+ assertSelection([3, 4], "Fifth to fourth item are selected");
+
+ // Even when the selection state does not change.
+ widget.setItemSelected(3, true);
+ assertFocus({ index: 3 }, "Focus remains on fourth item");
+ assertSelection([3, 4], "Same selection");
+ EventUtils.synthesizeKey("KEY_ArrowRight", { shiftKey: true }, win);
+ assertFocus({ index: 2 }, "Focus moves to third item");
+ assertSelection([2, 3], "Fourth to third item are selected");
+ EventUtils.synthesizeKey("KEY_ArrowRight", { shiftKey: true }, win);
+ assertFocus({ index: 1 }, "Focus moves to second item");
+ assertSelection([1, 2, 3], "Fourth to second item are selected");
+
+ widget.setItemSelected(4, false);
+ assertFocus({ index: 1 }, "Focus remains on second item");
+ assertSelection([1, 2, 3], "Same selection");
+ EventUtils.synthesizeKey("KEY_ArrowLeft", { shiftKey: true }, win);
+ assertFocus({ index: 2 }, "Focus moves to third item");
+ assertSelection([1, 2], "Second to third item are selected");
+
+ // Same when selecting with space (no modifier).
+ EventUtils.synthesizeKey(" ", {}, win);
+ assertFocus({ index: 2 }, "Focus remains on third item");
+ assertSelection([2], "Third item is selected");
+
+ EventUtils.synthesizeKey("KEY_ArrowLeft", { shiftKey: true }, win);
+ assertFocus({ index: 3 }, "Focus moves to fourth item");
+ assertSelection([2, 3], "Third to fourth item are selected");
+
+ // Same when using the selectSingleItem API.
+ widget.selectSingleItem(1);
+ assertFocus({ index: 1 }, "Focus moves to second item");
+ assertSelection([1], "Second item is selected");
+
+ EventUtils.synthesizeKey("KEY_ArrowRight", { shiftKey: true }, win);
+ assertFocus({ index: 0 }, "Focus moves to first item");
+ assertSelection([0, 1], "Second to first item are selected");
+
+ // If focus goes out and we return, the range origin is remembered.
+ stepFocus(true, { element: after }, "Move focus outside the widget");
+ assertSelection([0, 1], "Second to first item are still selected");
+ stepFocus(false, { index: 0 }, "Focus returns to the widget");
+ assertSelection([0, 1], "Second to first item are still selected");
+
+ EventUtils.synthesizeKey("KEY_End", { shiftKey: true }, win);
+ assertFocus({ index: 4 }, "Focus moves to last item");
+ assertSelection([1, 2, 3, 4], "Second to fifth item are selected");
+
+ // Clicking empty space does not clear it.
+ clickWidgetEmptySpace({});
+ assertFocus({ index: 4 }, "Focus remains on last item");
+ assertSelection([1, 2, 3, 4], "Second to fifth item are still selected");
+
+ // Shift+Click an item will use the same range origin established by the
+ // current selection.
+ clickWidgetItem(3, { shiftKey: true });
+ assertFocus({ index: 3 }, "Focus moves to fourth item");
+ assertSelection([1, 2, 3], "Second to fourth item are selected");
+
+ // Clicking without the modifier breaks the range selection sequence.
+ clickWidgetItem(2, {});
+ assertFocus({ index: 2 }, "Focus moves to third item");
+ assertSelection([2], "Only the third item is selected");
+
+ // Shift click will select between the third item and the the clicked item.
+ clickWidgetItem(4, { shiftKey: true });
+ assertFocus({ index: 4 }, "Focus moves to fifth item");
+ assertSelection([2, 3, 4], "Third to fifth item are selected");
+
+ // Reverse direction about the same point.
+ clickWidgetItem(0, { shiftKey: true });
+ assertFocus({ index: 0 }, "Focus moves to first item");
+ assertSelection([0, 1, 2], "Third to first item are selected");
+
+ // Ctrl+Click breaks the range selection sequence.
+ clickWidgetItem(1, { ctrlKey: true });
+ assertFocus({ index: 1 }, "Focus moves to second item");
+ assertSelection([0, 2], "Second item is unselected");
+ clickWidgetItem(3, { shiftKey: true });
+ assertFocus({ index: 3 }, "Focus moves to fourth item");
+ assertSelection([1, 2, 3], "Second to fourth item are selected");
+
+ // Same when Ctrl+Click on non-selected.
+ clickWidgetItem(4, { ctrlKey: true });
+ assertFocus({ index: 4 }, "Focus moves to fifth item");
+ assertSelection([1, 2, 3, 4], "Fifth item is selected");
+ clickWidgetItem(3, { shiftKey: true });
+ assertFocus({ index: 3 }, "Focus moves to fourth item");
+ assertSelection([3, 4], "Fifth to fourth item are selected");
+
+ // Selecting-all also breaks range selection sequence.
+ EventUtils.synthesizeKey("KEY_ArrowRight", { ctrlKey: true }, win);
+ assertFocus({ index: 2 }, "Focus moves to third item");
+ assertSelection([3, 4], "Same selection");
+
+ selectAllShortcut();
+ assertFocus({ index: 2 }, "Focus remains on third item");
+ assertSelection([0, 1, 2, 3, 4], "All items selected");
+
+ EventUtils.synthesizeKey("KEY_ArrowRight", { shiftKey: true }, win);
+ assertFocus({ index: 1 }, "Focus moves to second item");
+ assertSelection([1, 2], "Third to second item selected");
+ }
+});
+
+// Adding items to widget with existing items, should not change the selected
+// item.
+add_task(function test_add_items_to_nonempty() {
+ for (let model of selectionModels) {
+ reset({ model, direction: "right-to-left" });
+ assertState([], "Empty");
+
+ widget.addItems(0, ["0-add"]);
+ stepFocus(true, { index: 0, text: "0-add" }, "Move focus to 0-add");
+ assertState([{ text: "0-add", selected: true, focused: true }], "One item");
+
+ // Add item after.
+ widget.addItems(1, ["1-add"]);
+ assertState(
+ [{ text: "0-add", selected: true, focused: true }, { text: "1-add" }],
+ "0-add still focused and selected"
+ );
+
+ // Add item before. 0-add moves to index 1.
+ widget.addItems(0, ["2-add"]);
+ assertState(
+ [
+ { text: "2-add" },
+ { text: "0-add", selected: true, focused: true },
+ { text: "1-add" },
+ ],
+ "0-add still focused and selected"
+ );
+
+ // Add several before.
+ widget.addItems(1, ["3-add", "4-add", "5-add"]);
+ assertState(
+ [
+ { text: "2-add" },
+ { text: "3-add" },
+ { text: "4-add" },
+ { text: "5-add" },
+ { text: "0-add", selected: true, focused: true },
+ { text: "1-add" },
+ ],
+ "0-add still focused and selected"
+ );
+
+ // Key navigation works.
+ EventUtils.synthesizeKey("KEY_ArrowRight", {}, win);
+ assertState(
+ [
+ { text: "2-add" },
+ { text: "3-add" },
+ { text: "4-add" },
+ { text: "5-add", selected: true, focused: true },
+ { text: "0-add" },
+ { text: "1-add" },
+ ],
+ "5-add becomes focused and selected"
+ );
+
+ // With focus outside the widget.
+ reset({ model, direction: "right-to-left" });
+ assertState([], "Empty");
+
+ widget.addItems(0, ["0-add"]);
+ stepFocus(true, { index: 0 }, "Move focus to 0-add");
+ assertState([{ text: "0-add", selected: true, focused: true }], "One item");
+
+ stepFocus(true, { element: after }, "Move focus to after widget");
+ // Add after.
+ widget.addItems(1, ["1-add", "2-add"]);
+ assertState(
+ [{ text: "0-add", selected: true }, { text: "1-add" }, { text: "2-add" }],
+ "0-add still selected but not focused"
+ );
+ stepFocus(false, { index: 0 }, "Move focus back to 0-add");
+ assertState(
+ [
+ { text: "0-add", selected: true, focused: true },
+ { text: "1-add" },
+ { text: "2-add" },
+ ],
+ "0-add selected and focused"
+ );
+
+ stepFocus(false, { element: before }, "Move focus to before widget");
+ // Add before.
+ widget.addItems(0, ["3-add", "4-add"]);
+ assertState(
+ [
+ { text: "3-add" },
+ { text: "4-add" },
+ { text: "0-add", selected: true },
+ { text: "1-add" },
+ { text: "2-add" },
+ ],
+ "0-add selected but not focused"
+ );
+ stepFocus(true, { index: 2 }, "Move focus back to 0-add");
+ assertState(
+ [
+ { text: "3-add" },
+ { text: "4-add" },
+ { text: "0-add", selected: true, focused: true },
+ { text: "1-add" },
+ { text: "2-add" },
+ ],
+ "0-add selected and focused"
+ );
+
+ // With focus separate from selection.
+ if (model == "focus") {
+ continue;
+ }
+
+ reset({ model, direction: "right-to-left" });
+ assertState([], "Empty");
+
+ widget.addItems(0, ["0-add", "1-add", "2-add"]);
+ assertState(
+ [{ text: "0-add" }, { text: "1-add" }, { text: "2-add" }],
+ "None selected or focused"
+ );
+ stepFocus(true, { index: 0 }, "Move focus to 0-add");
+
+ // With selection after focus.
+ EventUtils.synthesizeKey("KEY_End", {}, win);
+ EventUtils.synthesizeKey("KEY_ArrowRight", { ctrlKey: true }, win);
+ assertState(
+ [
+ { text: "0-add" },
+ { text: "1-add", focused: true },
+ { text: "2-add", selected: true },
+ ],
+ "Selection after focus"
+ );
+
+ // Add after both selection and focus.
+ widget.addItems(3, ["3-add"]);
+ assertState(
+ [
+ { text: "0-add" },
+ { text: "1-add", focused: true },
+ { text: "2-add", selected: true },
+ { text: "3-add" },
+ ],
+ "Same items selected and focused"
+ );
+
+ // Add before both selection and focus.
+ widget.addItems(1, ["4-add"]);
+ assertState(
+ [
+ { text: "0-add" },
+ { text: "4-add" },
+ { text: "1-add", focused: true },
+ { text: "2-add", selected: true },
+ { text: "3-add" },
+ ],
+ "Same items selected and focused"
+ );
+
+ // Before selection, after focus.
+ widget.addItems(3, ["5-add"]);
+ assertState(
+ [
+ { text: "0-add" },
+ { text: "4-add" },
+ { text: "1-add", focused: true },
+ { text: "5-add" },
+ { text: "2-add", selected: true },
+ { text: "3-add" },
+ ],
+ "Same items selected and focused"
+ );
+
+ // Swap selection to be before focus.
+ EventUtils.synthesizeKey("KEY_ArrowRight", {}, win);
+ EventUtils.synthesizeKey("KEY_ArrowLeft", { ctrlKey: true }, win);
+ EventUtils.synthesizeKey("KEY_ArrowLeft", { ctrlKey: true }, win);
+ assertState(
+ [
+ { text: "0-add" },
+ { text: "4-add", selected: true },
+ { text: "1-add" },
+ { text: "5-add", focused: true },
+ { text: "2-add" },
+ { text: "3-add" },
+ ],
+ "Selection before focus"
+ );
+
+ // After selection, before focus.
+ widget.addItems(2, ["6-add", "7-add", "8-add"]);
+ assertState(
+ [
+ { text: "0-add" },
+ { text: "4-add", selected: true },
+ { text: "6-add" },
+ { text: "7-add" },
+ { text: "8-add" },
+ { text: "1-add" },
+ { text: "5-add", focused: true },
+ { text: "2-add" },
+ { text: "3-add" },
+ ],
+ "Same items selected and focused"
+ );
+
+ // With multi-selection.
+ if (model == "browse") {
+ continue;
+ }
+
+ reset({ model, direction: "right-to-left" });
+ assertState([], "Empty");
+
+ widget.addItems(0, ["0-add", "1-add", "2-add"]);
+ assertState(
+ [{ text: "0-add" }, { text: "1-add" }, { text: "2-add" }],
+ "None selected"
+ );
+ stepFocus(true, { index: 0 }, "Move focus to 0-add");
+
+ // Select all.
+ EventUtils.synthesizeKey("KEY_End", { shiftKey: true }, win);
+ assertState(
+ [
+ { text: "0-add", selected: true },
+ { text: "1-add", selected: true },
+ { text: "2-add", selected: true, focused: true },
+ ],
+ "All selected"
+ );
+
+ // Add after all.
+ widget.addItems(3, ["3-add", "4-add", "5-add"]);
+ assertState(
+ [
+ { text: "0-add", selected: true },
+ { text: "1-add", selected: true },
+ { text: "2-add", selected: true, focused: true },
+ { text: "3-add" },
+ { text: "4-add" },
+ { text: "5-add" },
+ ],
+ "Same range selected"
+ );
+
+ // Can continue shift selection to newly added item
+ EventUtils.synthesizeKey("KEY_ArrowLeft", { shiftKey: true }, win);
+ assertState(
+ [
+ { text: "0-add", selected: true },
+ { text: "1-add", selected: true },
+ { text: "2-add", selected: true },
+ { text: "3-add", selected: true, focused: true },
+ { text: "4-add" },
+ { text: "5-add" },
+ ],
+ "Range extended to new item"
+ );
+
+ // Add before all.
+ widget.addItems(0, ["6-add", "7-add"]);
+ assertState(
+ [
+ { text: "6-add" },
+ { text: "7-add" },
+ { text: "0-add", selected: true },
+ { text: "1-add", selected: true },
+ { text: "2-add", selected: true },
+ { text: "3-add", selected: true, focused: true },
+ { text: "4-add" },
+ { text: "5-add" },
+ ],
+ "Same range selected"
+ );
+
+ // Can continue shift selection about the "0-add" item.
+ EventUtils.synthesizeKey("KEY_ArrowRight", { shiftKey: true }, win);
+ assertState(
+ [
+ { text: "6-add" },
+ { text: "7-add" },
+ { text: "0-add", selected: true },
+ { text: "1-add", selected: true },
+ { text: "2-add", selected: true, focused: true },
+ { text: "3-add" },
+ { text: "4-add" },
+ { text: "5-add" },
+ ],
+ "Range extended backward"
+ );
+
+ // And change direction of shift selection range.
+ EventUtils.synthesizeKey("KEY_Home", { ctrlKey: true }, win);
+ assertState(
+ [
+ { text: "6-add", focused: true },
+ { text: "7-add" },
+ { text: "0-add", selected: true },
+ { text: "1-add", selected: true },
+ { text: "2-add", selected: true },
+ { text: "3-add" },
+ { text: "4-add" },
+ { text: "5-add" },
+ ],
+ "Focus moves to first item"
+ );
+ EventUtils.synthesizeKey("KEY_ArrowLeft", { shiftKey: true }, win);
+ assertState(
+ [
+ { text: "6-add" },
+ { text: "7-add", selected: true, focused: true },
+ { text: "0-add", selected: true },
+ { text: "1-add" },
+ { text: "2-add" },
+ { text: "3-add" },
+ { text: "4-add" },
+ { text: "5-add" },
+ ],
+ "Selection pivoted about 0-add"
+ );
+
+ // Add items in the middle of the range. Selection in the range is not added
+ // initially.
+ widget.addItems(2, ["8-add", "9-add", "10-add"]);
+ assertState(
+ [
+ { text: "6-add" },
+ { text: "7-add", selected: true, focused: true },
+ { text: "8-add" },
+ { text: "9-add" },
+ { text: "10-add" },
+ { text: "0-add", selected: true },
+ { text: "1-add" },
+ { text: "2-add" },
+ { text: "3-add" },
+ { text: "4-add" },
+ { text: "5-add" },
+ ],
+ "Backward range selection with single gap"
+ );
+
+ // But continuing the shift selection will fill in the holes again.
+ EventUtils.synthesizeKey("KEY_ArrowRight", { shiftKey: true }, win);
+ assertState(
+ [
+ { text: "6-add", selected: true, focused: true },
+ { text: "7-add", selected: true },
+ { text: "8-add", selected: true },
+ { text: "9-add", selected: true },
+ { text: "10-add", selected: true },
+ { text: "0-add", selected: true },
+ { text: "1-add" },
+ { text: "2-add" },
+ { text: "3-add" },
+ { text: "4-add" },
+ { text: "5-add" },
+ ],
+ "Backward range selection with no gap"
+ );
+
+ // Do the same but with a selection range moving forward and two holes.
+ EventUtils.synthesizeKey("KEY_ArrowLeft", {}, win);
+ EventUtils.synthesizeKey("KEY_ArrowLeft", {}, win);
+ EventUtils.synthesizeKey("KEY_ArrowLeft", { shiftKey: true }, win);
+ EventUtils.synthesizeKey("KEY_ArrowLeft", { shiftKey: true }, win);
+ assertState(
+ [
+ { text: "6-add" },
+ { text: "7-add" },
+ { text: "8-add", selected: true },
+ { text: "9-add", selected: true },
+ { text: "10-add", selected: true, focused: true },
+ { text: "0-add" },
+ { text: "1-add" },
+ { text: "2-add" },
+ { text: "3-add" },
+ { text: "4-add" },
+ { text: "5-add" },
+ ],
+ "Forward range selection"
+ );
+
+ widget.addItems(3, ["11-add"]);
+ assertState(
+ [
+ { text: "6-add" },
+ { text: "7-add" },
+ { text: "8-add", selected: true },
+ { text: "11-add" },
+ { text: "9-add", selected: true },
+ { text: "10-add", selected: true, focused: true },
+ { text: "0-add" },
+ { text: "1-add" },
+ { text: "2-add" },
+ { text: "3-add" },
+ { text: "4-add" },
+ { text: "5-add" },
+ ],
+ "Forward range selection with one gap"
+ );
+
+ widget.addItems(5, ["12-add", "13-add"]);
+ assertState(
+ [
+ { text: "6-add" },
+ { text: "7-add" },
+ { text: "8-add", selected: true },
+ { text: "11-add" },
+ { text: "9-add", selected: true },
+ { text: "12-add" },
+ { text: "13-add" },
+ { text: "10-add", selected: true, focused: true },
+ { text: "0-add" },
+ { text: "1-add" },
+ { text: "2-add" },
+ { text: "3-add" },
+ { text: "4-add" },
+ { text: "5-add" },
+ ],
+ "Forward range selection with two gaps"
+ );
+
+ // Continuing the shift selection will fill in the holes.
+ EventUtils.synthesizeKey("KEY_ArrowLeft", { shiftKey: true }, win);
+ assertState(
+ [
+ { text: "6-add" },
+ { text: "7-add" },
+ { text: "8-add", selected: true },
+ { text: "11-add", selected: true },
+ { text: "9-add", selected: true },
+ { text: "12-add", selected: true },
+ { text: "13-add", selected: true },
+ { text: "10-add", selected: true },
+ { text: "0-add", selected: true, focused: true },
+ { text: "1-add" },
+ { text: "2-add" },
+ { text: "3-add" },
+ { text: "4-add" },
+ { text: "5-add" },
+ ],
+ "Extended range forward with no gaps"
+ );
+
+ // With multi-selection via toggling.
+ EventUtils.synthesizeKey("KEY_ArrowLeft", { ctrlKey: true }, win);
+ EventUtils.synthesizeKey("KEY_ArrowLeft", { ctrlKey: true }, win);
+ EventUtils.synthesizeKey(" ", { ctrlKey: true }, win);
+ assertState(
+ [
+ { text: "6-add" },
+ { text: "7-add" },
+ { text: "8-add", selected: true },
+ { text: "11-add", selected: true },
+ { text: "9-add", selected: true },
+ { text: "12-add", selected: true },
+ { text: "13-add", selected: true },
+ { text: "10-add", selected: true },
+ { text: "0-add", selected: true },
+ { text: "1-add" },
+ { text: "2-add", selected: true, focused: true },
+ { text: "3-add" },
+ { text: "4-add" },
+ { text: "5-add" },
+ ],
+ "Selected 2-add"
+ );
+ EventUtils.synthesizeKey("KEY_ArrowRight", { ctrlKey: true }, win);
+ EventUtils.synthesizeKey("KEY_ArrowRight", { ctrlKey: true }, win);
+ EventUtils.synthesizeKey("KEY_ArrowRight", { ctrlKey: true }, win);
+ EventUtils.synthesizeKey("KEY_ArrowRight", { ctrlKey: true }, win);
+ EventUtils.synthesizeKey(" ", { ctrlKey: true }, win);
+ assertState(
+ [
+ { text: "6-add" },
+ { text: "7-add" },
+ { text: "8-add", selected: true },
+ { text: "11-add", selected: true },
+ { text: "9-add", selected: true },
+ { text: "12-add", selected: true },
+ { text: "13-add", focused: true },
+ { text: "10-add", selected: true },
+ { text: "0-add", selected: true },
+ { text: "1-add" },
+ { text: "2-add", selected: true },
+ { text: "3-add" },
+ { text: "4-add" },
+ { text: "5-add" },
+ ],
+ "De-selected 13-add"
+ );
+
+ widget.addItems(6, ["14-add", "15-add"]);
+ assertState(
+ [
+ { text: "6-add" },
+ { text: "7-add" },
+ { text: "8-add", selected: true },
+ { text: "11-add", selected: true },
+ { text: "9-add", selected: true },
+ { text: "12-add", selected: true },
+ { text: "14-add" },
+ { text: "15-add" },
+ { text: "13-add", focused: true },
+ { text: "10-add", selected: true },
+ { text: "0-add", selected: true },
+ { text: "1-add" },
+ { text: "2-add", selected: true },
+ { text: "3-add" },
+ { text: "4-add" },
+ { text: "5-add" },
+ ],
+ "Same selected items"
+ );
+
+ widget.addItems(3, ["16-add"]);
+ assertState(
+ [
+ { text: "6-add" },
+ { text: "7-add" },
+ { text: "8-add", selected: true },
+ { text: "16-add" },
+ { text: "11-add", selected: true },
+ { text: "9-add", selected: true },
+ { text: "12-add", selected: true },
+ { text: "14-add" },
+ { text: "15-add" },
+ { text: "13-add", focused: true },
+ { text: "10-add", selected: true },
+ { text: "0-add", selected: true },
+ { text: "1-add" },
+ { text: "2-add", selected: true },
+ { text: "3-add" },
+ { text: "4-add" },
+ { text: "5-add" },
+ ],
+ "Same selected items"
+ );
+
+ // With select-all
+ selectAllShortcut();
+ assertState(
+ [
+ { text: "6-add", selected: true },
+ { text: "7-add", selected: true },
+ { text: "8-add", selected: true },
+ { text: "16-add", selected: true },
+ { text: "11-add", selected: true },
+ { text: "9-add", selected: true },
+ { text: "12-add", selected: true },
+ { text: "14-add", selected: true },
+ { text: "15-add", selected: true },
+ { text: "13-add", selected: true, focused: true },
+ { text: "10-add", selected: true },
+ { text: "0-add", selected: true },
+ { text: "1-add", selected: true },
+ { text: "2-add", selected: true },
+ { text: "3-add", selected: true },
+ { text: "4-add", selected: true },
+ { text: "5-add", selected: true },
+ ],
+ "All items selected"
+ );
+
+ // Added items do not become selected.
+ widget.addItems(4, ["17-add", "18-add"]);
+ assertState(
+ [
+ { text: "6-add", selected: true },
+ { text: "7-add", selected: true },
+ { text: "8-add", selected: true },
+ { text: "16-add", selected: true },
+ { text: "17-add" },
+ { text: "18-add" },
+ { text: "11-add", selected: true },
+ { text: "9-add", selected: true },
+ { text: "12-add", selected: true },
+ { text: "14-add", selected: true },
+ { text: "15-add", selected: true },
+ { text: "13-add", selected: true, focused: true },
+ { text: "10-add", selected: true },
+ { text: "0-add", selected: true },
+ { text: "1-add", selected: true },
+ { text: "2-add", selected: true },
+ { text: "3-add", selected: true },
+ { text: "4-add", selected: true },
+ { text: "5-add", selected: true },
+ ],
+ "Added items not selected"
+ );
+
+ // Added items will be selected if we select-all again.
+ selectAllShortcut();
+ assertState(
+ [
+ { text: "6-add", selected: true },
+ { text: "7-add", selected: true },
+ { text: "8-add", selected: true },
+ { text: "16-add", selected: true },
+ { text: "17-add", selected: true },
+ { text: "18-add", selected: true },
+ { text: "11-add", selected: true },
+ { text: "9-add", selected: true },
+ { text: "12-add", selected: true },
+ { text: "14-add", selected: true },
+ { text: "15-add", selected: true },
+ { text: "13-add", selected: true, focused: true },
+ { text: "10-add", selected: true },
+ { text: "0-add", selected: true },
+ { text: "1-add", selected: true },
+ { text: "2-add", selected: true },
+ { text: "3-add", selected: true },
+ { text: "4-add", selected: true },
+ { text: "5-add", selected: true },
+ ],
+ "All items selected"
+ );
+ }
+});
+
+/**
+ * Test that pressing a key on a non-empty widget that has focus on itself will
+ * move to the expected index.
+ *
+ * @param {object} initialState - The initial state of the widget to set up.
+ * @param {string} initialState.model - The selection model to use.
+ * @param {string} initialState.direction - The layout direction of the widget.
+ * @param {number} initialState.numItems - The number of items in the widget.
+ * @param {Function} [initialState.scroll] - A method to call to scroll the
+ * widget.
+ * @param {string} key - The key to press once the widget is set up.
+ * @param {number} index - The expected index for the item that will receive
+ * focus after the key press.
+ */
+function subtest_keypress_on_focused_widget(initialState, key, index) {
+ let { model, direction, numItems, scroll } = initialState;
+ for (let ctrlKey of [false, true]) {
+ for (let shiftKey of [false, true]) {
+ info(
+ `Adding items to empty ${direction} widget and then pressing ${
+ ctrlKey ? "Ctrl+" : ""
+ }${shiftKey ? "Shift+" : ""}${key}`
+ );
+ reset({ model, direction });
+
+ stepFocus(true, { element: widget }, "Move focus onto empty widget");
+ widget.addItems(
+ 0,
+ range(0, numItems).map(i => `add-${i}`)
+ );
+ scroll?.();
+
+ assertFocus(
+ { element: widget },
+ "Focus remains on the widget after adding items"
+ );
+ assertSelection([], "No items are selected yet");
+
+ EventUtils.synthesizeKey(key, { ctrlKey, shiftKey }, win);
+ if (
+ (ctrlKey && shiftKey) ||
+ (model == "browse" && shiftKey) ||
+ (model == "focus" && (ctrlKey || shiftKey))
+ ) {
+ // Does nothing.
+ assertFocus({ element: widget }, "Focus remains on widget");
+ assertSelection([], "No change in selection");
+ continue;
+ }
+
+ assertFocus({ index }, `Focus moves to ${index} after ${key}`);
+ if (ctrlKey) {
+ assertSelection([], `No selection if pressing Ctrl+${key}`);
+ } else if (shiftKey) {
+ assertSelection(
+ range(0, index + 1),
+ `Range selection from 0 to ${index} if pressing Shift+${key}`
+ );
+ } else {
+ assertSelection([index], `Item selected after ${key}`);
+ }
+ }
+ }
+}
+
+// If items are added to an empty widget that has focus, nothing happens
+// initially. Arrow keys will focus the first item.
+add_task(function test_add_items_to_empty_with_focus() {
+ for (let model of selectionModels) {
+ // Step navigation always takes us to the first item.
+ subtest_keypress_on_focused_widget(
+ { model, direction: "top-to-bottom", numItems: 3 },
+ "KEY_ArrowUp",
+ 0
+ );
+ subtest_keypress_on_focused_widget(
+ { model, direction: "top-to-bottom", numItems: 3 },
+ "KEY_ArrowDown",
+ 0
+ );
+ subtest_keypress_on_focused_widget(
+ { model, direction: "right-to-left", numItems: 3 },
+ "KEY_ArrowRight",
+ 0
+ );
+ subtest_keypress_on_focused_widget(
+ { model, direction: "right-to-left", numItems: 3 },
+ "KEY_ArrowLeft",
+ 0
+ );
+ subtest_keypress_on_focused_widget(
+ { model, direction: "left-to-right", numItems: 3 },
+ "KEY_ArrowLeft",
+ 0
+ );
+ subtest_keypress_on_focused_widget(
+ { model, direction: "left-to-right", numItems: 3 },
+ "KEY_ArrowRight",
+ 0
+ );
+ // Home also takes us to the first item.
+ subtest_keypress_on_focused_widget(
+ { model, direction: "top-to-bottom", numItems: 3 },
+ "KEY_Home",
+ 0
+ );
+ subtest_keypress_on_focused_widget(
+ { model, direction: "right-to-left", numItems: 3 },
+ "KEY_Home",
+ 0
+ );
+ subtest_keypress_on_focused_widget(
+ { model, direction: "left-to-right", numItems: 3 },
+ "KEY_Home",
+ 0
+ );
+ // End takes us to the last item.
+ subtest_keypress_on_focused_widget(
+ { model, direction: "top-to-bottom", numItems: 3 },
+ "KEY_End",
+ 2
+ );
+ subtest_keypress_on_focused_widget(
+ { model, direction: "right-to-left", numItems: 3 },
+ "KEY_End",
+ 2
+ );
+ subtest_keypress_on_focused_widget(
+ { model, direction: "left-to-right", numItems: 3 },
+ "KEY_End",
+ 2
+ );
+ // PageUp and PageDown take us to the start or end of the visible page.
+ subtest_keypress_on_focused_widget(
+ { model, direction: "top-to-bottom", numItems: 3 },
+ "KEY_PageUp",
+ 0
+ );
+ subtest_keypress_on_focused_widget(
+ { model, direction: "top-to-bottom", numItems: 3 },
+ "KEY_PageDown",
+ 2
+ );
+ subtest_keypress_on_focused_widget(
+ {
+ model,
+ direction: "top-to-bottom",
+ numItems: 30,
+ scroll: () => {
+ widget.scrollTop = 270;
+ },
+ },
+ "KEY_PageUp",
+ 9
+ );
+ subtest_keypress_on_focused_widget(
+ {
+ model,
+ direction: "top-to-bottom",
+ numItems: 30,
+ scroll: () => {
+ widget.scrollTop = 60;
+ },
+ },
+ "KEY_PageDown",
+ 21
+ );
+
+ // Arrow keys in other directions do nothing.
+ reset({ model, direction: "top-to-bottom" });
+ stepFocus(true, { element: widget }, "Move focus onto empty widget");
+ widget.addItems(0, ["First", "Second"]);
+ for (let key of ["KEY_ArrowRight", "KEY_ArrowLeft"]) {
+ EventUtils.synthesizeKey(key, {}, win);
+ assertFocus({ element: widget }, `Focus remains on widget after ${key}`);
+ assertSelection([], `No items become selected after ${key}`);
+ }
+
+ reset({ model, direction: "right-to-left" });
+ stepFocus(true, { element: widget }, "Move focus onto empty widget");
+ widget.addItems(0, ["First", "Second"]);
+ for (let key of ["KEY_ArrowUp", "KEY_ArrowDown"]) {
+ EventUtils.synthesizeKey(key, {}, win);
+ assertFocus({ element: widget }, `Focus remains on widget after ${key}`);
+ assertSelection([], `No items become selected after ${key}`);
+ }
+
+ // Pressing Space does nothing.
+ reset({ model });
+ stepFocus(true, { element: widget }, "Move focus onto empty widget");
+ widget.addItems(0, ["First", "Second"]);
+ for (let ctrlKey of [false, true]) {
+ for (let shiftKey of [false, true]) {
+ info(
+ `Pressing ${ctrlKey ? "Ctrl+" : ""}${shiftKey ? "Shift+" : ""}Space`
+ );
+ EventUtils.synthesizeKey(" ", {}, win);
+ assertFocus({ element: widget }, "Focus remains on widget after Space");
+ assertSelection([], "No items become selected after Space");
+ }
+ }
+
+ // Selecting all
+ reset({ model });
+ stepFocus(true, { element: widget }, "Move focus onto empty widget");
+ widget.addItems(0, ["First", "Second", "Third"]);
+
+ selectAllShortcut();
+ assertFocus({ element: widget }, "Focus remains on the widget");
+ if (model == "browse-multi") {
+ assertSelection([0, 1, 2], "All items selected");
+ } else {
+ assertSelection([], "still no selection");
+ }
+
+ // Adding and then removing items does not set focus.
+ reset({ model });
+ stepFocus(true, { element: widget }, "Move focus onto empty widget");
+ widget.addItems(0, ["First", "Second", "Third", "Fourth"]);
+ widget.removeItems(2, 2);
+ assertState(
+ [{ text: "First" }, { text: "Second" }],
+ "No item focused or selected"
+ );
+ assertFocus({ element: widget }, "Focus remains on the widget");
+ widget.removeItems(0, 1);
+ assertState([{ text: "Second" }], "No item focused or selected");
+ assertFocus({ element: widget }, "Focus remains on the widget");
+
+ // Moving items does not set focus.
+ reset({ model });
+ stepFocus(true, { element: widget }, "Move focus onto empty widget");
+ widget.addItems(0, ["First", "Second", "Third", "Fourth"]);
+ widget.moveItems(1, 0, 2, false);
+ assertState(
+ [
+ { text: "Second" },
+ { text: "Third" },
+ { text: "First" },
+ { text: "Fourth" },
+ ],
+ "No item focused or selected"
+ );
+ assertFocus({ element: widget }, "Focus remains on the widget");
+ widget.moveItems(0, 1, 3, true);
+ assertState(
+ [
+ { text: "Fourth" },
+ { text: "Second" },
+ { text: "Third" },
+ { text: "First" },
+ ],
+ "No item focused or selected"
+ );
+ assertFocus({ element: widget }, "Focus remains on the widget");
+
+ // This does not effect clicking.
+ // NOTE: case where widget does not initially have focus on clicking is
+ // handled by test_initial_no_select_focus
+ for (let ctrlKey of [false, true]) {
+ for (let shiftKey of [false, true]) {
+ info(
+ `Adding items to empty focused widget and then ${
+ ctrlKey ? "Ctrl+" : ""
+ }${shiftKey ? "Shift+" : ""}Click`
+ );
+ reset({ model });
+ stepFocus(true, { element: widget }, "Move focus onto empty widget");
+ widget.addItems(0, ["First", "Second", "Third"]);
+
+ // Clicking empty space does nothing.
+ clickWidgetEmptySpace({ ctrlKey, shiftKey });
+ assertFocus({ element: widget }, "Focus remains on widget");
+ assertSelection([], "No item selected");
+
+ // Clicking an item can change focus and selection.
+ clickWidgetItem(1, { ctrlKey, shiftKey });
+ if (
+ (ctrlKey && shiftKey) ||
+ ((model == "focus" || model == "browse") && (ctrlKey || shiftKey))
+ ) {
+ assertFocus({ element: widget }, "Focus remains on widget");
+ assertSelection([], "No selection");
+ continue;
+ }
+ assertFocus({ index: 1 }, "Focus moves to second item");
+ if (shiftKey) {
+ assertSelection([0, 1], "First and second item selected");
+ } else {
+ assertSelection([1], "Second item selected");
+ }
+ }
+ }
+ }
+});
+
+// Removing items from the widget with existing items, may change focus or
+// selection if the corresponding item was removed.
+add_task(function test_remove_items_nonempty() {
+ for (let model of selectionModels) {
+ reset({ model, direction: "right-to-left" });
+
+ widget.addItems(0, ["0-add", "1-add", "2-add", "3-add", "4-add", "5-add"]);
+ assertState(
+ [
+ { text: "0-add" },
+ { text: "1-add" },
+ { text: "2-add" },
+ { text: "3-add" },
+ { text: "4-add" },
+ { text: "5-add" },
+ ],
+ "No initial focus or selection"
+ );
+
+ clickWidgetItem(2, {});
+ assertState(
+ [
+ { text: "0-add" },
+ { text: "1-add" },
+ { text: "2-add", selected: true, focused: true },
+ { text: "3-add" },
+ { text: "4-add" },
+ { text: "5-add" },
+ ],
+ "2-add focused and selected"
+ );
+
+ // Remove one after.
+ widget.removeItems(3, 1);
+ assertState(
+ [
+ { text: "0-add" },
+ { text: "1-add" },
+ { text: "2-add", selected: true, focused: true },
+ { text: "4-add" },
+ { text: "5-add" },
+ ],
+ "2-add still focused and selected"
+ );
+
+ // Remove one before.
+ widget.removeItems(0, 1);
+ assertState(
+ [
+ { text: "1-add" },
+ { text: "2-add", selected: true, focused: true },
+ { text: "4-add" },
+ { text: "5-add" },
+ ],
+ "2-add still focused and selected"
+ );
+
+ widget.addItems(0, ["6-add", "7-add"]);
+ assertState(
+ [
+ { text: "6-add" },
+ { text: "7-add" },
+ { text: "1-add" },
+ { text: "2-add", selected: true, focused: true },
+ { text: "4-add" },
+ { text: "5-add" },
+ ],
+ "2-add still focused and selected"
+ );
+
+ // Remove several before.
+ widget.removeItems(1, 2);
+ assertState(
+ [
+ { text: "6-add" },
+ { text: "2-add", selected: true, focused: true },
+ { text: "4-add" },
+ { text: "5-add" },
+ ],
+ "2-add still focused and selected"
+ );
+
+ // Remove selected and focused. Focus should move to the next item.
+ widget.removeItems(1, 1);
+ assertState(
+ [
+ { text: "6-add" },
+ { text: "4-add", selected: true, focused: true },
+ { text: "5-add" },
+ ],
+ "Selection and focus move to 4-add"
+ );
+
+ widget.addItems(0, ["8-add"]);
+ widget.addItems(3, ["9-add", "10-add"]);
+ assertState(
+ [
+ { text: "8-add" },
+ { text: "6-add" },
+ { text: "4-add", selected: true, focused: true },
+ { text: "9-add" },
+ { text: "10-add" },
+ { text: "5-add" },
+ ],
+ "Selection and focus still on 4-add"
+ );
+
+ // Remove selected and focused, not at boundary.
+ widget.removeItems(1, 3);
+ assertState(
+ [
+ { text: "8-add" },
+ { text: "10-add", selected: true, focused: true },
+ { text: "5-add" },
+ ],
+ "Selection and focus move to 10-add"
+ );
+
+ // Remove last item whilst it has focus. Focus should move to the new last
+ // item.
+ EventUtils.synthesizeKey("KEY_ArrowLeft", {}, win);
+ assertState(
+ [
+ { text: "8-add" },
+ { text: "10-add" },
+ { text: "5-add", selected: true, focused: true },
+ ],
+ "Last item is focused and selected"
+ );
+
+ widget.removeItems(2, 1);
+ assertState(
+ [{ text: "8-add" }, { text: "10-add", selected: true, focused: true }],
+ "New last item is focused and selected"
+ );
+
+ // Delete focused whilst outside widget.
+ widget.addItems(2, ["11-add"]);
+ assertState(
+ [
+ { text: "8-add" },
+ { text: "10-add", selected: true, focused: true },
+ { text: "11-add" },
+ ],
+ "10-add is focused and selected"
+ );
+ stepFocus(false, { element: before });
+
+ widget.removeItems(1, 1);
+ assertFocus({ element: before }, "Focus remains outside widget");
+ assertState(
+ [{ text: "8-add" }, { text: "11-add", selected: true }],
+ "11-add becomes selected"
+ );
+
+ stepFocus(true, { index: 1 }, "11-add becomes focused");
+ assertState(
+ [{ text: "8-add" }, { text: "11-add", selected: true, focused: true }],
+ "11-add is selected"
+ );
+
+ // With focus separate from selected.
+ if (model == "focus") {
+ continue;
+ }
+
+ // Move selection to be before focus.
+ widget.addItems(2, ["12-add", "13-add", "14-add"]);
+ assertState(
+ [
+ { text: "8-add" },
+ { text: "11-add", selected: true, focused: true },
+ { text: "12-add" },
+ { text: "13-add" },
+ { text: "14-add" },
+ ],
+ "11-add is selected and focused"
+ );
+
+ EventUtils.synthesizeKey("KEY_ArrowLeft", { ctrlKey: true }, win);
+ assertState(
+ [
+ { text: "8-add" },
+ { text: "11-add", selected: true },
+ { text: "12-add", focused: true },
+ { text: "13-add" },
+ { text: "14-add" },
+ ],
+ "Selection before focus"
+ );
+
+ // Remove focused, but not selected.
+ widget.removeItems(2, 1);
+ assertState(
+ [
+ { text: "8-add" },
+ { text: "11-add", selected: true },
+ { text: "13-add", focused: true },
+ { text: "14-add" },
+ ],
+ "Focus moves to 13-add, but selection is the same"
+ );
+
+ // Remove focused and selected.
+ widget.removeItems(1, 2);
+ assertState(
+ [{ text: "8-add" }, { text: "14-add", selected: true, focused: true }],
+ "Focus moves to 14-add and becomes selected"
+ );
+
+ // Restore selection before focus.
+ widget.addItems(0, ["15-add"]);
+ assertState(
+ [
+ { text: "15-add" },
+ { text: "8-add" },
+ { text: "14-add", selected: true, focused: true },
+ ],
+ "14-add has focus and selection"
+ );
+ EventUtils.synthesizeKey("KEY_ArrowRight", {}, win);
+ assertState(
+ [
+ { text: "15-add" },
+ { text: "8-add", selected: true, focused: true },
+ { text: "14-add" },
+ ],
+ "8-add is focused and selected"
+ );
+ EventUtils.synthesizeKey("KEY_ArrowLeft", { ctrlKey: true }, win);
+ assertState(
+ [
+ { text: "15-add" },
+ { text: "8-add", selected: true },
+ { text: "14-add", focused: true },
+ ],
+ "Selection before focus again"
+ );
+
+ // Remove selected, but not focused.
+ widget.removeItems(1, 1);
+ assertState(
+ [{ text: "15-add" }, { text: "14-add", focused: true }],
+ "14-add still has focus, but selection is lost"
+ );
+
+ // Move selection to be after focus.
+ widget.addItems(1, ["16-add", "17-add"]);
+ widget.addItems(4, ["18-add"]);
+ assertState(
+ [
+ { text: "15-add" },
+ { text: "16-add" },
+ { text: "17-add" },
+ { text: "14-add", focused: true },
+ { text: "18-add" },
+ ],
+ "Still no selection"
+ );
+ // Select focused.
+ EventUtils.synthesizeKey(" ", {}, win);
+ assertFocus({ index: 3 }, "14-add has focus");
+ assertSelection([3], "14-add is selected");
+ assertState(
+ [
+ { text: "15-add" },
+ { text: "16-add" },
+ { text: "17-add" },
+ { text: "14-add", selected: true, focused: true },
+ { text: "18-add" },
+ ],
+ "14-add is selected and focused"
+ );
+ // Move focus.
+ EventUtils.synthesizeKey("KEY_ArrowRight", { ctrlKey: true }, win);
+ EventUtils.synthesizeKey("KEY_ArrowRight", { ctrlKey: true }, win);
+ assertState(
+ [
+ { text: "15-add" },
+ { text: "16-add", focused: true },
+ { text: "17-add" },
+ { text: "14-add", selected: true },
+ { text: "18-add" },
+ ],
+ "Selection after focus"
+ );
+
+ // Remove focused, but not selected.
+ widget.removeItems(1, 1);
+ assertState(
+ [
+ { text: "15-add" },
+ { text: "17-add", focused: true },
+ { text: "14-add", selected: true },
+ { text: "18-add" },
+ ],
+ "Focus moves to 17-add, selection stays on 14-add"
+ );
+
+ // Remove focused and selected.
+ widget.removeItems(1, 2);
+ assertState(
+ [{ text: "15-add" }, { text: "18-add", selected: true, focused: true }],
+ "Focus and selection moves to 18-add"
+ );
+
+ // Restore selection after focus.
+ widget.addItems(2, ["19-add", "20-add"]);
+ assertState(
+ [
+ { text: "15-add" },
+ { text: "18-add", selected: true, focused: true },
+ { text: "19-add" },
+ { text: "20-add" },
+ ],
+ "Still no selection"
+ );
+ EventUtils.synthesizeKey("KEY_ArrowLeft", {}, win);
+ assertState(
+ [
+ { text: "15-add" },
+ { text: "18-add" },
+ { text: "19-add", selected: true, focused: true },
+ { text: "20-add" },
+ ],
+ "19-add focused and selected"
+ );
+ EventUtils.synthesizeKey("KEY_ArrowRight", { ctrlKey: true }, win);
+ assertState(
+ [
+ { text: "15-add" },
+ { text: "18-add", focused: true },
+ { text: "19-add", selected: true },
+ { text: "20-add" },
+ ],
+ "Selection after focus again"
+ );
+
+ // Remove selected, but not focused.
+ widget.removeItems(2, 2);
+ assertState(
+ [{ text: "15-add" }, { text: "18-add", focused: true }],
+ "18-add still has focus, but selection is lost"
+ );
+
+ // With multi-selection
+ if (model == "browse") {
+ continue;
+ }
+
+ widget.addItems(0, ["21-add", "22-add", "23-add"]);
+ assertState(
+ [
+ { text: "21-add" },
+ { text: "22-add" },
+ { text: "23-add" },
+ { text: "15-add" },
+ { text: "18-add", focused: true },
+ ],
+ "18-add focused, no selection yet"
+ );
+ widget.addItems(5, [
+ "24-add",
+ "25-add",
+ "26-add",
+ "27-add",
+ "28-add",
+ "29-add",
+ "30-add",
+ "31-add",
+ ]);
+ assertState(
+ [
+ { text: "21-add" },
+ { text: "22-add" },
+ { text: "23-add" },
+ { text: "15-add" },
+ { text: "18-add", focused: true },
+ { text: "24-add" },
+ { text: "25-add" },
+ { text: "26-add" },
+ { text: "27-add" },
+ { text: "28-add" },
+ { text: "29-add" },
+ { text: "30-add" },
+ { text: "31-add" },
+ ],
+ "18-add focused, no selection yet"
+ );
+
+ EventUtils.synthesizeKey("KEY_ArrowLeft", { shiftKey: true }, win);
+ EventUtils.synthesizeKey("KEY_ArrowLeft", { shiftKey: true }, win);
+ EventUtils.synthesizeKey("KEY_ArrowLeft", { shiftKey: true }, win);
+
+ assertState(
+ [
+ { text: "21-add" },
+ { text: "22-add" },
+ { text: "23-add" },
+ { text: "15-add" },
+ { text: "18-add", selected: true },
+ { text: "24-add", selected: true },
+ { text: "25-add", selected: true },
+ { text: "26-add", selected: true, focused: true },
+ { text: "27-add" },
+ { text: "28-add" },
+ { text: "29-add" },
+ { text: "30-add" },
+ { text: "31-add" },
+ ],
+ "Forward range selection from 18-add to 26-add"
+ );
+
+ // Delete after the selection range
+ widget.removeItems(10, 1);
+ assertState(
+ [
+ { text: "21-add" },
+ { text: "22-add" },
+ { text: "23-add" },
+ { text: "15-add" },
+ { text: "18-add", selected: true },
+ { text: "24-add", selected: true },
+ { text: "25-add", selected: true },
+ { text: "26-add", selected: true, focused: true },
+ { text: "27-add" },
+ { text: "28-add" },
+ { text: "30-add" },
+ { text: "31-add" },
+ ],
+ "Same range selection"
+ );
+
+ // Delete before the selection range.
+ widget.removeItems(1, 1);
+ assertState(
+ [
+ { text: "21-add" },
+ { text: "23-add" },
+ { text: "15-add" },
+ { text: "18-add", selected: true },
+ { text: "24-add", selected: true },
+ { text: "25-add", selected: true },
+ { text: "26-add", selected: true, focused: true },
+ { text: "27-add" },
+ { text: "28-add" },
+ { text: "30-add" },
+ { text: "31-add" },
+ ],
+ "Same range selection"
+ );
+
+ // Delete the start of the selection range.
+ widget.removeItems(2, 3);
+ assertState(
+ [
+ { text: "21-add" },
+ { text: "23-add" },
+ { text: "25-add", selected: true },
+ { text: "26-add", selected: true, focused: true },
+ { text: "27-add" },
+ { text: "28-add" },
+ { text: "30-add" },
+ { text: "31-add" },
+ ],
+ "Selection range from 25-add to 26-add"
+ );
+
+ // Selection pivot is now around 25-add.
+ EventUtils.synthesizeKey("KEY_Home", { shiftKey: true }, win);
+ assertState(
+ [
+ { text: "21-add", selected: true, focused: true },
+ { text: "23-add", selected: true },
+ { text: "25-add", selected: true },
+ { text: "26-add" },
+ { text: "27-add" },
+ { text: "28-add" },
+ { text: "30-add" },
+ { text: "31-add" },
+ ],
+ "Selection range from 25-add to 21-add"
+ );
+ EventUtils.synthesizeKey("KEY_End", { shiftKey: true }, win);
+ assertState(
+ [
+ { text: "21-add" },
+ { text: "23-add" },
+ { text: "25-add", selected: true },
+ { text: "26-add", selected: true },
+ { text: "27-add", selected: true },
+ { text: "28-add", selected: true },
+ { text: "30-add", selected: true },
+ { text: "31-add", selected: true, focused: true },
+ ],
+ "Selection range from 25-add to 31-add"
+ );
+ EventUtils.synthesizeKey("KEY_ArrowRight", { shiftKey: true }, win);
+ EventUtils.synthesizeKey("KEY_ArrowRight", { shiftKey: true }, win);
+ assertState(
+ [
+ { text: "21-add" },
+ { text: "23-add" },
+ { text: "25-add", selected: true },
+ { text: "26-add", selected: true },
+ { text: "27-add", selected: true },
+ { text: "28-add", selected: true, focused: true },
+ { text: "30-add" },
+ { text: "31-add" },
+ ],
+ "Selection range from 25-add to 28-add"
+ );
+
+ // Delete the end of the selection.
+ // As a special case, the focus moves to the end of the selection, rather
+ // than to the next item.
+ widget.removeItems(4, 2);
+ assertState(
+ [
+ { text: "21-add" },
+ { text: "23-add" },
+ { text: "25-add", selected: true },
+ { text: "26-add", selected: true, focused: true },
+ { text: "30-add" },
+ { text: "31-add" },
+ ],
+ "Selection range from 25-add to 26-add"
+ );
+
+ // Do same with a gap.
+ EventUtils.synthesizeKey("KEY_ArrowLeft", { shiftKey: true }, win);
+ assertState(
+ [
+ { text: "21-add" },
+ { text: "23-add" },
+ { text: "25-add", selected: true },
+ { text: "26-add", selected: true },
+ { text: "30-add", selected: true, focused: true },
+ { text: "31-add" },
+ ],
+ "Continue selection range from 25-add to 30-add"
+ );
+
+ widget.addItems(3, ["32-add"]);
+ assertState(
+ [
+ { text: "21-add" },
+ { text: "23-add" },
+ { text: "25-add", selected: true },
+ { text: "32-add" },
+ { text: "26-add", selected: true },
+ { text: "30-add", selected: true, focused: true },
+ { text: "31-add" },
+ ],
+ "Selection range from 25-add to 30-add with gap"
+ );
+
+ widget.removeItems(5, 1);
+ assertState(
+ [
+ { text: "21-add" },
+ { text: "23-add" },
+ { text: "25-add", selected: true },
+ { text: "32-add" },
+ { text: "26-add", selected: true, focused: true },
+ { text: "31-add" },
+ ],
+ "Focus moves to the end of the range, after the gap"
+ );
+
+ // Do the same with a gap and all items after the gap are removed.
+ widget.addItems(6, ["33-add", "34-add"]);
+ assertState(
+ [
+ { text: "21-add" },
+ { text: "23-add" },
+ { text: "25-add", selected: true },
+ { text: "32-add" },
+ { text: "26-add", selected: true, focused: true },
+ { text: "31-add" },
+ { text: "33-add" },
+ { text: "34-add" },
+ ],
+ "Added 33-add and 34-add"
+ );
+
+ clickWidgetItem(5, { shiftKey: true });
+ assertState(
+ [
+ { text: "21-add" },
+ { text: "23-add" },
+ { text: "25-add", selected: true },
+ { text: "32-add", selected: true },
+ { text: "26-add", selected: true },
+ { text: "31-add", selected: true, focused: true },
+ { text: "33-add" },
+ { text: "34-add" },
+ ],
+ "Selection extended to 31-add and gap filled"
+ );
+
+ widget.addItems(4, ["35-add", "36-add", "37-add"]);
+ assertState(
+ [
+ { text: "21-add" },
+ { text: "23-add" },
+ { text: "25-add", selected: true },
+ { text: "32-add", selected: true },
+ { text: "35-add" },
+ { text: "36-add" },
+ { text: "37-add" },
+ { text: "26-add", selected: true },
+ { text: "31-add", selected: true, focused: true },
+ { text: "33-add" },
+ { text: "34-add" },
+ ],
+ "Selection from 25-add to 31-add with gap"
+ );
+
+ widget.removeItems(6, 3);
+ assertState(
+ [
+ { text: "21-add" },
+ { text: "23-add" },
+ { text: "25-add", selected: true },
+ { text: "32-add", selected: true, focused: true },
+ { text: "35-add" },
+ { text: "36-add" },
+ { text: "33-add" },
+ { text: "34-add" },
+ ],
+ "Focus jumps gap to what is left of the selection range"
+ );
+
+ // Same, with entire gap also removed.
+ clickWidgetItem(6, { shiftKey: true });
+ widget.addItems(5, ["38-add"]);
+ assertState(
+ [
+ { text: "21-add" },
+ { text: "23-add" },
+ { text: "25-add", selected: true },
+ { text: "32-add", selected: true },
+ { text: "35-add", selected: true },
+ { text: "38-add" },
+ { text: "36-add", selected: true },
+ { text: "33-add", selected: true, focused: true },
+ { text: "34-add" },
+ ],
+ "Selection from 25-add to 33-add with gap"
+ );
+
+ widget.removeItems(4, 4);
+ assertState(
+ [
+ { text: "21-add" },
+ { text: "23-add" },
+ { text: "25-add", selected: true },
+ { text: "32-add", selected: true, focused: true },
+ { text: "34-add" },
+ ],
+ "Focus moves to end of what is left of the selection range"
+ );
+
+ // Test deleting the end of the selection with focus in a gap.
+ // We don't expect to follow the special treatment because the user has
+ // explicitly moved the focus "outside" of the selected range.
+ widget.addItems(3, ["39-add"]);
+ EventUtils.synthesizeKey("KEY_ArrowRight", { ctrlKey: true }, win);
+ assertState(
+ [
+ { text: "21-add" },
+ { text: "23-add" },
+ { text: "25-add", selected: true },
+ { text: "39-add", focused: true },
+ { text: "32-add", selected: true },
+ { text: "34-add" },
+ ],
+ "Focus in range gap"
+ );
+
+ widget.removeItems(3, 2);
+ assertState(
+ [
+ { text: "21-add" },
+ { text: "23-add" },
+ { text: "25-add", selected: true },
+ { text: "34-add", focused: true },
+ ],
+ "Focus moves from gap to 34-add, outside the selection range"
+ );
+
+ // Same, but deleting the start of the range.
+ widget.addItems(4, ["40-add", "41-add", "42-add"]);
+ clickWidgetItem(5, { shiftKey: true });
+ assertState(
+ [
+ { text: "21-add" },
+ { text: "23-add" },
+ { text: "25-add", selected: true },
+ { text: "34-add", selected: true },
+ { text: "40-add", selected: true },
+ { text: "41-add", selected: true, focused: true },
+ { text: "42-add" },
+ ],
+ "Selection from 25-add to 41-add"
+ );
+
+ widget.addItems(3, ["43-add", "44-add", "45-add"]);
+ EventUtils.synthesizeKey("KEY_Home", { ctrlKey: true }, win);
+ EventUtils.synthesizeKey("KEY_ArrowLeft", { ctrlKey: true }, win);
+ EventUtils.synthesizeKey("KEY_ArrowLeft", { ctrlKey: true }, win);
+ EventUtils.synthesizeKey("KEY_ArrowLeft", { ctrlKey: true }, win);
+ assertState(
+ [
+ { text: "21-add" },
+ { text: "23-add" },
+ { text: "25-add", selected: true },
+ { text: "43-add", focused: true },
+ { text: "44-add" },
+ { text: "45-add" },
+ { text: "34-add", selected: true },
+ { text: "40-add", selected: true },
+ { text: "41-add", selected: true },
+ { text: "42-add" },
+ ],
+ "Focus in gap"
+ );
+
+ widget.removeItems(1, 3);
+ assertState(
+ [
+ { text: "21-add" },
+ { text: "44-add", focused: true },
+ { text: "45-add" },
+ { text: "34-add", selected: true },
+ { text: "40-add", selected: true },
+ { text: "41-add", selected: true },
+ { text: "42-add" },
+ ],
+ "Focus moves to next item, rather than the selection start"
+ );
+
+ // Test deleting the end of the selection with the focus towards the end of
+ // the range.
+ widget.addItems(7, ["46-add", "47-add", "48-add", "49-add", "50-add"]);
+ clickWidgetItem(7, { shiftKey: true });
+ EventUtils.synthesizeKey("KEY_ArrowRight", { ctrlKey: true }, win);
+ assertState(
+ [
+ { text: "21-add" },
+ { text: "44-add" },
+ { text: "45-add" },
+ { text: "34-add", selected: true },
+ { text: "40-add", selected: true },
+ { text: "41-add", selected: true },
+ { text: "42-add", selected: true, focused: true },
+ { text: "46-add", selected: true },
+ { text: "47-add" },
+ { text: "48-add" },
+ { text: "49-add" },
+ { text: "50-add" },
+ ],
+ "Range selection from 34-add to 46-add, with focus on 42-add"
+ );
+
+ widget.removeItems(6, 2);
+ assertState(
+ [
+ { text: "21-add" },
+ { text: "44-add" },
+ { text: "45-add" },
+ { text: "34-add", selected: true },
+ { text: "40-add", selected: true },
+ { text: "41-add", selected: true, focused: true },
+ { text: "47-add" },
+ { text: "48-add" },
+ { text: "49-add" },
+ { text: "50-add" },
+ ],
+ "Focus still moves to the end of the selection"
+ );
+
+ // Test deleting with focus after the end of the range.
+ EventUtils.synthesizeKey("KEY_ArrowLeft", { ctrlKey: true });
+ assertState(
+ [
+ { text: "21-add" },
+ { text: "44-add" },
+ { text: "45-add" },
+ { text: "34-add", selected: true },
+ { text: "40-add", selected: true },
+ { text: "41-add", selected: true },
+ { text: "47-add", focused: true },
+ { text: "48-add" },
+ { text: "49-add" },
+ { text: "50-add" },
+ ],
+ "Focus still moves to the end of the selection"
+ );
+
+ widget.removeItems(5, 2);
+ assertState(
+ [
+ { text: "21-add" },
+ { text: "44-add" },
+ { text: "45-add" },
+ { text: "34-add", selected: true },
+ { text: "40-add", selected: true },
+ { text: "48-add", focused: true },
+ { text: "49-add" },
+ { text: "50-add" },
+ ],
+ "Focus remains outside the range"
+ );
+
+ // Test deleting with focus in the middle of the range, and end of the range
+ // is not deleted.
+ EventUtils.synthesizeKey("KEY_ArrowLeft", { shiftKey: true });
+ EventUtils.synthesizeKey("KEY_ArrowRight", { ctrlKey: true });
+ EventUtils.synthesizeKey("KEY_ArrowRight", { ctrlKey: true });
+ assertState(
+ [
+ { text: "21-add" },
+ { text: "44-add" },
+ { text: "45-add" },
+ { text: "34-add", selected: true },
+ { text: "40-add", selected: true, focused: true },
+ { text: "48-add", selected: true },
+ { text: "49-add", selected: true },
+ { text: "50-add" },
+ ],
+ "Focus in the middle of the range"
+ );
+
+ widget.removeItems(4, 1);
+ assertState(
+ [
+ { text: "21-add" },
+ { text: "44-add" },
+ { text: "45-add" },
+ { text: "34-add", selected: true },
+ { text: "48-add", selected: true, focused: true },
+ { text: "49-add", selected: true },
+ { text: "50-add" },
+ ],
+ "Focus moves to next item, rather than the end of the range"
+ );
+
+ // With focus just before a gap.
+ widget.addItems(5, ["51-add", "52-add"]);
+ assertState(
+ [
+ { text: "21-add" },
+ { text: "44-add" },
+ { text: "45-add" },
+ { text: "34-add", selected: true },
+ { text: "48-add", selected: true, focused: true },
+ { text: "51-add" },
+ { text: "52-add" },
+ { text: "49-add", selected: true },
+ { text: "50-add" },
+ ],
+ "Focus just before a gap"
+ );
+
+ widget.removeItems(4, 1);
+ assertState(
+ [
+ { text: "21-add" },
+ { text: "44-add" },
+ { text: "45-add" },
+ { text: "34-add", selected: true },
+ { text: "51-add", focused: true },
+ { text: "52-add" },
+ { text: "49-add", selected: true },
+ { text: "50-add" },
+ ],
+ "Focus moves forward into the gap"
+ );
+
+ // Selection pivot is about 34-add
+ clickWidgetItem(1, { shiftKey: true });
+ assertState(
+ [
+ { text: "21-add" },
+ { text: "44-add", selected: true, focused: true },
+ { text: "45-add", selected: true },
+ { text: "34-add", selected: true },
+ { text: "51-add" },
+ { text: "52-add" },
+ { text: "49-add" },
+ { text: "50-add" },
+ ],
+ "Selection from 34-add backward to 44-add"
+ );
+ clickWidgetItem(5, { shiftKey: true });
+ assertState(
+ [
+ { text: "21-add" },
+ { text: "44-add" },
+ { text: "45-add" },
+ { text: "34-add", selected: true },
+ { text: "51-add", selected: true },
+ { text: "52-add", selected: true, focused: true },
+ { text: "49-add" },
+ { text: "50-add" },
+ ],
+ "Selection from 34-add forward to 52-add"
+ );
+
+ // Delete the whole range.
+ widget.removeItems(3, 3);
+ assertState(
+ [
+ { text: "21-add" },
+ { text: "44-add" },
+ { text: "45-add" },
+ { text: "49-add", selected: true, focused: true },
+ { text: "50-add" },
+ ],
+ "Focus and selection moves to after the selection range"
+ );
+
+ // Do the same with focus outside the range.
+ widget.addItems(5, ["53-add", "54-add", "55-add"]);
+ EventUtils.synthesizeKey("KEY_Home", {}, win);
+ EventUtils.synthesizeKey("KEY_ArrowLeft", {}, win);
+ clickWidgetItem(2, { shiftKey: true });
+ EventUtils.synthesizeKey("KEY_ArrowLeft", { ctrlKey: true }, win);
+ assertState(
+ [
+ { text: "21-add" },
+ { text: "44-add", selected: true },
+ { text: "45-add", selected: true },
+ { text: "49-add", focused: true },
+ { text: "50-add" },
+ { text: "53-add" },
+ { text: "54-add" },
+ { text: "55-add" },
+ ],
+ "Focus outside selection"
+ );
+
+ widget.removeItems(1, 2);
+ assertState(
+ [
+ { text: "21-add" },
+ { text: "49-add", focused: true },
+ { text: "50-add" },
+ { text: "53-add" },
+ { text: "54-add" },
+ { text: "55-add" },
+ ],
+ "Focus remains on same item and unselected"
+ );
+
+ // Do the same, but the focus is also removed.
+ EventUtils.synthesizeKey("KEY_ArrowLeft", { shiftKey: true }, win);
+ EventUtils.synthesizeKey("KEY_ArrowLeft", { ctrlKey: true }, win);
+ assertState(
+ [
+ { text: "21-add" },
+ { text: "49-add", selected: true },
+ { text: "50-add", selected: true },
+ { text: "53-add", focused: true },
+ { text: "54-add" },
+ { text: "55-add" },
+ ],
+ "Focus outside selection"
+ );
+
+ widget.removeItems(1, 3);
+ assertState(
+ [
+ { text: "21-add" },
+ { text: "54-add", selected: true, focused: true },
+ { text: "55-add" },
+ ],
+ "Focus and selection moves to 49-add"
+ );
+
+ // * Do the same tests but with selection travelling backwards. *
+ widget.addItems(3, [
+ "56-add",
+ "57-add",
+ "58-add",
+ "59-add",
+ "60-add",
+ "61-add",
+ "62-add",
+ "63-add",
+ "64-add",
+ "65-add",
+ "66-add",
+ ]);
+ assertState(
+ [
+ { text: "21-add" },
+ { text: "54-add", selected: true, focused: true },
+ { text: "55-add" },
+ { text: "56-add" },
+ { text: "57-add" },
+ { text: "58-add" },
+ { text: "59-add" },
+ { text: "60-add" },
+ { text: "61-add" },
+ { text: "62-add" },
+ { text: "63-add" },
+ { text: "64-add" },
+ { text: "65-add" },
+ { text: "66-add" },
+ ],
+ "Same selection and focus"
+ );
+ EventUtils.synthesizeKey("KEY_End", {}, win);
+ EventUtils.synthesizeKey("KEY_ArrowRight", {}, win);
+ EventUtils.synthesizeKey("KEY_ArrowRight", {}, win);
+ EventUtils.synthesizeKey("KEY_ArrowRight", {}, win);
+ EventUtils.synthesizeKey("KEY_ArrowRight", {}, win);
+ EventUtils.synthesizeKey("KEY_ArrowRight", { shiftKey: true }, win);
+ EventUtils.synthesizeKey("KEY_ArrowRight", { shiftKey: true }, win);
+ EventUtils.synthesizeKey("KEY_ArrowRight", { shiftKey: true }, win);
+ EventUtils.synthesizeKey("KEY_ArrowRight", { shiftKey: true }, win);
+ EventUtils.synthesizeKey("KEY_ArrowRight", { shiftKey: true }, win);
+ assertState(
+ [
+ { text: "21-add" },
+ { text: "54-add" },
+ { text: "55-add" },
+ { text: "56-add" },
+ { text: "57-add", selected: true, focused: true },
+ { text: "58-add", selected: true },
+ { text: "59-add", selected: true },
+ { text: "60-add", selected: true },
+ { text: "61-add", selected: true },
+ { text: "62-add", selected: true },
+ { text: "63-add" },
+ { text: "64-add" },
+ { text: "65-add" },
+ { text: "66-add" },
+ ],
+ "Backward range selection from 62-add to 57-add"
+ );
+
+ // Delete after the selection range
+ widget.removeItems(11, 2);
+ assertState(
+ [
+ { text: "21-add" },
+ { text: "54-add" },
+ { text: "55-add" },
+ { text: "56-add" },
+ { text: "57-add", selected: true, focused: true },
+ { text: "58-add", selected: true },
+ { text: "59-add", selected: true },
+ { text: "60-add", selected: true },
+ { text: "61-add", selected: true },
+ { text: "62-add", selected: true },
+ { text: "63-add" },
+ { text: "66-add" },
+ ],
+ "Same range selection"
+ );
+
+ // Delete before the selection range.
+ widget.removeItems(2, 2);
+ assertState(
+ [
+ { text: "21-add" },
+ { text: "54-add" },
+ { text: "57-add", selected: true, focused: true },
+ { text: "58-add", selected: true },
+ { text: "59-add", selected: true },
+ { text: "60-add", selected: true },
+ { text: "61-add", selected: true },
+ { text: "62-add", selected: true },
+ { text: "63-add" },
+ { text: "66-add" },
+ ],
+ "Same range selection"
+ );
+
+ // Delete the end of the selection range.
+ widget.removeItems(7, 1);
+ assertState(
+ [
+ { text: "21-add" },
+ { text: "54-add" },
+ { text: "57-add", selected: true, focused: true },
+ { text: "58-add", selected: true },
+ { text: "59-add", selected: true },
+ { text: "60-add", selected: true },
+ { text: "61-add", selected: true },
+ { text: "63-add" },
+ { text: "66-add" },
+ ],
+ "Selection range backwards from 61-add to 57-add"
+ );
+
+ // Selection pivot is now around 61-add.
+ EventUtils.synthesizeKey("KEY_End", { shiftKey: true }, win);
+ assertState(
+ [
+ { text: "21-add" },
+ { text: "54-add" },
+ { text: "57-add" },
+ { text: "58-add" },
+ { text: "59-add" },
+ { text: "60-add" },
+ { text: "61-add", selected: true },
+ { text: "63-add", selected: true },
+ { text: "66-add", selected: true, focused: true },
+ ],
+ "Selection range forwards from 61-add to 66-add"
+ );
+ EventUtils.synthesizeKey("KEY_Home", { shiftKey: true }, win);
+ assertState(
+ [
+ { text: "21-add", selected: true, focused: true },
+ { text: "54-add", selected: true },
+ { text: "57-add", selected: true },
+ { text: "58-add", selected: true },
+ { text: "59-add", selected: true },
+ { text: "60-add", selected: true },
+ { text: "61-add", selected: true },
+ { text: "63-add" },
+ { text: "66-add" },
+ ],
+ "Selection range backwards from 61-add to 21-add"
+ );
+ EventUtils.synthesizeKey("KEY_ArrowLeft", { shiftKey: true }, win);
+ EventUtils.synthesizeKey("KEY_ArrowLeft", { shiftKey: true }, win);
+ assertState(
+ [
+ { text: "21-add" },
+ { text: "54-add" },
+ { text: "57-add", selected: true, focused: true },
+ { text: "58-add", selected: true },
+ { text: "59-add", selected: true },
+ { text: "60-add", selected: true },
+ { text: "61-add", selected: true },
+ { text: "63-add" },
+ { text: "66-add" },
+ ],
+ "Selection range backwards from 61-add to 57-add"
+ );
+
+ // Delete the start of the selection.
+ widget.removeItems(1, 3);
+ assertState(
+ [
+ { text: "21-add" },
+ { text: "59-add", selected: true, focused: true },
+ { text: "60-add", selected: true },
+ { text: "61-add", selected: true },
+ { text: "63-add" },
+ { text: "66-add" },
+ ],
+ "Selection range shrinks to 59-add"
+ );
+
+ // Do the same with a gap after the focus and its next item.
+ widget.addItems(3, ["67-add", "68-add", "69-add"]);
+ assertState(
+ [
+ { text: "21-add" },
+ { text: "59-add", selected: true, focused: true },
+ { text: "60-add", selected: true },
+ { text: "67-add" },
+ { text: "68-add" },
+ { text: "69-add" },
+ { text: "61-add", selected: true },
+ { text: "63-add" },
+ { text: "66-add" },
+ ],
+ "Selection range backwards from 61-add to 59-add with gap"
+ );
+
+ widget.removeItems(1, 1);
+ assertState(
+ [
+ { text: "21-add" },
+ { text: "60-add", selected: true, focused: true },
+ { text: "67-add" },
+ { text: "68-add" },
+ { text: "69-add" },
+ { text: "61-add", selected: true },
+ { text: "63-add" },
+ { text: "66-add" },
+ ],
+ "Focus moves to the next item, before gap"
+ );
+
+ // Do the same with a gap and all items before the gap are removed.
+ EventUtils.synthesizeKey("KEY_ArrowLeft", { shiftKey: true }, win);
+ assertState(
+ [
+ { text: "21-add" },
+ { text: "60-add" },
+ { text: "67-add", selected: true, focused: true },
+ { text: "68-add", selected: true },
+ { text: "69-add", selected: true },
+ { text: "61-add", selected: true },
+ { text: "63-add" },
+ { text: "66-add" },
+ ],
+ "Selection range backward reduced to 67-add with gap filled"
+ );
+ widget.addItems(4, ["70-add", "71-add"]);
+ assertState(
+ [
+ { text: "21-add" },
+ { text: "60-add" },
+ { text: "67-add", selected: true, focused: true },
+ { text: "68-add", selected: true },
+ { text: "70-add" },
+ { text: "71-add" },
+ { text: "69-add", selected: true },
+ { text: "61-add", selected: true },
+ { text: "63-add" },
+ { text: "66-add" },
+ ],
+ "Selection range backward from 61-add to 67-add with gap"
+ );
+
+ widget.removeItems(2, 2);
+ assertState(
+ [
+ { text: "21-add" },
+ { text: "60-add" },
+ { text: "70-add" },
+ { text: "71-add" },
+ { text: "69-add", selected: true, focused: true },
+ { text: "61-add", selected: true },
+ { text: "63-add" },
+ { text: "66-add" },
+ ],
+ "Focus jumps gap to selection range"
+ );
+
+ // Same, with entire gap also removed.
+ clickWidgetItem(1, { shiftKey: true });
+ widget.addItems(2, ["72-add"]);
+ assertState(
+ [
+ { text: "21-add" },
+ { text: "60-add", selected: true, focused: true },
+ { text: "72-add" },
+ { text: "70-add", selected: true },
+ { text: "71-add", selected: true },
+ { text: "69-add", selected: true },
+ { text: "61-add", selected: true },
+ { text: "63-add" },
+ { text: "66-add" },
+ ],
+ "Selection range backwards from 60-add to 70-add"
+ );
+
+ widget.removeItems(1, 3);
+ assertState(
+ [
+ { text: "21-add" },
+ { text: "71-add", selected: true, focused: true },
+ { text: "69-add", selected: true },
+ { text: "61-add", selected: true },
+ { text: "63-add" },
+ { text: "66-add" },
+ ],
+ "Focus moves to the start of what is left of the selection range"
+ );
+
+ // Test deleting the start of the selection with focus in a gap.
+ // We don't expect to follow the special treatment because the user has
+ // explicitly moved the focus "outside" of the selected range.
+ widget.addItems(2, ["73-add", "74-add", "75-add"]);
+ EventUtils.synthesizeKey("KEY_ArrowLeft", { ctrlKey: true }, win);
+ assertState(
+ [
+ { text: "21-add" },
+ { text: "71-add", selected: true },
+ { text: "73-add", focused: true },
+ { text: "74-add" },
+ { text: "75-add" },
+ { text: "69-add", selected: true },
+ { text: "61-add", selected: true },
+ { text: "63-add" },
+ { text: "66-add" },
+ ],
+ "Focus in range gap"
+ );
+
+ widget.removeItems(1, 2);
+ assertState(
+ [
+ { text: "21-add" },
+ { text: "74-add", focused: true },
+ { text: "75-add" },
+ { text: "69-add", selected: true },
+ { text: "61-add", selected: true },
+ { text: "63-add" },
+ { text: "66-add" },
+ ],
+ "Focus moves to the next item, rather than the selection range"
+ );
+
+ // Same, but deleting the end of the range.
+ clickWidgetItem(1, { shiftKey: true });
+ EventUtils.synthesizeKey("KEY_ArrowLeft", { ctrlKey: true }, win);
+ EventUtils.synthesizeKey("KEY_ArrowLeft", { ctrlKey: true }, win);
+ assertState(
+ [
+ { text: "21-add" },
+ { text: "74-add", selected: true },
+ { text: "75-add", selected: true },
+ { text: "69-add", selected: true, focused: true },
+ { text: "61-add", selected: true },
+ { text: "63-add" },
+ { text: "66-add" },
+ ],
+ "Selection range backward from 61-add to 74-add, with focus shifted"
+ );
+ widget.addItems(3, ["76-add", "77-add"]);
+ assertState(
+ [
+ { text: "21-add" },
+ { text: "74-add", selected: true },
+ { text: "75-add", selected: true },
+ { text: "76-add" },
+ { text: "77-add" },
+ { text: "69-add", selected: true, focused: true },
+ { text: "61-add", selected: true },
+ { text: "63-add" },
+ { text: "66-add" },
+ ],
+ "Focus in gap"
+ );
+
+ widget.removeItems(5, 2);
+ assertState(
+ [
+ { text: "21-add" },
+ { text: "74-add", selected: true },
+ { text: "75-add", selected: true },
+ { text: "76-add" },
+ { text: "77-add" },
+ { text: "63-add", focused: true },
+ { text: "66-add" },
+ ],
+ "Focus moves to next item, rather than selection range end"
+ );
+
+ // Selection pivot now about 75-add.
+ EventUtils.synthesizeKey("KEY_ArrowRight", { shiftKey: true }, win);
+ assertState(
+ [
+ { text: "21-add" },
+ { text: "74-add" },
+ { text: "75-add", selected: true },
+ { text: "76-add", selected: true },
+ { text: "77-add", selected: true, focused: true },
+ { text: "63-add" },
+ { text: "66-add" },
+ ],
+ "Selection range forward from 75-add to 77-add"
+ );
+
+ widget.addItems(1, ["78-add", "79-add", "80-add"]);
+ clickWidgetItem(2, { shiftKey: true });
+ assertState(
+ [
+ { text: "21-add" },
+ { text: "78-add" },
+ { text: "79-add", selected: true, focused: true },
+ { text: "80-add", selected: true },
+ { text: "74-add", selected: true },
+ { text: "75-add", selected: true },
+ { text: "76-add" },
+ { text: "77-add" },
+ { text: "63-add" },
+ { text: "66-add" },
+ ],
+ "Selection range backward from 75-add to 79-add"
+ );
+
+ // Move focus to the end of the range and delete again, but with no gap this
+ // time.
+ EventUtils.synthesizeKey("KEY_ArrowLeft", { ctrlKey: true }, win);
+ EventUtils.synthesizeKey("KEY_ArrowLeft", { ctrlKey: true }, win);
+ EventUtils.synthesizeKey("KEY_ArrowLeft", { ctrlKey: true }, win);
+ assertState(
+ [
+ { text: "21-add" },
+ { text: "78-add" },
+ { text: "79-add", selected: true },
+ { text: "80-add", selected: true },
+ { text: "74-add", selected: true },
+ { text: "75-add", selected: true, focused: true },
+ { text: "76-add" },
+ { text: "77-add" },
+ { text: "63-add" },
+ { text: "66-add" },
+ ],
+ "Focus moved to the end of the selection"
+ );
+
+ widget.removeItems(5, 2);
+ assertState(
+ [
+ { text: "21-add" },
+ { text: "78-add" },
+ { text: "79-add", selected: true },
+ { text: "80-add", selected: true },
+ { text: "74-add", selected: true },
+ { text: "77-add", focused: true },
+ { text: "63-add" },
+ { text: "66-add" },
+ ],
+ "Focus moved to the next item rather than the selection start"
+ );
+
+ // Deleting with focus before the selection start.
+ EventUtils.synthesizeKey("KEY_Home", { ctrlKey: true });
+ EventUtils.synthesizeKey("KEY_ArrowLeft", { ctrlKey: true });
+ assertState(
+ [
+ { text: "21-add" },
+ { text: "78-add", focused: true },
+ { text: "79-add", selected: true },
+ { text: "80-add", selected: true },
+ { text: "74-add", selected: true },
+ { text: "77-add" },
+ { text: "63-add" },
+ { text: "66-add" },
+ ],
+ "Focus before the selection start"
+ );
+
+ widget.removeItems(1, 1);
+ assertState(
+ [
+ { text: "21-add" },
+ { text: "79-add", selected: true, focused: true },
+ { text: "80-add", selected: true },
+ { text: "74-add", selected: true },
+ { text: "77-add" },
+ { text: "63-add" },
+ { text: "66-add" },
+ ],
+ "Focus moves to the next item, which happens to be in the selection range"
+ );
+
+ // Test deleting with focus in the middle of the range.
+ EventUtils.synthesizeKey("KEY_ArrowRight", { shiftKey: true });
+ EventUtils.synthesizeKey("KEY_ArrowLeft", { ctrlKey: true });
+ assertState(
+ [
+ { text: "21-add", selected: true },
+ { text: "79-add", selected: true, focused: true },
+ { text: "80-add", selected: true },
+ { text: "74-add", selected: true },
+ { text: "77-add" },
+ { text: "63-add" },
+ { text: "66-add" },
+ ],
+ "Selection range backwards from 74-add to 21-add, with focus in middle"
+ );
+
+ widget.removeItems(1, 1);
+ assertState(
+ [
+ { text: "21-add", selected: true },
+ { text: "80-add", selected: true, focused: true },
+ { text: "74-add", selected: true },
+ { text: "77-add" },
+ { text: "63-add" },
+ { text: "66-add" },
+ ],
+ "Focus moves to the next item, rather than the selection start or end"
+ );
+
+ // Delete the whole range.
+ widget.removeItems(0, 4);
+ assertState(
+ [{ text: "63-add", selected: true, focused: true }, { text: "66-add" }],
+ "Focus and selection move to the next remaining item"
+ );
+
+ // Do the same with focus outside the range.
+ widget.addItems(0, ["81-add", "82-add", "83-add", "84-add", "85-add"]);
+ EventUtils.synthesizeKey("KEY_ArrowRight", { shiftKey: true }, win);
+ EventUtils.synthesizeKey("KEY_ArrowRight", { ctrlKey: true }, win);
+ assertState(
+ [
+ { text: "81-add" },
+ { text: "82-add" },
+ { text: "83-add" },
+ { text: "84-add", focused: true },
+ { text: "85-add", selected: true },
+ { text: "63-add", selected: true },
+ { text: "66-add" },
+ ],
+ "Focus outside backward selection range"
+ );
+
+ widget.removeItems(4, 2);
+ assertState(
+ [
+ { text: "81-add" },
+ { text: "82-add" },
+ { text: "83-add" },
+ { text: "84-add", focused: true },
+ { text: "66-add" },
+ ],
+ "Focus remains the same and is not selected"
+ );
+
+ // Same, but with focus also removed.
+ widget.addItems(5, ["86-add"]);
+ EventUtils.synthesizeKey("KEY_ArrowRight", { shiftKey: true }, win);
+ EventUtils.synthesizeKey("KEY_ArrowRight", { ctrlKey: true }, win);
+ assertState(
+ [
+ { text: "81-add" },
+ { text: "82-add", focused: true },
+ { text: "83-add", selected: true },
+ { text: "84-add", selected: true },
+ { text: "66-add" },
+ { text: "86-add" },
+ ],
+ "Focus outside backwards selection range"
+ );
+
+ widget.removeItems(1, 3);
+ assertState(
+ [
+ { text: "81-add" },
+ { text: "66-add", selected: true, focused: true },
+ { text: "86-add" },
+ ],
+ "Focus moves to next item and selected"
+ );
+
+ // With multi-selection via toggling.
+
+ widget.addItems(3, [
+ "87-add",
+ "88-add",
+ "89-add",
+ "90-add",
+ "91-add",
+ "92-add",
+ "93-add",
+ "94-add",
+ ]);
+ EventUtils.synthesizeKey("KEY_ArrowLeft", {}, win);
+ EventUtils.synthesizeKey("KEY_ArrowLeft", { ctrlKey: true }, win);
+ EventUtils.synthesizeKey("KEY_ArrowLeft", { shiftKey: true }, win);
+ EventUtils.synthesizeKey("KEY_ArrowLeft", { shiftKey: true }, win);
+ EventUtils.synthesizeKey("KEY_ArrowLeft", { shiftKey: true }, win);
+ assertState(
+ [
+ { text: "81-add" },
+ { text: "66-add" },
+ { text: "86-add" },
+ { text: "87-add", selected: true },
+ { text: "88-add", selected: true },
+ { text: "89-add", selected: true },
+ { text: "90-add", selected: true, focused: true },
+ { text: "91-add" },
+ { text: "92-add" },
+ { text: "93-add" },
+ { text: "94-add" },
+ ],
+ "Start with range selection forward from 87-add to 90-add"
+ );
+
+ clickWidgetItem(4, { ctrlKey: true });
+ clickWidgetItem(5, { ctrlKey: true });
+ assertState(
+ [
+ { text: "81-add" },
+ { text: "66-add" },
+ { text: "86-add" },
+ { text: "87-add", selected: true },
+ { text: "88-add" },
+ { text: "89-add", focused: true },
+ { text: "90-add", selected: true },
+ { text: "91-add" },
+ { text: "92-add" },
+ { text: "93-add" },
+ { text: "94-add" },
+ ],
+ "Range selection from 87-add to 90-add, with 88-add and 89-add unselected"
+ );
+
+ EventUtils.synthesizeKey("KEY_ArrowLeft", { ctrlKey: true });
+ EventUtils.synthesizeKey("KEY_ArrowLeft", { ctrlKey: true });
+ EventUtils.synthesizeKey(" ", { ctrlKey: true });
+ assertState(
+ [
+ { text: "81-add" },
+ { text: "66-add" },
+ { text: "86-add" },
+ { text: "87-add", selected: true },
+ { text: "88-add" },
+ { text: "89-add" },
+ { text: "90-add", selected: true },
+ { text: "91-add", selected: true, focused: true },
+ { text: "92-add" },
+ { text: "93-add" },
+ { text: "94-add" },
+ ],
+ "Mixed selection"
+ );
+
+ // Remove before the selected items
+ widget.removeItems(0, 2);
+ assertState(
+ [
+ { text: "86-add" },
+ { text: "87-add", selected: true },
+ { text: "88-add" },
+ { text: "89-add" },
+ { text: "90-add", selected: true },
+ { text: "91-add", selected: true, focused: true },
+ { text: "92-add" },
+ { text: "93-add" },
+ { text: "94-add" },
+ ],
+ "Same selection and focus"
+ );
+
+ // Remove after
+ widget.removeItems(6, 1);
+ assertState(
+ [
+ { text: "86-add" },
+ { text: "87-add", selected: true },
+ { text: "88-add" },
+ { text: "89-add" },
+ { text: "90-add", selected: true },
+ { text: "91-add", selected: true, focused: true },
+ { text: "93-add" },
+ { text: "94-add" },
+ ],
+ "Same selection and focus"
+ );
+
+ // Removed the focused item, unlike a simple range selection, the focused
+ // item is not bound to stay within the selected items.
+ widget.removeItems(5, 1);
+ assertState(
+ [
+ { text: "86-add" },
+ { text: "87-add", selected: true },
+ { text: "88-add" },
+ { text: "89-add" },
+ { text: "90-add", selected: true },
+ { text: "93-add", focused: true },
+ { text: "94-add" },
+ ],
+ "Focus moves to next item and not selected"
+ );
+
+ // Remove the unselected items, merging the two ranges together.
+ widget.removeItems(2, 2);
+ assertState(
+ [
+ { text: "86-add" },
+ { text: "87-add", selected: true },
+ { text: "90-add", selected: true },
+ { text: "93-add", focused: true },
+ { text: "94-add" },
+ ],
+ "Focus remains the same"
+ );
+
+ // Remove the selected items.
+ widget.removeItems(1, 2);
+ assertState(
+ [
+ { text: "86-add" },
+ { text: "93-add", focused: true },
+ { text: "94-add" },
+ ],
+ "Focus remains the same, and not selected"
+ );
+
+ // Remove all selected items, including the focused item.
+ widget.addItems(0, [
+ "95-add",
+ "96-add",
+ "97-add",
+ "98-add",
+ "99-add",
+ "100-add",
+ "101-add",
+ ]);
+ EventUtils.synthesizeKey(" ", {}, win);
+ assertState(
+ [
+ { text: "95-add" },
+ { text: "96-add" },
+ { text: "97-add" },
+ { text: "98-add" },
+ { text: "99-add" },
+ { text: "100-add" },
+ { text: "101-add" },
+ { text: "86-add" },
+ { text: "93-add", selected: true, focused: true },
+ { text: "94-add" },
+ ],
+ "Single selection"
+ );
+
+ clickWidgetItem(6, { ctrlKey: true });
+ clickWidgetItem(5, { ctrlKey: true });
+ assertState(
+ [
+ { text: "95-add" },
+ { text: "96-add" },
+ { text: "97-add" },
+ { text: "98-add" },
+ { text: "99-add" },
+ { text: "100-add", selected: true, focused: true },
+ { text: "101-add", selected: true },
+ { text: "86-add" },
+ { text: "93-add", selected: true },
+ { text: "94-add" },
+ ],
+ "Mixed selection with focus selected"
+ );
+
+ widget.removeItems(5, 4);
+ assertState(
+ [
+ { text: "95-add" },
+ { text: "96-add" },
+ { text: "97-add" },
+ { text: "98-add" },
+ { text: "99-add" },
+ { text: "94-add", selected: true, focused: true },
+ ],
+ "Focus moves to next item and selected"
+ );
+
+ // Remove all selected, with focus outside the selection
+ clickWidgetItem(1, {});
+ clickWidgetItem(3, { ctrlKey: true });
+ EventUtils.synthesizeKey("KEY_ArrowRight", { ctrlKey: true });
+ assertState(
+ [
+ { text: "95-add" },
+ { text: "96-add", selected: true },
+ { text: "97-add", focused: true },
+ { text: "98-add", selected: true },
+ { text: "99-add" },
+ { text: "94-add" },
+ ],
+ "Mixed selection with focus not selected"
+ );
+
+ widget.removeItems(1, 3);
+ assertState(
+ [
+ { text: "95-add" },
+ { text: "99-add", selected: true, focused: true },
+ { text: "94-add" },
+ ],
+ "Focus moves to next item and selected"
+ );
+
+ // With select all.
+ widget.addItems(0, ["102-add", "103-add", "104-add"]);
+ widget.addItems(6, ["105-add", "106-add"]);
+ selectAllShortcut();
+ assertState(
+ [
+ { text: "102-add", selected: true },
+ { text: "103-add", selected: true },
+ { text: "104-add", selected: true },
+ { text: "95-add", selected: true },
+ { text: "99-add", selected: true, focused: true },
+ { text: "94-add", selected: true },
+ { text: "105-add", selected: true },
+ { text: "106-add", selected: true },
+ ],
+ "All selected"
+ );
+
+ // Remove middle and focused.
+ widget.removeItems(4, 1);
+ assertState(
+ [
+ { text: "102-add", selected: true },
+ { text: "103-add", selected: true },
+ { text: "104-add", selected: true },
+ { text: "95-add", selected: true },
+ { text: "94-add", selected: true, focused: true },
+ { text: "105-add", selected: true },
+ { text: "106-add", selected: true },
+ ],
+ "Focus moves to the next item, selections remain"
+ );
+
+ // Remove before focused.
+ widget.removeItems(1, 1);
+ assertState(
+ [
+ { text: "102-add", selected: true },
+ { text: "104-add", selected: true },
+ { text: "95-add", selected: true },
+ { text: "94-add", selected: true, focused: true },
+ { text: "105-add", selected: true },
+ { text: "106-add", selected: true },
+ ],
+ "Focus and selection remain"
+ );
+
+ // Remove after the focus.
+ widget.removeItems(4, 2);
+ assertState(
+ [
+ { text: "102-add", selected: true },
+ { text: "104-add", selected: true },
+ { text: "95-add", selected: true },
+ { text: "94-add", selected: true, focused: true },
+ ],
+ "Focus and selection remain"
+ );
+
+ // Remove end and focused.
+ widget.removeItems(3, 1);
+ assertState(
+ [
+ { text: "102-add", selected: true },
+ { text: "104-add", selected: true },
+ { text: "95-add", selected: true, focused: true },
+ ],
+ "Focus moves to the last item, selection remains"
+ );
+
+ // Remove start and focused.
+ EventUtils.synthesizeKey("KEY_Home", { ctrlKey: true }, win);
+ assertState(
+ [
+ { text: "102-add", selected: true, focused: true },
+ { text: "104-add", selected: true },
+ { text: "95-add", selected: true },
+ ],
+ "Focus on first item"
+ );
+
+ widget.removeItems(0, 1);
+ assertState(
+ [
+ { text: "104-add", selected: true, focused: true },
+ { text: "95-add", selected: true },
+ ],
+ "Focus moves to next item"
+ );
+
+ // Remove items to cause two distinct ranges to merge together, with
+ // in-between ranges removed.
+ widget.addItems(2, [
+ "107-add",
+ "108-add",
+ "109-add",
+ "110-add",
+ "111-add",
+ "112-add",
+ "113-add",
+ ]);
+ clickWidgetItem(0, { ctrlKey: true });
+ assertState(
+ [
+ { text: "104-add", focused: true },
+ { text: "95-add", selected: true },
+ { text: "107-add" },
+ { text: "108-add" },
+ { text: "109-add" },
+ { text: "110-add" },
+ { text: "111-add" },
+ { text: "112-add" },
+ { text: "113-add" },
+ ],
+ "Added items and de-selected 104-add"
+ );
+
+ clickWidgetItem(3, { ctrlKey: true });
+ clickWidgetItem(4, { ctrlKey: true });
+ clickWidgetItem(6, { ctrlKey: true });
+ clickWidgetItem(8, { ctrlKey: true });
+ assertState(
+ [
+ { text: "104-add" },
+ { text: "95-add", selected: true },
+ { text: "107-add" },
+ { text: "108-add", selected: true },
+ { text: "109-add", selected: true },
+ { text: "110-add" },
+ { text: "111-add", selected: true },
+ { text: "112-add" },
+ { text: "113-add", selected: true, focused: true },
+ ],
+ "Several selection ranges"
+ );
+
+ widget.removeItems(2, 6);
+ assertState(
+ [
+ { text: "104-add" },
+ { text: "95-add", selected: true },
+ { text: "113-add", selected: true, focused: true },
+ ],
+ "End ranges merged together"
+ );
+
+ // Do the same, but where parts of the end ranges are also removed.
+ widget.addItems(3, [
+ "114-add",
+ "115-add",
+ "116-add",
+ "117-add",
+ "118-add",
+ "119-add",
+ "120-add",
+ "121-add",
+ "122-add",
+ "123-add",
+ "124-add",
+ "125-add",
+ "126-add",
+ "127-add",
+ ]);
+ clickWidgetItem(4, { ctrlKey: true });
+ clickWidgetItem(5, { ctrlKey: true });
+ clickWidgetItem(7, { ctrlKey: true });
+ clickWidgetItem(10, { ctrlKey: true });
+ clickWidgetItem(12, { ctrlKey: true });
+ clickWidgetItem(13, { ctrlKey: true });
+ clickWidgetItem(14, { ctrlKey: true });
+ clickWidgetItem(16, { ctrlKey: true });
+
+ clickWidgetItem(9, { ctrlKey: true });
+ assertState(
+ [
+ { text: "104-add" },
+ { text: "95-add", selected: true },
+ { text: "113-add", selected: true },
+ { text: "114-add" },
+ { text: "115-add", selected: true },
+ { text: "116-add", selected: true },
+ { text: "117-add" },
+ { text: "118-add", selected: true },
+ { text: "119-add" },
+ { text: "120-add", selected: true, focused: true },
+ { text: "121-add", selected: true },
+ { text: "122-add" },
+ { text: "123-add", selected: true },
+ { text: "124-add", selected: true },
+ { text: "125-add", selected: true },
+ { text: "126-add" },
+ { text: "127-add", selected: true },
+ ],
+ "Several ranges"
+ );
+
+ widget.removeItems(5, 8);
+ assertState(
+ [
+ { text: "104-add" },
+ { text: "95-add", selected: true },
+ { text: "113-add", selected: true },
+ { text: "114-add" },
+ { text: "115-add", selected: true },
+ { text: "124-add", selected: true, focused: true },
+ { text: "125-add", selected: true },
+ { text: "126-add" },
+ { text: "127-add", selected: true },
+ ],
+ "Two ranges merged and rest removed, focus moves to next item"
+ );
+ }
+});
+
+// If widget is emptied whilst focused, focus moves to widget.
+add_task(function test_emptying_widget() {
+ for (let model of selectionModels) {
+ // Empty with focused widget.
+ reset({ model });
+ stepFocus(true, { element: widget }, "Initial");
+ widget.addItems(0, ["First", "Second"]);
+ assertFocus({ element: widget }, "Focus still on widget after adding");
+ widget.removeItems(0, 2);
+ assertFocus({ element: widget }, "Focus still on widget after removing");
+
+ // Empty with focused item.
+ widget.addItems(0, ["First", "Second"]);
+ EventUtils.synthesizeKey("KEY_Home", {}, win);
+ assertFocus({ index: 0 }, "Focus on first item");
+ widget.removeItems(0, 2);
+ assertFocus({ element: widget }, "Focus moves to widget after removing");
+
+ // Empty with focus elsewhere.
+ widget.addItems(0, ["First", "Second"]);
+ stepFocus(false, { element: before }, "Focus elsewhere");
+ widget.removeItems(0, 2);
+ assertFocus({ element: before }, "Focus still elsewhere after removing");
+ stepFocus(true, { element: widget }, "Widget becomes focused");
+
+ // Empty with focus elsewhere, but active item.
+ widget.addItems(0, ["First", "Second"]);
+ // Move away from and back to widget to focus second item.
+ stepFocus(true, { element: after }, "Focus elsewhere");
+ widget.selectSingleItem(1);
+ stepFocus(false, { index: 1 }, "Focus on second item");
+ stepFocus(false, { element: before }, "Return focus to elsewhere");
+ widget.removeItems(0, 2);
+ assertFocus({ element: before }, "Focus still elsewhere after removing");
+ stepFocus(true, { element: widget }, "Widget becomes focused");
+ }
+});
+
+/**
+ * Test moving items in the widget.
+ *
+ * @param {string} model - The selection model to use.
+ * @param {boolean} reCreate - Whether the widget should reCreate the items when
+ * moving them.
+ */
+function subtest_move_items(model, reCreate) {
+ reset({ model, direction: "right-to-left" });
+
+ widget.addItems(0, [
+ "0-add",
+ "1-add",
+ "2-add",
+ "3-add",
+ "4-add",
+ "5-add",
+ "6-add",
+ "7-add",
+ "8-add",
+ "9-add",
+ "10-add",
+ "11-add",
+ "12-add",
+ "13-add",
+ ]);
+ clickWidgetItem(5, {});
+
+ assertState(
+ [
+ { text: "0-add" },
+ { text: "1-add" },
+ { text: "2-add" },
+ { text: "3-add" },
+ { text: "4-add" },
+ { text: "5-add", selected: true, focused: true },
+ { text: "6-add" },
+ { text: "7-add" },
+ { text: "8-add" },
+ { text: "9-add" },
+ { text: "10-add" },
+ { text: "11-add" },
+ { text: "12-add" },
+ { text: "13-add" },
+ ],
+ "Item 5 selected and focused"
+ );
+
+ // Move items before focus.
+ widget.moveItems(4, 3, 1, reCreate);
+ assertState(
+ [
+ { text: "0-add" },
+ { text: "1-add" },
+ { text: "2-add" },
+ { text: "4-add" },
+ { text: "3-add" },
+ { text: "5-add", selected: true, focused: true },
+ { text: "6-add" },
+ { text: "7-add" },
+ { text: "8-add" },
+ { text: "9-add" },
+ { text: "10-add" },
+ { text: "11-add" },
+ { text: "12-add" },
+ { text: "13-add" },
+ ],
+ "Same focus and selection"
+ );
+ widget.moveItems(1, 3, 2, reCreate);
+ assertState(
+ [
+ { text: "0-add" },
+ { text: "4-add" },
+ { text: "3-add" },
+ { text: "1-add" },
+ { text: "2-add" },
+ { text: "5-add", selected: true, focused: true },
+ { text: "6-add" },
+ { text: "7-add" },
+ { text: "8-add" },
+ { text: "9-add" },
+ { text: "10-add" },
+ { text: "11-add" },
+ { text: "12-add" },
+ { text: "13-add" },
+ ],
+ "Same focus and selection"
+ );
+
+ // Move items after focus.
+ widget.moveItems(6, 8, 2, reCreate);
+ assertState(
+ [
+ { text: "0-add" },
+ { text: "4-add" },
+ { text: "3-add" },
+ { text: "1-add" },
+ { text: "2-add" },
+ { text: "5-add", selected: true, focused: true },
+ { text: "8-add" },
+ { text: "9-add" },
+ { text: "6-add" },
+ { text: "7-add" },
+ { text: "10-add" },
+ { text: "11-add" },
+ { text: "12-add" },
+ { text: "13-add" },
+ ],
+ "Same focus and selection"
+ );
+ widget.moveItems(9, 6, 1, reCreate);
+ assertState(
+ [
+ { text: "0-add" },
+ { text: "4-add" },
+ { text: "3-add" },
+ { text: "1-add" },
+ { text: "2-add" },
+ { text: "5-add", selected: true, focused: true },
+ { text: "7-add" },
+ { text: "8-add" },
+ { text: "9-add" },
+ { text: "6-add" },
+ { text: "10-add" },
+ { text: "11-add" },
+ { text: "12-add" },
+ { text: "13-add" },
+ ],
+ "Same focus and selection"
+ );
+
+ // Move from before focus to after focus.
+ widget.moveItems(2, 3, 3, reCreate);
+ assertState(
+ [
+ { text: "0-add" },
+ { text: "4-add" },
+ { text: "5-add", selected: true, focused: true },
+ { text: "3-add" },
+ { text: "1-add" },
+ { text: "2-add" },
+ { text: "7-add" },
+ { text: "8-add" },
+ { text: "9-add" },
+ { text: "6-add" },
+ { text: "10-add" },
+ { text: "11-add" },
+ { text: "12-add" },
+ { text: "13-add" },
+ ],
+ "Same focus and selection, but moved"
+ );
+
+ // Move from after focus to before focus.
+ widget.moveItems(3, 2, 5, reCreate);
+ assertState(
+ [
+ { text: "0-add" },
+ { text: "4-add" },
+ { text: "3-add" },
+ { text: "1-add" },
+ { text: "2-add" },
+ { text: "7-add" },
+ { text: "8-add" },
+ { text: "5-add", selected: true, focused: true },
+ { text: "9-add" },
+ { text: "6-add" },
+ { text: "10-add" },
+ { text: "11-add" },
+ { text: "12-add" },
+ { text: "13-add" },
+ ],
+ "Same focus and selection, but moved"
+ );
+
+ // Move selected and focused up.
+ widget.moveItems(7, 3, 1, reCreate);
+ assertState(
+ [
+ { text: "0-add" },
+ { text: "4-add" },
+ { text: "3-add" },
+ { text: "5-add", selected: true, focused: true },
+ { text: "1-add" },
+ { text: "2-add" },
+ { text: "7-add" },
+ { text: "8-add" },
+ { text: "9-add" },
+ { text: "6-add" },
+ { text: "10-add" },
+ { text: "11-add" },
+ { text: "12-add" },
+ { text: "13-add" },
+ ],
+ "Focus and selection moved to index 3"
+ );
+
+ // Move down.
+ widget.moveItems(3, 5, 1, reCreate);
+ assertState(
+ [
+ { text: "0-add" },
+ { text: "4-add" },
+ { text: "3-add" },
+ { text: "1-add" },
+ { text: "2-add" },
+ { text: "5-add", selected: true, focused: true },
+ { text: "7-add" },
+ { text: "8-add" },
+ { text: "9-add" },
+ { text: "6-add" },
+ { text: "10-add" },
+ { text: "11-add" },
+ { text: "12-add" },
+ { text: "13-add" },
+ ],
+ "Focus and selection moved to index 5"
+ );
+
+ // Move in a group.
+ widget.moveItems(4, 5, 3, reCreate);
+ assertState(
+ [
+ { text: "0-add" },
+ { text: "4-add" },
+ { text: "3-add" },
+ { text: "1-add" },
+ { text: "8-add" },
+ { text: "2-add" },
+ { text: "5-add", selected: true, focused: true },
+ { text: "7-add" },
+ { text: "9-add" },
+ { text: "6-add" },
+ { text: "10-add" },
+ { text: "11-add" },
+ { text: "12-add" },
+ { text: "13-add" },
+ ],
+ "Focus and selection moved to index 6"
+ );
+ widget.moveItems(5, 4, 3, reCreate);
+ assertState(
+ [
+ { text: "0-add" },
+ { text: "4-add" },
+ { text: "3-add" },
+ { text: "1-add" },
+ { text: "2-add" },
+ { text: "5-add", selected: true, focused: true },
+ { text: "7-add" },
+ { text: "8-add" },
+ { text: "9-add" },
+ { text: "6-add" },
+ { text: "10-add" },
+ { text: "11-add" },
+ { text: "12-add" },
+ { text: "13-add" },
+ ],
+ "Focus and selection moved back to index 5"
+ );
+
+ // With focus split from selection.
+ if (model == "focus") {
+ return;
+ }
+
+ // Focus before selection.
+ EventUtils.synthesizeKey("KEY_ArrowRight", { ctrlKey: true }, win);
+ EventUtils.synthesizeKey("KEY_ArrowRight", { ctrlKey: true }, win);
+ assertState(
+ [
+ { text: "0-add" },
+ { text: "4-add" },
+ { text: "3-add" },
+ { text: "1-add", focused: true },
+ { text: "2-add" },
+ { text: "5-add", selected: true },
+ { text: "7-add" },
+ { text: "8-add" },
+ { text: "9-add" },
+ { text: "6-add" },
+ { text: "10-add" },
+ { text: "11-add" },
+ { text: "12-add" },
+ { text: "13-add" },
+ ],
+ "Focus before selection"
+ );
+
+ // Move before both.
+ widget.moveItems(0, 1, 1, reCreate);
+ assertState(
+ [
+ { text: "4-add" },
+ { text: "0-add" },
+ { text: "3-add" },
+ { text: "1-add", focused: true },
+ { text: "2-add" },
+ { text: "5-add", selected: true },
+ { text: "7-add" },
+ { text: "8-add" },
+ { text: "9-add" },
+ { text: "6-add" },
+ { text: "10-add" },
+ { text: "11-add" },
+ { text: "12-add" },
+ { text: "13-add" },
+ ],
+ "Same focus and selection"
+ );
+
+ // Move after both.
+ widget.moveItems(8, 7, 1, reCreate);
+ assertState(
+ [
+ { text: "4-add" },
+ { text: "0-add" },
+ { text: "3-add" },
+ { text: "1-add", focused: true },
+ { text: "2-add" },
+ { text: "5-add", selected: true },
+ { text: "7-add" },
+ { text: "9-add" },
+ { text: "8-add" },
+ { text: "6-add" },
+ { text: "10-add" },
+ { text: "11-add" },
+ { text: "12-add" },
+ { text: "13-add" },
+ ],
+ "Same focus and selection"
+ );
+
+ // Move focus to after selected.
+ widget.moveItems(3, 6, 2, reCreate);
+ assertState(
+ [
+ { text: "4-add" },
+ { text: "0-add" },
+ { text: "3-add" },
+ { text: "5-add", selected: true },
+ { text: "7-add" },
+ { text: "9-add" },
+ { text: "1-add", focused: true },
+ { text: "2-add" },
+ { text: "8-add" },
+ { text: "6-add" },
+ { text: "10-add" },
+ { text: "11-add" },
+ { text: "12-add" },
+ { text: "13-add" },
+ ],
+ "Focus moved to after selection"
+ );
+
+ // Move focus before selected.
+ widget.moveItems(5, 2, 3, reCreate);
+ assertState(
+ [
+ { text: "4-add" },
+ { text: "0-add" },
+ { text: "9-add" },
+ { text: "1-add", focused: true },
+ { text: "2-add" },
+ { text: "3-add" },
+ { text: "5-add", selected: true },
+ { text: "7-add" },
+ { text: "8-add" },
+ { text: "6-add" },
+ { text: "10-add" },
+ { text: "11-add" },
+ { text: "12-add" },
+ { text: "13-add" },
+ ],
+ "Focus moved to before selection"
+ );
+
+ // Move selection before focus.
+ widget.moveItems(5, 1, 5, reCreate);
+ assertState(
+ [
+ { text: "4-add" },
+ { text: "3-add" },
+ { text: "5-add", selected: true },
+ { text: "7-add" },
+ { text: "8-add" },
+ { text: "6-add" },
+ { text: "0-add" },
+ { text: "9-add" },
+ { text: "1-add", focused: true },
+ { text: "2-add" },
+ { text: "10-add" },
+ { text: "11-add" },
+ { text: "12-add" },
+ { text: "13-add" },
+ ],
+ "Selected moved to before focus"
+ );
+
+ // Move selection after focus.
+ widget.moveItems(2, 8, 1, reCreate);
+ assertState(
+ [
+ { text: "4-add" },
+ { text: "3-add" },
+ { text: "7-add" },
+ { text: "8-add" },
+ { text: "6-add" },
+ { text: "0-add" },
+ { text: "9-add" },
+ { text: "1-add", focused: true },
+ { text: "5-add", selected: true },
+ { text: "2-add" },
+ { text: "10-add" },
+ { text: "11-add" },
+ { text: "12-add" },
+ { text: "13-add" },
+ ],
+ "Selected moved to after focus"
+ );
+
+ // Navigation still works.
+ EventUtils.synthesizeKey("KEY_ArrowRight", { ctrlKey: true }, win);
+ assertState(
+ [
+ { text: "4-add" },
+ { text: "3-add" },
+ { text: "7-add" },
+ { text: "8-add" },
+ { text: "6-add" },
+ { text: "0-add" },
+ { text: "9-add", focused: true },
+ { text: "1-add" },
+ { text: "5-add", selected: true },
+ { text: "2-add" },
+ { text: "10-add" },
+ { text: "11-add" },
+ { text: "12-add" },
+ { text: "13-add" },
+ ],
+ "Selected moved to after focus"
+ );
+
+ // Test with multi-selection.
+ if (model == "browse") {
+ return;
+ }
+
+ EventUtils.synthesizeKey("KEY_ArrowRight", { shiftKey: true }, win);
+ EventUtils.synthesizeKey("KEY_ArrowRight", { shiftKey: true }, win);
+ assertState(
+ [
+ { text: "4-add" },
+ { text: "3-add" },
+ { text: "7-add" },
+ { text: "8-add" },
+ { text: "6-add", selected: true, focused: true },
+ { text: "0-add", selected: true },
+ { text: "9-add", selected: true },
+ { text: "1-add" },
+ { text: "5-add" },
+ { text: "2-add" },
+ { text: "10-add" },
+ { text: "11-add" },
+ { text: "12-add" },
+ { text: "13-add" },
+ ],
+ "Range selection from 9-add to 6-add"
+ );
+
+ // Move non-selected into the middle of the selected.
+ widget.moveItems(8, 5, 2, reCreate);
+ assertState(
+ [
+ { text: "4-add" },
+ { text: "3-add" },
+ { text: "7-add" },
+ { text: "8-add" },
+ { text: "6-add", selected: true, focused: true },
+ { text: "5-add" },
+ { text: "2-add" },
+ { text: "0-add", selected: true },
+ { text: "9-add", selected: true },
+ { text: "1-add" },
+ { text: "10-add" },
+ { text: "11-add" },
+ { text: "12-add" },
+ { text: "13-add" },
+ ],
+ "Non-selected gap"
+ );
+
+ // Moving an item always ends a Shift range selection.
+ EventUtils.synthesizeKey("KEY_ArrowRight", { shiftKey: true }, win);
+ assertState(
+ [
+ { text: "4-add" },
+ { text: "3-add" },
+ { text: "7-add" },
+ { text: "8-add", selected: true, focused: true },
+ { text: "6-add", selected: true },
+ { text: "5-add" },
+ { text: "2-add" },
+ { text: "0-add" },
+ { text: "9-add" },
+ { text: "1-add" },
+ { text: "10-add" },
+ { text: "11-add" },
+ { text: "12-add" },
+ { text: "13-add" },
+ ],
+ "Range selection from 6-add to 8-add"
+ );
+
+ clickWidgetItem(9, { shiftKey: true }, win);
+ assertState(
+ [
+ { text: "4-add" },
+ { text: "3-add" },
+ { text: "7-add" },
+ { text: "8-add" },
+ { text: "6-add", selected: true },
+ { text: "5-add", selected: true },
+ { text: "2-add", selected: true },
+ { text: "0-add", selected: true },
+ { text: "9-add", selected: true },
+ { text: "1-add", selected: true, focused: true },
+ { text: "10-add" },
+ { text: "11-add" },
+ { text: "12-add" },
+ { text: "13-add" },
+ ],
+ "Range selection from 6-add to 1-add"
+ );
+
+ // Move selected to middle of selected.
+ widget.moveItems(8, 6, 2, reCreate);
+ assertState(
+ [
+ { text: "4-add" },
+ { text: "3-add" },
+ { text: "7-add" },
+ { text: "8-add" },
+ { text: "6-add", selected: true },
+ { text: "5-add", selected: true },
+ { text: "9-add", selected: true },
+ { text: "1-add", selected: true, focused: true },
+ { text: "2-add", selected: true },
+ { text: "0-add", selected: true },
+ { text: "10-add" },
+ { text: "11-add" },
+ { text: "12-add" },
+ { text: "13-add" },
+ ],
+ "Selection block"
+ );
+
+ // Also ends a Shift range selection.
+ EventUtils.synthesizeKey("KEY_ArrowRight", { shiftKey: true }, win);
+ EventUtils.synthesizeKey("KEY_ArrowRight", { shiftKey: true }, win);
+ assertState(
+ [
+ { text: "4-add" },
+ { text: "3-add" },
+ { text: "7-add" },
+ { text: "8-add" },
+ { text: "6-add" },
+ { text: "5-add", selected: true, focused: true },
+ { text: "9-add", selected: true },
+ { text: "1-add", selected: true },
+ { text: "2-add" },
+ { text: "0-add" },
+ { text: "10-add" },
+ { text: "11-add" },
+ { text: "12-add" },
+ { text: "13-add" },
+ ],
+ "Range selection from 1-add to 5-add"
+ );
+
+ // Move from start of selection to end.
+ widget.moveItems(5, 7, 1, reCreate);
+ assertState(
+ [
+ { text: "4-add" },
+ { text: "3-add" },
+ { text: "7-add" },
+ { text: "8-add" },
+ { text: "6-add" },
+ { text: "9-add", selected: true },
+ { text: "1-add", selected: true },
+ { text: "5-add", selected: true, focused: true },
+ { text: "2-add" },
+ { text: "0-add" },
+ { text: "10-add" },
+ { text: "11-add" },
+ { text: "12-add" },
+ { text: "13-add" },
+ ],
+ "Moved to end"
+ );
+
+ // And reverse.
+ widget.moveItems(7, 5, 1, reCreate);
+ assertState(
+ [
+ { text: "4-add" },
+ { text: "3-add" },
+ { text: "7-add" },
+ { text: "8-add" },
+ { text: "6-add" },
+ { text: "5-add", selected: true, focused: true },
+ { text: "9-add", selected: true },
+ { text: "1-add", selected: true },
+ { text: "2-add" },
+ { text: "0-add" },
+ { text: "10-add" },
+ { text: "11-add" },
+ { text: "12-add" },
+ { text: "13-add" },
+ ],
+ "Moved back to start"
+ );
+
+ // Also broke Shift range selection.
+ EventUtils.synthesizeKey("KEY_ArrowRight", { shiftKey: true }, win);
+ EventUtils.synthesizeKey("KEY_ArrowRight", { shiftKey: true }, win);
+ EventUtils.synthesizeKey("KEY_ArrowRight", { shiftKey: true }, win);
+ EventUtils.synthesizeKey("KEY_ArrowRight", { shiftKey: true }, win);
+ assertState(
+ [
+ { text: "4-add" },
+ { text: "3-add", selected: true, focused: true },
+ { text: "7-add", selected: true },
+ { text: "8-add", selected: true },
+ { text: "6-add", selected: true },
+ { text: "5-add", selected: true },
+ { text: "9-add" },
+ { text: "1-add" },
+ { text: "2-add" },
+ { text: "0-add" },
+ { text: "10-add" },
+ { text: "11-add" },
+ { text: "12-add" },
+ { text: "13-add" },
+ ],
+ "Range selection from 5-add to 3-add"
+ );
+
+ EventUtils.synthesizeKey("KEY_ArrowLeft", { ctrlKey: true }, win);
+ EventUtils.synthesizeKey(" ", { ctrlKey: true }, win);
+ EventUtils.synthesizeKey("KEY_End", { ctrlKey: true }, win);
+ EventUtils.synthesizeKey(" ", { ctrlKey: true }, win);
+ EventUtils.synthesizeKey("KEY_ArrowRight", { ctrlKey: true }, win);
+ EventUtils.synthesizeKey("KEY_ArrowRight", { ctrlKey: true }, win);
+ EventUtils.synthesizeKey(" ", { ctrlKey: true }, win);
+ EventUtils.synthesizeKey("KEY_ArrowRight", { ctrlKey: true }, win);
+ EventUtils.synthesizeKey("KEY_ArrowRight", { ctrlKey: true }, win);
+ EventUtils.synthesizeKey(" ", { ctrlKey: true }, win);
+ EventUtils.synthesizeKey("KEY_ArrowRight", { ctrlKey: true }, win);
+ EventUtils.synthesizeKey(" ", { ctrlKey: true }, win);
+
+ assertState(
+ [
+ { text: "4-add" },
+ { text: "3-add", selected: true },
+ { text: "7-add" },
+ { text: "8-add", selected: true },
+ { text: "6-add", selected: true },
+ { text: "5-add", selected: true },
+ { text: "9-add" },
+ { text: "1-add" },
+ { text: "2-add", selected: true, focused: true },
+ { text: "0-add", selected: true },
+ { text: "10-add" },
+ { text: "11-add", selected: true },
+ { text: "12-add" },
+ { text: "13-add", selected: true },
+ ],
+ "Multi-selection"
+ );
+
+ // Move selected with gap into middle of a selection block.
+ widget.moveItems(8, 4, 6, reCreate);
+ assertState(
+ [
+ { text: "4-add" },
+ { text: "3-add", selected: true },
+ { text: "7-add" },
+ { text: "8-add", selected: true },
+ { text: "2-add", selected: true, focused: true },
+ { text: "0-add", selected: true },
+ { text: "10-add" },
+ { text: "11-add", selected: true },
+ { text: "12-add" },
+ { text: "13-add", selected: true },
+ { text: "6-add", selected: true },
+ { text: "5-add", selected: true },
+ { text: "9-add" },
+ { text: "1-add" },
+ ],
+ "Merged ranges together on both sides"
+ );
+
+ // Move selected with gap to start of a selection block.
+ widget.moveItems(5, 1, 5, reCreate);
+ assertState(
+ [
+ { text: "4-add" },
+ { text: "0-add", selected: true },
+ { text: "10-add" },
+ { text: "11-add", selected: true },
+ { text: "12-add" },
+ { text: "13-add", selected: true },
+ { text: "3-add", selected: true },
+ { text: "7-add" },
+ { text: "8-add", selected: true },
+ { text: "2-add", selected: true, focused: true },
+ { text: "6-add", selected: true },
+ { text: "5-add", selected: true },
+ { text: "9-add" },
+ { text: "1-add" },
+ ],
+ "Merged ranges together at start"
+ );
+
+ // Move selected with gap to end of a selection block.
+ widget.moveItems(1, 4, 8, reCreate);
+ assertState(
+ [
+ { text: "4-add" },
+ { text: "2-add", selected: true, focused: true },
+ { text: "6-add", selected: true },
+ { text: "5-add", selected: true },
+ { text: "0-add", selected: true },
+ { text: "10-add" },
+ { text: "11-add", selected: true },
+ { text: "12-add" },
+ { text: "13-add", selected: true },
+ { text: "3-add", selected: true },
+ { text: "7-add" },
+ { text: "8-add", selected: true },
+ { text: "9-add" },
+ { text: "1-add" },
+ ],
+ "Merged ranges together at end"
+ );
+
+ // Move block with non-selected boundaries into middle of selected.
+ widget.moveItems(5, 3, 6, reCreate);
+ assertState(
+ [
+ { text: "4-add" },
+ { text: "2-add", selected: true, focused: true },
+ { text: "6-add", selected: true },
+ { text: "10-add" },
+ { text: "11-add", selected: true },
+ { text: "12-add" },
+ { text: "13-add", selected: true },
+ { text: "3-add", selected: true },
+ { text: "7-add" },
+ { text: "5-add", selected: true },
+ { text: "0-add", selected: true },
+ { text: "8-add", selected: true },
+ { text: "9-add" },
+ { text: "1-add" },
+ ],
+ "Split range block"
+ );
+
+ // Move block with selected at start into middle of selected.
+ widget.moveItems(1, 6, 5, reCreate);
+ assertState(
+ [
+ { text: "4-add" },
+ { text: "13-add", selected: true },
+ { text: "3-add", selected: true },
+ { text: "7-add" },
+ { text: "5-add", selected: true },
+ { text: "0-add", selected: true },
+ { text: "2-add", selected: true, focused: true },
+ { text: "6-add", selected: true },
+ { text: "10-add" },
+ { text: "11-add", selected: true },
+ { text: "12-add" },
+ { text: "8-add", selected: true },
+ { text: "9-add" },
+ { text: "1-add" },
+ ],
+ "Merged ranges together at start"
+ );
+
+ // Move block with selected at end into middle of selected.
+ widget.moveItems(8, 6, 4, reCreate);
+ assertState(
+ [
+ { text: "4-add" },
+ { text: "13-add", selected: true },
+ { text: "3-add", selected: true },
+ { text: "7-add" },
+ { text: "5-add", selected: true },
+ { text: "0-add", selected: true },
+ { text: "10-add" },
+ { text: "11-add", selected: true },
+ { text: "12-add" },
+ { text: "8-add", selected: true },
+ { text: "2-add", selected: true, focused: true },
+ { text: "6-add", selected: true },
+ { text: "9-add" },
+ { text: "1-add" },
+ ],
+ "Merged ranges together at end"
+ );
+
+ // Move selected into non-selected region and move to start.
+ widget.moveItems(4, 0, 6, reCreate);
+ assertState(
+ [
+ { text: "5-add", selected: true },
+ { text: "0-add", selected: true },
+ { text: "10-add" },
+ { text: "11-add", selected: true },
+ { text: "12-add" },
+ { text: "8-add", selected: true },
+ { text: "4-add" },
+ { text: "13-add", selected: true },
+ { text: "3-add", selected: true },
+ { text: "7-add" },
+ { text: "2-add", selected: true, focused: true },
+ { text: "6-add", selected: true },
+ { text: "9-add" },
+ { text: "1-add" },
+ ],
+ "Merged ranges together at end"
+ );
+
+ // Remove gap between two selections and move to end.
+ widget.moveItems(2, 9, 5, reCreate);
+ assertState(
+ [
+ { text: "5-add", selected: true },
+ { text: "0-add", selected: true },
+ { text: "13-add", selected: true },
+ { text: "3-add", selected: true },
+ { text: "7-add" },
+ { text: "2-add", selected: true, focused: true },
+ { text: "6-add", selected: true },
+ { text: "9-add" },
+ { text: "1-add" },
+ { text: "10-add" },
+ { text: "11-add", selected: true },
+ { text: "12-add" },
+ { text: "8-add", selected: true },
+ { text: "4-add" },
+ ],
+ "Merged ranges together"
+ );
+
+ // Navigation still works.
+ EventUtils.synthesizeKey("KEY_ArrowLeft", {}, win);
+ assertState(
+ [
+ { text: "5-add" },
+ { text: "0-add" },
+ { text: "13-add" },
+ { text: "3-add" },
+ { text: "7-add" },
+ { text: "2-add" },
+ { text: "6-add", selected: true, focused: true },
+ { text: "9-add" },
+ { text: "1-add" },
+ { text: "10-add" },
+ { text: "11-add" },
+ { text: "12-add" },
+ { text: "8-add" },
+ { text: "4-add" },
+ ],
+ "Move by one index and single select"
+ );
+}
+
+// Moving items in the widget will move focus and selection with the moved
+// items.
+add_task(function test_move_items() {
+ for (let model of selectionModels) {
+ // We want to be sure the methods work with or without re-creating the
+ // item elements.
+ subtest_move_items(model, false);
+ subtest_move_items(model, true);
+ }
+});
+
+// Test that dragging is possible.
+add_task(function test_can_drag_items() {
+ /**
+ * Assert that dragging can occur and takes place with the expected selection.
+ *
+ * @param {number} index - The index of the item to start dragging on. We also
+ * expect this item to have focus during and after dragging.
+ * @param {number[]} selection - The expected selection during and after
+ * dragging.
+ * @param {string} msg - A message to use in assertions.
+ */
+ function assertDragstart(index, selection, msg) {
+ let element = widget.items[index].element;
+ let eventFired = false;
+
+ let dragstartListener = event => {
+ eventFired = true;
+ Assert.ok(
+ element.contains(event.target),
+ `Item ${index} contains the dragstart target`
+ );
+ assertFocus({ index }, `Item ${index} has focus in dragstart: ${msg}`);
+ assertSelection(selection, `Selection in dragstart: ${msg}`);
+ };
+ widget.addEventListener("dragstart", dragstartListener, true);
+
+ // Synthesize the start of a drag.
+ let rect = element.getBoundingClientRect();
+ let x = rect.left + rect.width / 2;
+ let y = rect.top + rect.height / 2;
+ EventUtils.synthesizeMouseAtPoint(x, y, { type: "mousedown" }, win);
+ EventUtils.synthesizeMouseAtPoint(x, y, { type: "mousemove" }, win);
+ EventUtils.synthesizeMouseAtPoint(x, y + 60, { type: "mousemove" }, win);
+ // Don't care about ending the drag.
+
+ Assert.ok(eventFired, `dragstart event fired: ${msg}`);
+ widget.removeEventListener("dragstart", dragstartListener, true);
+ assertSelection(selection, `Same selection after dragging: ${msg}`);
+ assertFocus(
+ { index },
+ `Item ${index} still has focus after dragging: ${msg}`
+ );
+ }
+
+ for (let model of selectionModels) {
+ reset({ model, draggable: true });
+ widget.addItems(0, ["First", "Second", "Third"]);
+ assertFocus({ element: before }, "Focus outside widget");
+ assertSelection([], "No initial selection");
+ assertDragstart(1, [1], "First drag with no focus or selection");
+
+ assertDragstart(1, [1], "Already selected item");
+ assertDragstart(2, [2], "Non-selected item");
+
+ reset({ model, draggable: true });
+ widget.addItems(0, ["First", "Second", "Third"]);
+ widget.selectSingleItem(1);
+ assertFocus({ element: before }, "Focus outside widget");
+ assertSelection([1], "Initial selection on item 1");
+ assertDragstart(1, [1], "First drag on selected item");
+
+ reset({ model, draggable: true });
+ widget.addItems(0, ["First", "Second", "Third", "Fourth", "Fifth"]);
+ widget.selectSingleItem(3);
+ assertFocus({ element: before }, "Focus outside widget");
+ assertSelection([3], "Initial selection on item 3");
+ assertDragstart(2, [2], "First drag on non-selected item");
+
+ // With focus split from selected.
+ if (model == "focus") {
+ continue;
+ }
+ EventUtils.synthesizeKey("KEY_ArrowLeft", { ctrlKey: true }, win);
+ assertFocus({ index: 3 }, "Focus on item 3");
+ assertSelection([2], "Item 2 is selected");
+ assertDragstart(3, [3], "Non-selected but focused item");
+
+ EventUtils.synthesizeKey("KEY_ArrowRight", { ctrlKey: true }, win);
+ assertFocus({ index: 2 }, "Focus on item 2");
+ assertSelection([3], "Item 3 is selected");
+ assertDragstart(3, [3], "Selected but non-focused item");
+
+ // With mutli-selection.
+ if (model == "browse") {
+ continue;
+ }
+
+ // Clicking a non-selected item will change to selection to the single item
+ // before dragging.
+ EventUtils.synthesizeKey("KEY_ArrowRight", { ctrlKey: true }, win);
+ EventUtils.synthesizeKey("KEY_ArrowRight", { ctrlKey: true }, win);
+ EventUtils.synthesizeKey(" ", { ctrlKey: true }, win);
+ assertFocus({ index: 1 }, "Focus on item 1");
+ assertSelection([1, 3], "Multi selection");
+ assertDragstart(2, [2], "Selection moves to item 2 before drag");
+
+ // Clicking a selected item will keep the same selection for dragging.
+ EventUtils.synthesizeKey("KEY_ArrowLeft", { ctrlKey: true }, win);
+ EventUtils.synthesizeKey("KEY_ArrowLeft", { ctrlKey: true }, win);
+ EventUtils.synthesizeKey(" ", { ctrlKey: true }, win);
+ assertFocus({ index: 4 }, "Focus on item 4");
+ assertSelection([2, 4], "Multi selection");
+ assertDragstart(
+ 4,
+ [2, 4],
+ "Selection same when dragging selected and focused"
+ );
+ assertDragstart(
+ 2,
+ [2, 4],
+ "Selection same when dragging selected and non-focussed"
+ );
+ }
+});