/* 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" ); } });