summaryrefslogtreecommitdiffstats
path: root/toolkit/content/tests/widgets/tree_shared.js
diff options
context:
space:
mode:
Diffstat (limited to 'toolkit/content/tests/widgets/tree_shared.js')
-rw-r--r--toolkit/content/tests/widgets/tree_shared.js2181
1 files changed, 2181 insertions, 0 deletions
diff --git a/toolkit/content/tests/widgets/tree_shared.js b/toolkit/content/tests/widgets/tree_shared.js
new file mode 100644
index 0000000000..62b93fdb69
--- /dev/null
+++ b/toolkit/content/tests/widgets/tree_shared.js
@@ -0,0 +1,2181 @@
+// 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 ? 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(event) {
+ keydownFired++;
+ }
+ function keypressListener(event) {
+ 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(row) {
+ 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(row) {
+ 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, testid, rowInfo) {
+ // 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(event) {
+ 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 + offsetX;
+
+ // Mouse move event.
+ await new Promise(resolve => {
+ window.addEventListener("mousemove", resolve, { once: true });
+ utils.sendNativeMouseEvent(
+ x,
+ y,
+ utils.NATIVE_MOUSE_MESSAGE_MOVE,
+ 0,
+ {},
+ component
+ );
+ });
+
+ // Wheel event.
+ await new Promise(resolve => {
+ window.addEventListener("wheel", resolve, { once: true });
+ utils.sendNativeMouseScrollEvent(
+ x,
+ y,
+ // 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
+ );
+ });
+
+ // 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, testname) {
+ // 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;
+}