// This file expects the following globals to be defined at various times. /* globals getCustomTreeViewCellInfo */ // This files relies on these specific Chrome/XBL globals /* globals TreeColumns, TreeColumn */ var columns_simpletree = [ { name: "name", label: "Name", key: true, properties: "one two" }, { name: "address", label: "Address" }, ]; var columns_hiertree = [ { name: "name", label: "Name", primary: true, key: true, properties: "one two", }, { name: "address", label: "Address" }, { name: "planet", label: "Planet" }, { name: "gender", label: "Gender", cycler: true }, ]; // XXXndeakin still to add some tests for: // cycler columns, checkbox cells // this test function expects a tree to have 8 rows in it when it isn't // expanded. The tree should only display four rows at a time. If editable, // the cell at row 1 and column 0 must be editable, and the cell at row 2 and // column 1 must not be editable. async function testtag_tree( treeid, treerowinfoid, seltype, columnstype, testid ) { // Stop keystrokes that aren't handled by the tree from leaking out and // scrolling the main Mochitests window! function preventDefault(event) { event.preventDefault(); } document.addEventListener("keypress", preventDefault); var multiple = seltype == "multiple"; var tree = document.getElementById(treeid); var treerowinfo = document.getElementById(treerowinfoid); var rowInfo; if (testid == "tree view") { rowInfo = getCustomTreeViewCellInfo(); } else { rowInfo = convertDOMtoTreeRowInfo(treerowinfo, 0, { value: -1 }); } var columnInfo = columnstype == "simple" ? columns_simpletree : columns_hiertree; is(tree.selType, seltype == "multiple" ? "" : seltype, testid + " seltype"); // note: the functions below should be in this order due to changes made in later tests await testtag_tree_treecolpicker(tree, columnInfo, testid); testtag_tree_columns(tree, columnInfo, testid); testtag_tree_TreeSelection(tree, testid, multiple); testtag_tree_TreeSelection_UI(tree, testid, multiple); testtag_tree_TreeView(tree, testid, rowInfo); is(tree.editable, false, "tree should not be editable"); // currently, the editable flag means that tree editing cannot be invoked // by the user. However, editing can still be started with a script. is(tree.editingRow, -1, testid + " initial editingRow"); is(tree.editingColumn, null, testid + " initial editingColumn"); testtag_tree_UI_editing(tree, testid, rowInfo); is( tree.editable, false, "tree should not be editable after testtag_tree_UI_editing" ); // currently, the editable flag means that tree editing cannot be invoked // by the user. However, editing can still be started with a script. is(tree.editingRow, -1, testid + " initial editingRow (continued)"); is(tree.editingColumn, null, testid + " initial editingColumn (continued)"); var ecolumn = tree.columns[0]; ok( !tree.startEditing(1, ecolumn), "non-editable trees shouldn't start editing" ); is( tree.editingRow, -1, testid + " failed startEditing shouldn't set editingRow" ); is( tree.editingColumn, null, testid + " failed startEditing shouldn't set editingColumn" ); tree.editable = true; ok(tree.startEditing(1, ecolumn), "startEditing should have returned true"); is(tree.editingRow, 1, testid + " startEditing editingRow"); is(tree.editingColumn, ecolumn, testid + " startEditing editingColumn"); is( tree.getAttribute("editing"), "true", testid + " startEditing editing attribute" ); tree.stopEditing(true); is(tree.editingRow, -1, testid + " stopEditing editingRow"); is(tree.editingColumn, null, testid + " stopEditing editingColumn"); is( tree.hasAttribute("editing"), false, testid + " stopEditing editing attribute" ); tree.startEditing(-1, ecolumn); is( tree.editingRow == -1 && tree.editingColumn == null, true, testid + " startEditing -1 editingRow" ); tree.startEditing(15, ecolumn); is( tree.editingRow == -1 && tree.editingColumn == null, true, testid + " startEditing 15 editingRow" ); tree.startEditing(1, null); is( tree.editingRow == -1 && tree.editingColumn == null, true, testid + " startEditing null column editingRow" ); tree.startEditing(2, tree.columns[1]); is( tree.editingRow == -1 && tree.editingColumn == null, true, testid + " startEditing non editable cell editingRow" ); tree.startEditing(1, ecolumn); var inputField = tree.inputField; is(inputField.localName, "input", testid + "inputField"); inputField.value = "Changed Value"; tree.stopEditing(true); is( tree.view.getCellText(1, ecolumn), "Changed Value", testid + "edit cell accept" ); // this cell can be edited, but stopEditing(false) means don't accept the change. tree.startEditing(1, ecolumn); inputField.value = "Second Value"; tree.stopEditing(false); is( tree.view.getCellText(1, ecolumn), "Changed Value", testid + "edit cell no accept" ); tree.editable = false; // do the sorting tests last as it will cause the rows to rearrange // skip them for the custom tree view if (testid != "tree view") { testtag_tree_TreeView_rows_sort(tree, testid, rowInfo); } testtag_tree_wheel(tree); document.removeEventListener("keypress", preventDefault); SimpleTest.finish(); } async function testtag_tree_treecolpicker(tree, expectedColumns, testid) { testid += " "; async function showAndHideTreecolpicker() { let treecolpicker = tree.querySelector("treecolpicker"); let treecolpickerMenupopup = treecolpicker.querySelector("menupopup"); await new Promise(resolve => { treecolpickerMenupopup.addEventListener("popupshown", resolve, { once: true, }); treecolpicker.querySelector("button").click(); }); let menuitems = treecolpicker.querySelectorAll("menuitem"); // Ignore the last "Restore Column Order" menu in the count: is( menuitems.length - 1, expectedColumns.length, testid + "Same number of columns" ); for (var c = 0; c < expectedColumns.length; c++) { is( menuitems[c].textContent, expectedColumns[c].label, testid + "treecolpicker menu matches" ); ok( !menuitems[c].querySelector("label").hidden, testid + "label not hidden" ); } await new Promise(resolve => { treecolpickerMenupopup.addEventListener("popuphidden", resolve, { once: true, }); treecolpickerMenupopup.hidePopup(); }); } // Regression test for Bug 1549931 (menuitem content being hidden upon second open) await showAndHideTreecolpicker(); await showAndHideTreecolpicker(); } function testtag_tree_columns(tree, expectedColumns, testid) { testid += " "; var columns = tree.columns; is( TreeColumns.isInstance(columns), true, testid + "columns is a TreeColumns" ); is(columns.count, expectedColumns.length, testid + "TreeColumns count"); is(columns.length, expectedColumns.length, testid + "TreeColumns length"); var treecols = tree.getElementsByTagName("treecols")[0]; var treecol = treecols.getElementsByTagName("treecol"); var x = 0; var primary = null, sorted = null, key = null; for (var c = 0; c < expectedColumns.length; c++) { var adjtestid = testid + " column " + c + " "; var column = columns[c]; var expectedColumn = expectedColumns[c]; is(columns.getColumnAt(c), column, adjtestid + "getColumnAt"); is( columns.getNamedColumn(expectedColumn.name), column, adjtestid + "getNamedColumn" ); is(columns.getColumnFor(treecol[c]), column, adjtestid + "getColumnFor"); if (expectedColumn.primary) { primary = column; } if (expectedColumn.sorted) { sorted = column; } if (expectedColumn.key) { key = column; } // XXXndeakin on Windows and Linux, some columns are one pixel to the // left of where they should be. Could just be a rounding issue. var adj = 1; is( column.x + adj >= x, true, adjtestid + "position is after last column " + column.x + "," + column.width + "," + x ); is(column.width > 0, true, adjtestid + "width is greater than 0"); x = column.x + column.width; // now check the TreeColumn properties is(TreeColumn.isInstance(column), true, adjtestid + "is a TreeColumn"); is(column.element, treecol[c], adjtestid + "element is treecol"); is(column.columns, columns, adjtestid + "columns is TreeColumns"); is(column.id, expectedColumn.name, adjtestid + "name"); is(column.index, c, adjtestid + "index"); is(column.primary, primary == column, adjtestid + "column is primary"); is( column.cycler, "cycler" in expectedColumn && expectedColumn.cycler, adjtestid + "column is cycler" ); is( column.editable, "editable" in expectedColumn && expectedColumn.editable, adjtestid + "column is editable" ); is( column.type, "type" in expectedColumn ? expectedColumn.type : 1, adjtestid + "type" ); is( column.getPrevious(), c > 0 ? columns[c - 1] : null, adjtestid + "getPrevious" ); is( column.getNext(), c < columns.length - 1 ? columns[c + 1] : null, adjtestid + "getNext" ); // check the view's getColumnProperties method var properties = tree.view.getColumnProperties(column); var expectedProperties = expectedColumn.properties; is(properties, expectedProperties || "", adjtestid + "getColumnProperties"); } is(columns.getFirstColumn(), columns[0], testid + "getFirstColumn"); is( columns.getLastColumn(), columns[columns.length - 1], testid + "getLastColumn" ); is(columns.getPrimaryColumn(), primary, testid + "getPrimaryColumn"); is(columns.getSortedColumn(), sorted, testid + "getSortedColumn"); is(columns.getKeyColumn(), key, testid + "getKeyColumn"); is(columns.getColumnAt(-1), null, testid + "getColumnAt under"); is(columns.getColumnAt(columns.length), null, testid + "getColumnAt over"); is(columns.getNamedColumn(""), null, testid + "getNamedColumn null"); is( columns.getNamedColumn("unknown"), null, testid + "getNamedColumn unknown" ); is(columns.getColumnFor(null), null, testid + "getColumnFor null"); is(columns.getColumnFor(tree), null, testid + "getColumnFor other"); } function testtag_tree_TreeSelection(tree, testid, multiple) { testid += " selection "; var selection = tree.view.selection; is( selection instanceof Ci.nsITreeSelection, true, testid + "selection is a TreeSelection" ); is(selection.single, !multiple, testid + "single"); testtag_tree_TreeSelection_State(tree, testid + "initial", -1, []); is(selection.shiftSelectPivot, -1, testid + "initial shiftSelectPivot"); selection.currentIndex = 2; testtag_tree_TreeSelection_State(tree, testid + "set currentIndex", 2, []); tree.currentIndex = 3; testtag_tree_TreeSelection_State( tree, testid + "set tree.currentIndex", 3, [] ); // test the select() method, which should deselect all rows and select // a single row selection.select(1); testtag_tree_TreeSelection_State(tree, testid + "select 1", 1, [1]); selection.select(3); testtag_tree_TreeSelection_State(tree, testid + "select 2", 3, [3]); selection.select(3); testtag_tree_TreeSelection_State(tree, testid + "select same", 3, [3]); selection.currentIndex = 1; testtag_tree_TreeSelection_State( tree, testid + "set currentIndex with single selection", 1, [3] ); tree.currentIndex = 2; testtag_tree_TreeSelection_State( tree, testid + "set tree.currentIndex with single selection", 2, [3] ); // check the toggleSelect method. In single selection mode, it only toggles on when // there isn't currently a selection. selection.toggleSelect(2); testtag_tree_TreeSelection_State( tree, testid + "toggleSelect 1", 2, multiple ? [2, 3] : [3] ); selection.toggleSelect(2); selection.toggleSelect(3); testtag_tree_TreeSelection_State(tree, testid + "toggleSelect 2", 3, []); // the current index doesn't change after a selectAll, so it should still be set to 1 // selectAll has no effect on single selection trees selection.currentIndex = 1; selection.selectAll(); testtag_tree_TreeSelection_State( tree, testid + "selectAll 1", 1, multiple ? [0, 1, 2, 3, 4, 5, 6, 7] : [] ); selection.toggleSelect(2); testtag_tree_TreeSelection_State( tree, testid + "toggleSelect after selectAll", 2, multiple ? [0, 1, 3, 4, 5, 6, 7] : [2] ); selection.clearSelection(); testtag_tree_TreeSelection_State(tree, testid + "clearSelection", 2, []); selection.toggleSelect(3); selection.toggleSelect(1); if (multiple) { selection.selectAll(); testtag_tree_TreeSelection_State( tree, testid + "selectAll 2", 1, [0, 1, 2, 3, 4, 5, 6, 7] ); } selection.currentIndex = 2; selection.clearSelection(); testtag_tree_TreeSelection_State( tree, testid + "clearSelection after selectAll", 2, [] ); is(selection.shiftSelectPivot, -1, testid + "shiftSelectPivot set to -1"); // rangedSelect and clearRange set the currentIndex to the endIndex. The // shiftSelectPivot property will be set to startIndex. selection.rangedSelect(1, 3, false); testtag_tree_TreeSelection_State( tree, testid + "rangedSelect no augment", multiple ? 3 : 2, multiple ? [1, 2, 3] : [] ); is( selection.shiftSelectPivot, multiple ? 1 : -1, testid + "shiftSelectPivot after rangedSelect no augment" ); if (multiple) { selection.select(1); selection.rangedSelect(0, 2, true); testtag_tree_TreeSelection_State( tree, testid + "rangedSelect augment", 2, [0, 1, 2] ); is( selection.shiftSelectPivot, 0, testid + "shiftSelectPivot after rangedSelect augment" ); selection.clearRange(1, 3); testtag_tree_TreeSelection_State(tree, testid + "rangedSelect augment", 3, [ 0, ]); // check that rangedSelect can take a start value higher than end selection.rangedSelect(3, 1, false); testtag_tree_TreeSelection_State( tree, testid + "rangedSelect reverse", 1, [1, 2, 3] ); is( selection.shiftSelectPivot, 3, testid + "shiftSelectPivot after rangedSelect reverse" ); // check that setting the current index doesn't change the selection selection.currentIndex = 0; testtag_tree_TreeSelection_State( tree, testid + "currentIndex with range selection", 0, [1, 2, 3] ); } // both values of rangedSelect may be the same selection.rangedSelect(2, 2, false); testtag_tree_TreeSelection_State(tree, testid + "rangedSelect one row", 2, [ 2, ]); is( selection.shiftSelectPivot, 2, testid + "shiftSelectPivot after selecting one row" ); if (multiple) { selection.rangedSelect(2, 3, true); // a start index of -1 means from the last point selection.rangedSelect(-1, 0, true); testtag_tree_TreeSelection_State( tree, testid + "rangedSelect -1 existing selection", 0, [0, 1, 2, 3] ); is( selection.shiftSelectPivot, 2, testid + "shiftSelectPivot after -1 existing selection" ); selection.currentIndex = 2; selection.rangedSelect(-1, 0, false); testtag_tree_TreeSelection_State( tree, testid + "rangedSelect -1 from currentIndex", 0, [0, 1, 2] ); is( selection.shiftSelectPivot, 2, testid + "shiftSelectPivot -1 from currentIndex" ); } // XXXndeakin need to test out of range values but these don't work properly /* selection.select(-1); testtag_tree_TreeSelection_State(tree, testid + "rangedSelect augment -1", -1, []); selection.select(8); testtag_tree_TreeSelection_State(tree, testid + "rangedSelect augment 8", 3, [0]); */ } function testtag_tree_TreeSelection_UI(tree, testid, multiple) { testid += " selection UI "; var selection = tree.view.selection; selection.clearSelection(); selection.currentIndex = 0; tree.focus(); var keydownFired = 0; var keypressFired = 0; function keydownListener() { keydownFired++; } function keypressListener() { keypressFired++; } // check that cursor up and down keys navigate up and down // select event fires after a delay so don't expect it. The reason it fires after a delay // is so that cursor navigation allows quicking skimming over a set of items without // actually firing events in-between, improving performance. The select event will only // be fired on the row where the cursor stops. window.addEventListener("keydown", keydownListener); window.addEventListener("keypress", keypressListener); synthesizeKeyExpectEvent("VK_DOWN", {}, tree, "!select", "key down"); testtag_tree_TreeSelection_State(tree, testid + "key down", 1, [1], 0); synthesizeKeyExpectEvent("VK_UP", {}, tree, "!select", "key up"); testtag_tree_TreeSelection_State(tree, testid + "key up", 0, [0], 0); synthesizeKeyExpectEvent("VK_UP", {}, tree, "!select", "key up at start"); testtag_tree_TreeSelection_State(tree, testid + "key up at start", 0, [0], 0); // pressing down while the last row is selected should not fire a select event, // as the selection won't have changed. Also the view is not scrolled in this case. selection.select(7); synthesizeKeyExpectEvent("VK_DOWN", {}, tree, "!select", "key down at end"); testtag_tree_TreeSelection_State(tree, testid + "key down at end", 7, [7], 0); // pressing keys while at the edge of the visible rows should scroll the list tree.scrollToRow(4); selection.select(4); synthesizeKeyExpectEvent("VK_UP", {}, tree, "!select", "key up with scroll"); is(tree.getFirstVisibleRow(), 3, testid + "key up with scroll"); tree.scrollToRow(0); selection.select(3); synthesizeKeyExpectEvent( "VK_DOWN", {}, tree, "!select", "key down with scroll" ); is(tree.getFirstVisibleRow(), 1, testid + "key down with scroll"); // accel key and cursor movement adjust currentIndex but should not change // the selection. In single selection mode, the selection will not change, // but instead will just scroll up or down a line tree.scrollToRow(0); selection.select(1); synthesizeKeyExpectEvent( "VK_DOWN", { accelKey: true }, tree, "!select", "key down with accel" ); testtag_tree_TreeSelection_State( tree, testid + "key down with accel", multiple ? 2 : 1, [1] ); if (!multiple) { is(tree.getFirstVisibleRow(), 1, testid + "key down with accel and scroll"); } tree.scrollToRow(4); selection.select(4); synthesizeKeyExpectEvent( "VK_UP", { accelKey: true }, tree, "!select", "key up with accel" ); testtag_tree_TreeSelection_State( tree, testid + "key up with accel", multiple ? 3 : 4, [4] ); if (!multiple) { is(tree.getFirstVisibleRow(), 3, testid + "key up with accel and scroll"); } // do this three times, one for each state of pageUpOrDownMovesSelection, // and then once with the accel key pressed for (let t = 0; t < 3; t++) { let testidmod = ""; if (t == 2) { testidmod = " with accel"; } else if (t == 1) { testidmod = " rev"; } var keymod = t == 2 ? { accelKey: true } : {}; var moveselection = tree.pageUpOrDownMovesSelection; if (t == 2) { moveselection = !moveselection; } tree.scrollToRow(4); selection.currentIndex = 6; selection.select(6); var expected = moveselection ? 4 : 6; synthesizeKeyExpectEvent( "VK_PAGE_UP", keymod, tree, "!select", "key page up" ); testtag_tree_TreeSelection_State( tree, testid + "key page up" + testidmod, expected, [expected], moveselection ? 4 : 0 ); expected = moveselection ? 0 : 6; synthesizeKeyExpectEvent( "VK_PAGE_UP", keymod, tree, "!select", "key page up again" ); testtag_tree_TreeSelection_State( tree, testid + "key page up again" + testidmod, expected, [expected], 0 ); expected = moveselection ? 0 : 6; synthesizeKeyExpectEvent( "VK_PAGE_UP", keymod, tree, "!select", "key page up at start" ); testtag_tree_TreeSelection_State( tree, testid + "key page up at start" + testidmod, expected, [expected], 0 ); tree.scrollToRow(0); selection.currentIndex = 1; selection.select(1); expected = moveselection ? 3 : 1; synthesizeKeyExpectEvent( "VK_PAGE_DOWN", keymod, tree, "!select", "key page down" ); testtag_tree_TreeSelection_State( tree, testid + "key page down" + testidmod, expected, [expected], moveselection ? 0 : 4 ); expected = moveselection ? 7 : 1; synthesizeKeyExpectEvent( "VK_PAGE_DOWN", keymod, tree, "!select", "key page down again" ); testtag_tree_TreeSelection_State( tree, testid + "key page down again" + testidmod, expected, [expected], 4 ); expected = moveselection ? 7 : 1; synthesizeKeyExpectEvent( "VK_PAGE_DOWN", keymod, tree, "!select", "key page down at start" ); testtag_tree_TreeSelection_State( tree, testid + "key page down at start" + testidmod, expected, [expected], 4 ); if (t < 2) { tree.pageUpOrDownMovesSelection = !tree.pageUpOrDownMovesSelection; } } tree.scrollToRow(4); selection.select(6); synthesizeKeyExpectEvent("VK_HOME", {}, tree, "!select", "key home"); testtag_tree_TreeSelection_State(tree, testid + "key home", 0, [0], 0); tree.scrollToRow(0); selection.select(1); synthesizeKeyExpectEvent("VK_END", {}, tree, "!select", "key end"); testtag_tree_TreeSelection_State(tree, testid + "key end", 7, [7], 4); // in single selection mode, the selection doesn't change in this case tree.scrollToRow(4); selection.select(6); synthesizeKeyExpectEvent( "VK_HOME", { accelKey: true }, tree, "!select", "key home with accel" ); testtag_tree_TreeSelection_State( tree, testid + "key home with accel", multiple ? 0 : 6, [6], 0 ); tree.scrollToRow(0); selection.select(1); synthesizeKeyExpectEvent( "VK_END", { accelKey: true }, tree, "!select", "key end with accel" ); testtag_tree_TreeSelection_State( tree, testid + "key end with accel", multiple ? 7 : 1, [1], 4 ); // next, test cursor navigation with selection. Here the select event will be fired selection.select(1); var eventExpected = multiple ? "select" : "!select"; synthesizeKeyExpectEvent( "VK_DOWN", { shiftKey: true }, tree, eventExpected, "key shift down to select" ); testtag_tree_TreeSelection_State( tree, testid + "key shift down to select", multiple ? 2 : 1, multiple ? [1, 2] : [1] ); is( selection.shiftSelectPivot, multiple ? 1 : -1, testid + "key shift down to select shiftSelectPivot" ); synthesizeKeyExpectEvent( "VK_UP", { shiftKey: true }, tree, eventExpected, "key shift up to unselect" ); testtag_tree_TreeSelection_State( tree, testid + "key shift up to unselect", 1, [1] ); is( selection.shiftSelectPivot, multiple ? 1 : -1, testid + "key shift up to unselect shiftSelectPivot" ); if (multiple) { synthesizeKeyExpectEvent( "VK_UP", { shiftKey: true }, tree, "select", "key shift up to select" ); testtag_tree_TreeSelection_State( tree, testid + "key shift up to select", 0, [0, 1] ); is( selection.shiftSelectPivot, 1, testid + "key shift up to select shiftSelectPivot" ); synthesizeKeyExpectEvent( "VK_DOWN", { shiftKey: true }, tree, "select", "key shift down to unselect" ); testtag_tree_TreeSelection_State( tree, testid + "key shift down to unselect", 1, [1] ); is( selection.shiftSelectPivot, 1, testid + "key shift down to unselect shiftSelectPivot" ); } // do this twice, one for each state of pageUpOrDownMovesSelection, however // when selecting with the shift key, pageUpOrDownMovesSelection is ignored // and the selection always changes var lastidx = tree.view.rowCount - 1; for (let t = 0; t < 2; t++) { let testidmod = t == 0 ? "" : " rev"; // If the top or bottom visible row is the current row, pressing shift and // page down / page up selects one page up or one page down. Otherwise, the // selection is made to the top or bottom of the visible area. tree.scrollToRow(lastidx - 3); selection.currentIndex = 6; selection.select(6); synthesizeKeyExpectEvent( "VK_PAGE_UP", { shiftKey: true }, tree, eventExpected, "key shift page up" ); testtag_tree_TreeSelection_State( tree, testid + "key shift page up" + testidmod, multiple ? 4 : 6, multiple ? [4, 5, 6] : [6] ); if (multiple) { synthesizeKeyExpectEvent( "VK_PAGE_UP", { shiftKey: true }, tree, "select", "key shift page up again" ); testtag_tree_TreeSelection_State( tree, testid + "key shift page up again" + testidmod, 0, [0, 1, 2, 3, 4, 5, 6] ); // no change in the selection, so no select event should be fired synthesizeKeyExpectEvent( "VK_PAGE_UP", { shiftKey: true }, tree, "!select", "key shift page up at start" ); testtag_tree_TreeSelection_State( tree, testid + "key shift page up at start" + testidmod, 0, [0, 1, 2, 3, 4, 5, 6] ); // deselect by paging down again synthesizeKeyExpectEvent( "VK_PAGE_DOWN", { shiftKey: true }, tree, "select", "key shift page down deselect" ); testtag_tree_TreeSelection_State( tree, testid + "key shift page down deselect" + testidmod, 3, [3, 4, 5, 6] ); } tree.scrollToRow(1); selection.currentIndex = 2; selection.select(2); synthesizeKeyExpectEvent( "VK_PAGE_DOWN", { shiftKey: true }, tree, eventExpected, "key shift page down" ); testtag_tree_TreeSelection_State( tree, testid + "key shift page down" + testidmod, multiple ? 4 : 2, multiple ? [2, 3, 4] : [2] ); if (multiple) { synthesizeKeyExpectEvent( "VK_PAGE_DOWN", { shiftKey: true }, tree, "select", "key shift page down again" ); testtag_tree_TreeSelection_State( tree, testid + "key shift page down again" + testidmod, 7, [2, 3, 4, 5, 6, 7] ); synthesizeKeyExpectEvent( "VK_PAGE_DOWN", { shiftKey: true }, tree, "!select", "key shift page down at start" ); testtag_tree_TreeSelection_State( tree, testid + "key shift page down at start" + testidmod, 7, [2, 3, 4, 5, 6, 7] ); synthesizeKeyExpectEvent( "VK_PAGE_UP", { shiftKey: true }, tree, "select", "key shift page up deselect" ); testtag_tree_TreeSelection_State( tree, testid + "key shift page up deselect" + testidmod, 4, [2, 3, 4] ); } // test when page down / page up is pressed when the view is scrolled such // that the selection is not visible if (multiple) { tree.scrollToRow(3); selection.currentIndex = 1; selection.select(1); synthesizeKeyExpectEvent( "VK_PAGE_DOWN", { shiftKey: true }, tree, eventExpected, "key shift page down with view scrolled down" ); testtag_tree_TreeSelection_State( tree, testid + "key shift page down with view scrolled down" + testidmod, 6, [1, 2, 3, 4, 5, 6], 3 ); tree.scrollToRow(2); selection.currentIndex = 6; selection.select(6); synthesizeKeyExpectEvent( "VK_PAGE_UP", { shiftKey: true }, tree, eventExpected, "key shift page up with view scrolled up" ); testtag_tree_TreeSelection_State( tree, testid + "key shift page up with view scrolled up" + testidmod, 2, [2, 3, 4, 5, 6], 2 ); tree.scrollToRow(2); selection.currentIndex = 0; selection.select(0); // don't expect the select event, as the selection won't have changed synthesizeKeyExpectEvent( "VK_PAGE_UP", { shiftKey: true }, tree, "!select", "key shift page up at start with view scrolled down" ); testtag_tree_TreeSelection_State( tree, testid + "key shift page up at start with view scrolled down" + testidmod, 0, [0], 0 ); tree.scrollToRow(0); selection.currentIndex = 7; selection.select(7); // don't expect the select event, as the selection won't have changed synthesizeKeyExpectEvent( "VK_PAGE_DOWN", { shiftKey: true }, tree, "!select", "key shift page down at end with view scrolled up" ); testtag_tree_TreeSelection_State( tree, testid + "key shift page down at end with view scrolled up" + testidmod, 7, [7], 4 ); } tree.pageUpOrDownMovesSelection = !tree.pageUpOrDownMovesSelection; } tree.scrollToRow(4); selection.select(5); synthesizeKeyExpectEvent( "VK_HOME", { shiftKey: true }, tree, eventExpected, "key shift home" ); testtag_tree_TreeSelection_State( tree, testid + "key shift home", multiple ? 0 : 5, multiple ? [0, 1, 2, 3, 4, 5] : [5], multiple ? 0 : 4 ); tree.scrollToRow(0); selection.select(3); synthesizeKeyExpectEvent( "VK_END", { shiftKey: true }, tree, eventExpected, "key shift end" ); testtag_tree_TreeSelection_State( tree, testid + "key shift end", multiple ? 7 : 3, multiple ? [3, 4, 5, 6, 7] : [3], multiple ? 4 : 0 ); // pressing space selects a row, pressing accel + space unselects a row selection.select(2); selection.currentIndex = 4; synthesizeKeyExpectEvent(" ", {}, tree, "select", "key space on"); // in single selection mode, space shouldn't do anything testtag_tree_TreeSelection_State( tree, testid + "key space on", 4, multiple ? [2, 4] : [2] ); if (multiple) { synthesizeKeyExpectEvent( " ", { accelKey: true }, tree, "select", "key space off" ); testtag_tree_TreeSelection_State(tree, testid + "key space off", 4, [2]); } // check that clicking on a row selects it tree.scrollToRow(0); selection.select(2); selection.currentIndex = 2; if (0) { // XXXndeakin disable these tests for now mouseOnCell(tree, 1, tree.columns[1], "mouse on row"); testtag_tree_TreeSelection_State( tree, testid + "mouse on row", 1, [1], 0, null ); } // restore the scroll position to the start of the page sendKey("HOME"); window.removeEventListener("keydown", keydownListener); window.removeEventListener("keypress", keypressListener); is(keydownFired, multiple ? 63 : 40, "keydown event wasn't fired properly"); is(keypressFired, multiple ? 2 : 1, "keypress event wasn't fired properly"); } function testtag_tree_UI_editing(tree, testid, rowInfo) { testid += " editing UI "; // check editing UI var ecolumn = tree.columns[0]; var rowIndex = 2; // temporary make the tree editable to test mouse double click var wasEditable = tree.editable; if (!wasEditable) { tree.editable = true; } // if this is a container save its current open status var row = rowInfo.rows[rowIndex]; var wasOpen = null; if (tree.view.isContainer(row)) { wasOpen = tree.view.isContainerOpen(row); } mouseDblClickOnCell(tree, rowIndex, ecolumn, testid + "edit on double click"); is(tree.editingColumn, ecolumn, testid + "editing column"); is(tree.editingRow, rowIndex, testid + "editing row"); // ensure that we don't expand an expandable container on edit if (wasOpen != null) { is( tree.view.isContainerOpen(row), wasOpen, testid + "opened container node on edit" ); } // ensure to restore editable attribute if (!wasEditable) { tree.editable = false; } var ci = tree.currentIndex; // cursor navigation should not change the selection while editing var testKey = function (key) { synthesizeKeyExpectEvent( key, {}, tree, "!select", "key " + key + " with editing" ); is( tree.editingRow == rowIndex && tree.editingColumn == ecolumn && tree.currentIndex == ci, true, testid + "key " + key + " while editing" ); }; testKey("VK_DOWN"); testKey("VK_UP"); testKey("VK_PAGE_DOWN"); testKey("VK_PAGE_UP"); testKey("VK_HOME"); testKey("VK_END"); // XXXndeakin figure out how to send characters to the textbox // inputField.inputField.focus() // synthesizeKeyExpectEvent(inputField.inputField, "b", null, ""); // tree.stopEditing(true); // is(tree.view.getCellText(0, ecolumn), "b", testid + "edit cell"); // Restore initial state. tree.stopEditing(false); } function testtag_tree_TreeView(tree, testid, rowInfo) { testid += " view "; var columns = tree.columns; var view = tree.view; is(view instanceof Ci.nsITreeView, true, testid + "view is a TreeView"); is(view.rowCount, rowInfo.rows.length, testid + "rowCount"); testtag_tree_TreeView_rows(tree, testid, rowInfo, 0); // note that this will only work for content trees currently view.setCellText(0, columns[1], "Changed Value"); is(view.getCellText(0, columns[1]), "Changed Value", "setCellText"); view.setCellValue(1, columns[0], "Another Changed Value"); is(view.getCellValue(1, columns[0]), "Another Changed Value", "setCellText"); } function testtag_tree_TreeView_rows(tree, testid, rowInfo, startRow) { var r; var columns = tree.columns; var view = tree.view; var length = rowInfo.rows.length; // methods to test along with the functions which determine the expected value var checkRowMethods = { isContainer(row) { return row.container; }, isContainerOpen() { return false; }, isContainerEmpty(row) { return row.children != null && !row.children.rows.length; }, isSeparator(row) { return row.separator; }, getRowProperties(row) { return row.properties; }, getLevel(row) { return row.level; }, getParentIndex(row) { return row.parent; }, hasNextSibling() { return r < startRow + length - 1; }, }; var checkCellMethods = { getCellText(row, cell) { return cell.label; }, getCellValue(row, cell) { return cell.value; }, getCellProperties(row, cell) { return cell.properties; }, isEditable(row, cell) { return cell.editable; }, getImageSrc(row, cell) { return cell.image; }, }; var failedMethods = {}; var checkMethod, actual, expected; var toggleOpenStateOK = true; for (r = startRow; r < length; r++) { var row = rowInfo.rows[r]; for (var c = 0; c < row.cells.length; c++) { var cell = row.cells[c]; for (checkMethod in checkCellMethods) { expected = checkCellMethods[checkMethod](row, cell); actual = view[checkMethod](r, columns[c]); if (actual !== expected) { failedMethods[checkMethod] = true; is( actual, expected, testid + "row " + r + " column " + c + " " + checkMethod + " is incorrect" ); } } } // compare row properties for (checkMethod in checkRowMethods) { expected = checkRowMethods[checkMethod](row, r); if (checkMethod == "hasNextSibling") { actual = view[checkMethod](r, r); } else { actual = view[checkMethod](r); } if (actual !== expected) { failedMethods[checkMethod] = true; is( actual, expected, testid + "row " + r + " " + checkMethod + " is incorrect" ); } } /* // open and recurse into containers if (row.container) { view.toggleOpenState(r); if (!view.isContainerOpen(r)) { toggleOpenStateOK = false; is(view.isContainerOpen(r), true, testid + "row " + r + " toggleOpenState open"); } testtag_tree_TreeView_rows(tree, testid + "container " + r + " ", row.children, r + 1); view.toggleOpenState(r); if (view.isContainerOpen(r)) { toggleOpenStateOK = false; is(view.isContainerOpen(r), false, testid + "row " + r + " toggleOpenState close"); } } */ } for (var failedMethod in failedMethods) { if (failedMethod in checkRowMethods) { delete checkRowMethods[failedMethod]; } if (failedMethod in checkCellMethods) { delete checkCellMethods[failedMethod]; } } for (checkMethod in checkRowMethods) { is(checkMethod + " ok", checkMethod + " ok", testid + checkMethod); } for (checkMethod in checkCellMethods) { is(checkMethod + " ok", checkMethod + " ok", testid + checkMethod); } if (toggleOpenStateOK) { is("toggleOpenState ok", "toggleOpenState ok", testid + "toggleOpenState"); } } function testtag_tree_TreeView_rows_sort(tree) { // check if cycleHeader sorts the columns var columnIndex = 0; var view = tree.view; var column = tree.columns[columnIndex]; var columnElement = column.element; var sortkey = columnElement.getAttribute("sort"); if (sortkey) { view.cycleHeader(column); is(tree.getAttribute("sort"), sortkey, "cycleHeader sort"); is( tree.getAttribute("sortDirection"), "ascending", "cycleHeader sortDirection ascending" ); is( columnElement.getAttribute("sortDirection"), "ascending", "cycleHeader column sortDirection" ); is( columnElement.getAttribute("sortActive"), "true", "cycleHeader column sortActive" ); view.cycleHeader(column); is( tree.getAttribute("sortDirection"), "descending", "cycleHeader sortDirection descending" ); is( columnElement.getAttribute("sortDirection"), "descending", "cycleHeader column sortDirection descending" ); view.cycleHeader(column); is( tree.getAttribute("sortDirection"), "", "cycleHeader sortDirection natural" ); is( columnElement.getAttribute("sortDirection"), "", "cycleHeader column sortDirection natural" ); // XXXndeakin content view isSorted needs to be tested } // Check that clicking on column header sorts the column. var columns = getSortedColumnArray(tree); is( columnElement.getAttribute("sortDirection"), "", "cycleHeader column sortDirection" ); // Click once on column header and check sorting has cycled once. mouseClickOnColumnHeader(columns, columnIndex, 0, 1); is( columnElement.getAttribute("sortDirection"), "ascending", "single click cycleHeader column sortDirection ascending" ); // Now simulate a double click. mouseClickOnColumnHeader(columns, columnIndex, 0, 2); if (navigator.platform.indexOf("Win") == 0) { // Windows cycles only once on double click. is( columnElement.getAttribute("sortDirection"), "descending", "double click cycleHeader column sortDirection descending" ); // 1 single clicks should restore natural sorting. mouseClickOnColumnHeader(columns, columnIndex, 0, 1); } // Check we have gone back to natural sorting. is( columnElement.getAttribute("sortDirection"), "", "cycleHeader column sortDirection" ); columnElement.setAttribute("sorthints", "twostate"); view.cycleHeader(column); is( tree.getAttribute("sortDirection"), "ascending", "cycleHeader sortDirection ascending twostate" ); view.cycleHeader(column); is( tree.getAttribute("sortDirection"), "descending", "cycleHeader sortDirection ascending twostate" ); view.cycleHeader(column); is( tree.getAttribute("sortDirection"), "ascending", "cycleHeader sortDirection ascending twostate again" ); columnElement.removeAttribute("sorthints"); view.cycleHeader(column); view.cycleHeader(column); is( columnElement.getAttribute("sortDirection"), "", "cycleHeader column sortDirection reset" ); } // checks if the current and selected rows are correct // current is the index of the current row // selected is an array of the indicies of the selected rows // viewidx is the row that should be visible at the top of the tree function testtag_tree_TreeSelection_State( tree, testid, current, selected, viewidx ) { var selection = tree.view.selection; is(selection.count, selected.length, testid + " count"); is(tree.currentIndex, current, testid + " currentIndex"); is(selection.currentIndex, current, testid + " TreeSelection currentIndex"); if (viewidx !== null && viewidx !== undefined) { is(tree.getFirstVisibleRow(), viewidx, testid + " first visible row"); } var actualSelected = []; var count = tree.view.rowCount; for (var s = 0; s < count; s++) { if (selection.isSelected(s)) { actualSelected.push(s); } } is( compareArrays(selected, actualSelected), true, testid + " selection [" + selected + "]" ); actualSelected = []; var rangecount = selection.getRangeCount(); for (var r = 0; r < rangecount; r++) { var start = {}, end = {}; selection.getRangeAt(r, start, end); for (var rs = start.value; rs <= end.value; rs++) { actualSelected.push(rs); } } is( compareArrays(selected, actualSelected), true, testid + " range selection [" + selected + "]" ); } function testtag_tree_column_reorder() { // Make sure the tree is scrolled into the view, otherwise the test will // fail var testframe = window.parent.document.getElementById("testframe"); if (testframe) { testframe.scrollIntoView(); } var tree = document.getElementById("tree-column-reorder"); var numColumns = tree.columns.count; var reference = []; for (let i = 0; i < numColumns; i++) { reference.push("col_" + i); } // Drag the first column to each position for (let i = 0; i < numColumns - 1; i++) { synthesizeColumnDrag(tree, i, i + 1, true); arrayMove(reference, i, i + 1, true); checkColumns(tree, reference, "drag first column right"); } // And back for (let i = numColumns - 1; i >= 1; i--) { synthesizeColumnDrag(tree, i, i - 1, false); arrayMove(reference, i, i - 1, false); checkColumns(tree, reference, "drag last column left"); } // Drag each column one column left for (let i = 1; i < numColumns; i++) { synthesizeColumnDrag(tree, i, i - 1, false); arrayMove(reference, i, i - 1, false); checkColumns(tree, reference, "drag each column left"); } // And back for (let i = numColumns - 2; i >= 0; i--) { synthesizeColumnDrag(tree, i, i + 1, true); arrayMove(reference, i, i + 1, true); checkColumns(tree, reference, "drag each column right"); } // Drag each column 5 to the right for (let i = 0; i < numColumns - 5; i++) { synthesizeColumnDrag(tree, i, i + 5, true); arrayMove(reference, i, i + 5, true); checkColumns(tree, reference, "drag each column 5 to the right"); } // And to the left for (let i = numColumns - 6; i >= 5; i--) { synthesizeColumnDrag(tree, i, i - 5, false); arrayMove(reference, i, i - 5, false); checkColumns(tree, reference, "drag each column 5 to the left"); } // Test that moving a column after itself does not move anything synthesizeColumnDrag(tree, 0, 0, true); checkColumns(tree, reference, "drag to itself"); is(document.treecolDragging, null, "drag to itself completed"); // XXX roc should this be here??? SimpleTest.finish(); } function testtag_tree_wheel(aTree) { const deltaModes = [ WheelEvent.DOM_DELTA_PIXEL, // 0 WheelEvent.DOM_DELTA_LINE, // 1 WheelEvent.DOM_DELTA_PAGE, // 2 ]; function helper(aStart, aDelta, aIntDelta, aDeltaMode) { aTree.scrollToRow(aStart); var expected; if (!aIntDelta) { expected = aStart; } else if (aDeltaMode != WheelEvent.DOM_DELTA_PAGE) { expected = aStart + aIntDelta; } else if (aIntDelta > 0) { expected = aStart + aTree.getPageLength(); } else { expected = aStart - aTree.getPageLength(); } if (expected < 0) { expected = 0; } if (expected > aTree.view.rowCount - aTree.getPageLength()) { expected = aTree.view.rowCount - aTree.getPageLength(); } synthesizeWheel(aTree.body, 1, 1, { deltaMode: aDeltaMode, deltaY: aDelta, lineOrPageDeltaY: aIntDelta, }); is( aTree.getFirstVisibleRow(), expected, "testtag_tree_wheel: vertical, starting " + aStart + " delta " + aDelta + " lineOrPageDelta " + aIntDelta + " aDeltaMode " + aDeltaMode ); aTree.scrollToRow(aStart); // Check that horizontal scrolling has no effect synthesizeWheel(aTree.body, 1, 1, { deltaMode: aDeltaMode, deltaX: aDelta, lineOrPageDeltaX: aIntDelta, }); is( aTree.getFirstVisibleRow(), aStart, "testtag_tree_wheel: horizontal, starting " + aStart + " delta " + aDelta + " lineOrPageDelta " + aIntDelta + " aDeltaMode " + aDeltaMode ); } var defaultPrevented = 0; function wheelListener() { defaultPrevented++; } window.addEventListener("wheel", wheelListener); deltaModes.forEach(function (aDeltaMode) { var delta = aDeltaMode == WheelEvent.DOM_DELTA_PIXEL ? 5.0 : 0.3; helper(2, -delta, 0, aDeltaMode); helper(2, -delta, -1, aDeltaMode); helper(2, delta, 0, aDeltaMode); helper(2, delta, 1, aDeltaMode); helper(2, -2 * delta, 0, aDeltaMode); helper(2, -2 * delta, -1, aDeltaMode); helper(2, 2 * delta, 0, aDeltaMode); helper(2, 2 * delta, 1, aDeltaMode); }); window.removeEventListener("wheel", wheelListener); is(defaultPrevented, 48, "wheel event default prevented"); } async function testtag_tree_scroll() { const tree = document.querySelector("tree"); info("Scroll down with the content scrollbar at the top"); await doScrollTest({ tree, initialTreeScrollRow: 0, initialContainerScrollTop: 0, scrollDelta: 10, isTreeScrollExpected: true, }); info("Scroll down with the content scrollbar at the middle"); await doScrollTest({ tree, initialTreeScrollRow: 3, initialContainerScrollTop: 0, scrollDelta: 10, isTreeScrollExpected: true, }); info("Scroll down with the content scrollbar at the bottom"); await doScrollTest({ tree, initialTreeScrollRow: 9, initialContainerScrollTop: 0, scrollDelta: 10, isTreeScrollExpected: false, }); info("Scroll up with the content scrollbar at the bottom"); await doScrollTest({ tree, initialTreeScrollRow: 9, initialContainerScrollTop: 50, scrollDelta: -10, isTreeScrollExpected: true, }); info("Scroll up with the content scrollbar at the middle"); await doScrollTest({ tree, initialTreeScrollRow: 5, initialContainerScrollTop: 50, scrollDelta: -10, isTreeScrollExpected: true, }); info("Scroll up with the content scrollbar at the top"); await doScrollTest({ tree, initialTreeScrollRow: 0, initialContainerScrollTop: 50, scrollDelta: -10, isTreeScrollExpected: false, }); info("Check whether the tree is not scrolled when the parent is scrolling"); await doScrollWhileScrollingParent(tree); info( "Check whether the tree component consumes wheel events even if the scroll is located at edge as long as the events are handled as the same series" ); await doScrollInSameSeries({ tree, initialTreeScrollRow: 0, initialContainerScrollTop: 0, scrollDelta: 10, }); await doScrollInSameSeries({ tree, initialTreeScrollRow: 9, initialContainerScrollTop: 50, scrollDelta: -10, }); SimpleTest.finish(); } async function doScrollInSameSeries({ tree, initialTreeScrollRow, initialContainerScrollTop, scrollDelta, }) { // Set enough value to mousewheel.scroll_series_timeout pref to ensure the wheel // event fired as the same series. Services.prefs.setIntPref("mousewheel.scroll_series_timeout", 1000); const scrollbar = tree.shadowRoot.querySelector( "scrollbar[orient='vertical']" ); const parent = tree.parentElement; tree.scrollToRow(initialTreeScrollRow); parent.scrollTop = initialContainerScrollTop; // Scroll until the scrollbar was moved to the specified amount. await SimpleTest.promiseWaitForCondition(async () => { await nativeScroll(tree, 10, 10, scrollDelta); const curpos = scrollbar.getAttribute("curpos"); return ( (scrollDelta < 0 && curpos == 0) || (scrollDelta > 0 && curpos == scrollbar.getAttribute("maxpos")) ); }); // More scroll as the same series. for (let i = 0; i < 10; i++) { await nativeScroll(tree, 10, 10, scrollDelta); } is( parent.scrollTop, initialContainerScrollTop, "The wheel events are condumed in tree component" ); const utils = SpecialPowers.getDOMWindowUtils(window); ok(!utils.getWheelScrollTarget(), "The parent should not handle the event"); Services.prefs.clearUserPref("mousewheel.scroll_series_timeout"); } async function doScrollWhileScrollingParent(tree) { // Set enough value to mousewheel.scroll_series_timeout pref to ensure the wheel // event fired as the same series. Services.prefs.setIntPref("mousewheel.scroll_series_timeout", 1000); const scrollbar = tree.shadowRoot.querySelector( "scrollbar[orient='vertical']" ); const parent = tree.parentElement; // Set initial scroll amount. tree.scrollToRow(0); parent.scrollTop = 0; const scrollAmount = scrollbar.getAttribute("curpos"); // Scroll parent from top to bottom. await SimpleTest.promiseWaitForCondition(async () => { await nativeScroll(parent, 10, 10, 10); return parent.scrollTop === parent.scrollTopMax; }); is( scrollAmount, scrollbar.getAttribute("curpos"), "The tree should not be scrolled" ); const utils = SpecialPowers.getDOMWindowUtils(window); await SimpleTest.promiseWaitForCondition(() => !utils.getWheelScrollTarget()); Services.prefs.clearUserPref("mousewheel.scroll_series_timeout"); } async function doScrollTest({ tree, initialTreeScrollRow, initialContainerScrollTop, scrollDelta, isTreeScrollExpected, }) { const scrollbar = tree.shadowRoot.querySelector( "scrollbar[orient='vertical']" ); const container = tree.parentElement; // Set initial scroll amount. tree.scrollToRow(initialTreeScrollRow); container.scrollTop = initialContainerScrollTop; const treeScrollAmount = scrollbar.getAttribute("curpos"); const containerScrollAmount = container.scrollTop; // Wait until changing either scroll. await SimpleTest.promiseWaitForCondition(async () => { await nativeScroll(tree, 10, 10, scrollDelta); return ( treeScrollAmount !== scrollbar.getAttribute("curpos") || containerScrollAmount !== container.scrollTop ); }); is( treeScrollAmount !== scrollbar.getAttribute("curpos"), isTreeScrollExpected, "Scroll of tree is expected" ); is( containerScrollAmount !== container.scrollTop, !isTreeScrollExpected, "Scroll of container is expected" ); // Wait until finishing wheel scroll transaction. const utils = SpecialPowers.getDOMWindowUtils(window); await SimpleTest.promiseWaitForCondition(() => !utils.getWheelScrollTarget()); } async function nativeScroll(component, offsetX, offsetY, scrollDelta) { const utils = SpecialPowers.getDOMWindowUtils(window); const x = component.screenX + offsetX; const y = component.screenY + offsetY; // Mouse move event. await new Promise(resolve => { info("waiting for mousemove"); window.addEventListener("mousemove", resolve, { once: true }); utils.sendNativeMouseEvent( x * window.devicePixelRatio, y * window.devicePixelRatio, utils.NATIVE_MOUSE_MESSAGE_MOVE, 0, {}, component ); }); // Wheel event. await new Promise(resolve => { info("waiting for wheel"); window.addEventListener("wheel", resolve, { once: true }); utils.sendNativeMouseScrollEvent( x * window.devicePixelRatio, y * window.devicePixelRatio, // nativeVerticalWheelEventMsg is defined in apz_test_native_event_utils.js // eslint-disable-next-line no-undef nativeVerticalWheelEventMsg(), 0, // nativeScrollUnits is defined in apz_test_native_event_utils.js // eslint-disable-next-line no-undef -nativeScrollUnits(component, scrollDelta), 0, 0, 0, component ); }); info("waiting for apz"); // promiseApzFlushedRepaints is defined in apz_test_utils.js // eslint-disable-next-line no-undef await promiseApzFlushedRepaints(); } function synthesizeColumnDrag( aTree, aMouseDownColumnNumber, aMouseUpColumnNumber, aAfter ) { var columns = getSortedColumnArray(aTree); var down = columns[aMouseDownColumnNumber].element; var up = columns[aMouseUpColumnNumber].element; // Target the initial mousedown in the middle of the column header so we // avoid the extra hit test space given to the splitter var columnWidth = down.getBoundingClientRect().width; var splitterHitWidth = columnWidth / 2; synthesizeMouse(down, splitterHitWidth, 3, { type: "mousedown" }); var offsetX = 0; if (aAfter) { offsetX = columnWidth; } if (aMouseUpColumnNumber > aMouseDownColumnNumber) { for (let i = aMouseDownColumnNumber; i <= aMouseUpColumnNumber; i++) { let move = columns[i].element; synthesizeMouse(move, offsetX, 3, { type: "mousemove" }); } } else { for (let i = aMouseDownColumnNumber; i >= aMouseUpColumnNumber; i--) { let move = columns[i].element; synthesizeMouse(move, offsetX, 3, { type: "mousemove" }); } } synthesizeMouse(up, offsetX, 3, { type: "mouseup" }); } function arrayMove(aArray, aFrom, aTo, aAfter) { var o = aArray.splice(aFrom, 1)[0]; if (aTo > aFrom) { aTo--; } if (aAfter) { aTo++; } aArray.splice(aTo, 0, o); } function getSortedColumnArray(aTree) { var columns = aTree.columns; var array = []; for (let i = 0; i < columns.length; i++) { array.push(columns.getColumnAt(i)); } array.sort(function (a, b) { var o1 = parseInt(a.element.style.order); var o2 = parseInt(b.element.style.order); return o1 - o2; }); return array; } function checkColumns(aTree, aReference, aMessage) { var columns = getSortedColumnArray(aTree); var ids = []; columns.forEach(function (e) { ids.push(e.element.id); }); is(compareArrays(ids, aReference), true, aMessage); } function mouseOnCell(tree, row, column, testname) { var rect = tree.getCoordsForCellItem(row, column, "text"); synthesizeMouseExpectEvent( tree.body, rect.x, rect.y, {}, tree, "select", testname ); } function mouseClickOnColumnHeader( aColumns, aColumnIndex, aButton, aClickCount ) { var columnHeader = aColumns[aColumnIndex].element; var columnHeaderRect = columnHeader.getBoundingClientRect(); var columnWidth = columnHeaderRect.right - columnHeaderRect.left; // For multiple click we send separate click events, with increasing // clickCount. This simulates the common behavior of multiple clicks. for (let i = 1; i <= aClickCount; i++) { // Target the middle of the column header. synthesizeMouse(columnHeader, columnWidth / 2, 3, { button: aButton, clickCount: i, }); } } function mouseDblClickOnCell(tree, row, column) { // select the row we will edit var selection = tree.view.selection; selection.select(row); tree.ensureRowIsVisible(row); // get cell coordinates var rect = tree.getCoordsForCellItem(row, column, "text"); synthesizeMouse(tree.body, rect.x, rect.y, { clickCount: 2 }); } function compareArrays(arr1, arr2) { if (arr1.length != arr2.length) { return false; } for (let i = 0; i < arr1.length; i++) { if (arr1[i] != arr2[i]) { return false; } } return true; } function convertDOMtoTreeRowInfo(treechildren, level, rowidx) { var obj = { rows: [] }; var parentidx = rowidx.value; treechildren = treechildren.childNodes; for (var r = 0; r < treechildren.length; r++) { rowidx.value++; var treeitem = treechildren[r]; if (treeitem.hasChildNodes()) { var treerow = treeitem.firstChild; var cellInfo = []; for (var c = 0; c < treerow.childNodes.length; c++) { var cell = treerow.childNodes[c]; cellInfo.push({ label: cell.getAttribute("label") || "", value: cell.getAttribute("value") || "", properties: cell.getAttribute("properties") || "", editable: cell.getAttribute("editable") != "false", selectable: cell.getAttribute("selectable") != "false", image: cell.getAttribute("src") || "", mode: cell.hasAttribute("mode") ? parseInt(cell.getAttribute("mode")) : 3, }); } var descendants = treeitem.lastChild; var children = treerow == descendants ? null : convertDOMtoTreeRowInfo(descendants, level + 1, rowidx); obj.rows.push({ cells: cellInfo, properties: treerow.getAttribute("properties") || "", container: treeitem.getAttribute("container") == "true", separator: treeitem.localName == "treeseparator", children, level, parent: parentidx, }); } } return obj; }