Bug 851443 - Tests for richgrid/richgriditem bindings + stubMethod test helper. r=jimm
authorSam Foster <sfoster@mozilla.com>
Mon, 18 Mar 2013 16:30:41 +0000
changeset 125248 196693568165
parent 125247 62512f057d58
child 125249 f7208a072eb7
push id24450
push userryanvm@gmail.com
push dateTue, 19 Mar 2013 13:43:35 +0000
treeherdermozilla-central@2c41bf87b4e5 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersjimm
bugs851443
milestone22.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 851443 - Tests for richgrid/richgriditem bindings + stubMethod test helper. r=jimm
browser/metro/base/content/bindings/grid.xml
browser/metro/base/tests/Makefile.in
browser/metro/base/tests/browser_tilegrid.xul
browser/metro/base/tests/browser_tiles.js
browser/metro/base/tests/head.js
--- a/browser/metro/base/content/bindings/grid.xml
+++ b/browser/metro/base/content/bindings/grid.xml
@@ -44,31 +44,45 @@
           ]]>
         </body>
       </method>
 
       <method name="toggleItemSelection">
         <parameter name="anItem"/>
         <body>
           <![CDATA[
-            anItem.selected = !anItem.selected;
+            let wasSelected = anItem.selected;
+            if ("single" == this.getAttribute("seltype")) {
+              this.clearSelection();
+            }
+            anItem.selected = !wasSelected;
+            this._selectedItem = wasSelected ? null : anItem;
             this._fireOnSelectionChange();
           ]]>
         </body>
       </method>
 
       <method name="selectItem">
         <parameter name="anItem"/>
         <body>
           <![CDATA[
-            this.clearSelection();
+            let wasSelected = anItem.selected,
+                isSingleMode = ("single" == this.getAttribute("seltype"));
+            if (isSingleMode) {
+              this.clearSelection();
+            }
             this._selectedItem = anItem;
-            this._selectedItem.selected = true;
-
-            this._fireOnSelect();
+            anItem.selected = true;
+            if (isSingleMode) {
+              if (!wasSelected) {
+                this._fireOnSelect();
+              }
+            } else {
+              this._fireOnSelectionChange();
+            }
           ]]>
         </body>
       </method>
 
       <method name="handleItemClick">
         <parameter name="aItem"/>
         <body>
           <![CDATA[
--- a/browser/metro/base/tests/Makefile.in
+++ b/browser/metro/base/tests/Makefile.in
@@ -10,16 +10,18 @@ relativesrcdir  = @relativesrcdir@
 
 include $(DEPTH)/config/autoconf.mk
 
 BROWSER_TESTS = \
   head.js \
   browser_test.js \
   browser_canonizeURL.js \
   browser_context_ui.js \
+  browser_tiles.js \
+  browser_tilegrid.xul \
   browser_onscreen_keyboard.js \
   browser_onscreen_keyboard.html \
   browser_remotetabs.js \
   browser_downloads.js \
   browser_plugin_input.html \
   browser_plugin_input_mouse.js \
   browser_plugin_input_keyboard.js \
   browser_context_menu_tests.js \
new file mode 100644
--- /dev/null
+++ b/browser/metro/base/tests/browser_tilegrid.xul
@@ -0,0 +1,56 @@
+<?xml version="1.0"?>
+
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+   - License, v. 2.0. If a copy of the MPL was not distributed with this
+   - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<?xml-stylesheet href="chrome://browser/skin/platform.css" type="text/css"?>
+<?xml-stylesheet href="chrome://browser/skin/browser.css" type="text/css"?>
+<?xml-stylesheet href="chrome://browser/content/browser.css" type="text/css"?>
+
+<!DOCTYPE window []>
+
+<window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
+
+  <vbox flex="1">
+    <hbox>
+      <richgrid id="grid1" seltype="single" flex="1">
+        <richgriditem value="about:blank" id="grid1_item1" label="First item"/>
+      </richgrid>
+    </hbox>
+    <hbox>
+      <richgrid id="emptygrid" seltype="single" flex="1"/>
+    </hbox>
+    <hbox>
+      <richgrid id="grid2" seltype="single" flex="1">
+        <richgriditem value="about:blank" id="grid2_item1" label="First item"/>
+        <richgriditem value="about:blank" id="grid2_item2" label="2nd item"/>
+      </richgrid>
+    </hbox>
+    <hbox>
+      <richgrid id="grid3" seltype="single" flex="1">
+        <richgriditem value="about:blank" id="grid3_item1" label="First item"/>
+        <richgriditem value="about:blank" id="grid3_item2" label="2nd item"/>
+      </richgrid>
+    </hbox>
+    <hbox>
+      <richgrid id="grid4" seltype="single" flex="1">
+        <richgriditem value="about:blank" id="grid4_item1" label="First item"/>
+        <richgriditem value="about:blank" id="grid4_item2" label="2nd item"/>
+      </richgrid>
+    </hbox>
+    <hbox>
+      <richgrid id="grid-select1" seltype="single" flex="1">
+        <richgriditem value="about:blank" id="grid-select1_item1" label="First item"/>
+        <richgriditem value="about:blank" id="grid-select1_item2" label="2nd item"/>
+      </richgrid>
+    </hbox>
+    <hbox>
+      <richgrid id="grid-select2" seltype="multiple" flex="1">
+        <richgriditem value="about:blank" id="grid-select2_item1" label="First item"/>
+        <richgriditem value="about:blank" id="grid-select2_item2" label="2nd item"/>
+      </richgrid>
+    </hbox>
+  </vbox>
+</window>
+
new file mode 100644
--- /dev/null
+++ b/browser/metro/base/tests/browser_tiles.js
@@ -0,0 +1,294 @@
+let doc, win;
+
+function test() {
+  waitForExplicitFinish();
+  Task.spawn(function(){
+    netscape.security.PrivilegeManager.enablePrivilege("UniversalXPConnect");
+
+    info(chromeRoot + "browser_tilegrid.xul");
+    yield addTab(chromeRoot + "browser_tilegrid.xul");
+    doc = Browser.selectedTab.browser.contentWindow.document;
+  }).then(runTests);
+}
+
+gTests.push({
+  desc: "richgrid binding is applied",
+  run: function() {
+    ok(doc, "doc got defined");
+
+    let grid = doc.querySelector("#grid1");
+    ok(grid, "#grid1 is found");
+    is(typeof grid.clearSelection, "function", "#grid1 has the binding applied");
+
+    is(grid.children.length, 1, "#grid1 has a single item");
+    is(grid.children[0].control, grid, "#grid1 item's control points back at #grid1'");
+  }
+});
+
+gTests.push({
+  desc: "item clicks are handled",
+  run: function() {
+    let grid = doc.querySelector("#grid1");
+    is(typeof grid.handleItemClick, "function", "grid.handleItemClick is a function");
+    let handleStub = stubMethod(grid, 'handleItemClick');
+    let itemId = "grid1_item1"; // grid.children[0].getAttribute("id");
+
+    // send click to item and wait for next tick;
+    EventUtils.sendMouseEvent({type: 'click'}, itemId, doc.defaultView);
+    yield waitForMs(0);
+
+    is(handleStub.callCount, 1, "handleItemClick was called when we clicked an item");
+    handleStub.restore();
+
+    // if the grid has a controller, it should be called too
+    let gridController = {
+      handleItemClick: function() {}
+    };
+    let controllerHandleStub = stubMethod(gridController, "handleItemClick");
+    let origController = grid.controller;
+    grid.controller = gridController;
+
+    // send click to item and wait for next tick;
+    EventUtils.sendMouseEvent({type: 'click'}, itemId, doc.defaultView);
+    yield waitForMs(0);
+
+    is(controllerHandleStub.callCount, 1, "controller.handleItemClick was called when we clicked an item");
+    is(controllerHandleStub.calledWith[0], doc.getElementById(itemId), "controller.handleItemClick was passed the grid item");
+    grid.controller = origController;
+  }
+});
+
+gTests.push({
+  desc: "arrangeItems",
+  run: function() {
+     // implements an arrangeItems method, with optional cols, rows signature
+    let grid = doc.querySelector("#grid1");
+    is(typeof grid.arrangeItems, "function", "arrangeItems is a function on the grid");
+    todo(false, "Test outcome of arrangeItems with cols and rows arguments");
+  }
+});
+
+gTests.push({
+  desc: "appendItem",
+  run: function() {
+     // implements an appendItem with signature title, uri, returns item element
+     // appendItem triggers arrangeItems
+    let grid = doc.querySelector("#emptygrid");
+
+    is(grid.itemCount, 0, "0 itemCount when empty");
+    is(grid.children.length, 0, "0 children when empty");
+    is(typeof grid.appendItem, "function", "appendItem is a function on the grid");
+
+    let arrangeStub = stubMethod(grid, "arrangeItems");
+    let newItem = grid.appendItem("test title", "about:blank");
+
+    ok(newItem && grid.children[0]==newItem, "appendItem gives back the item");
+    is(grid.itemCount, 1, "itemCount is incremented when we appendItem");
+    is(newItem.getAttribute("label"), "test title", "title ends up on label attribute");
+    is(newItem.getAttribute("value"), "about:blank", "url ends up on value attribute");
+
+    is(arrangeStub.callCount, 1, "arrangeItems is called when we appendItem");
+    arrangeStub.restore();
+  }
+});
+
+gTests.push({
+  desc: "getItemAtIndex",
+  run: function() {
+     // implements a getItemAtIndex method
+    let grid = doc.querySelector("#grid2");
+    is(typeof grid.getItemAtIndex, "function", "getItemAtIndex is a function on the grid");
+    is(grid.getItemAtIndex(0).getAttribute("id"), "grid2_item1", "getItemAtIndex retrieves the first item");
+    is(grid.getItemAtIndex(1).getAttribute("id"), "grid2_item2", "getItemAtIndex item at index 2");
+    ok(!grid.getItemAtIndex(5), "getItemAtIndex out-of-bounds index returns falsy");
+  }
+});
+
+gTests.push({
+  desc: "removeItemAt",
+  run: function() {
+     // implements a removeItemAt method, with 'index' signature
+     // removeItemAt triggers arrangeItems
+    let grid = doc.querySelector("#grid2");
+
+    is(grid.itemCount, 2, "2 items initially");
+    is(typeof grid.removeItemAt, "function", "removeItemAt is a function on the grid");
+
+    let arrangeStub = stubMethod(grid, "arrangeItems");
+    let removedItem = grid.removeItemAt(0);
+
+    ok(removedItem, "removeItemAt gives back an item");
+    is(removedItem.getAttribute("id"), "grid2_item1", "removeItemAt gives back the correct item");
+    is(grid.children[0].getAttribute("id"), "grid2_item2", "2nd item becomes the first item");
+    is(grid.itemCount, 1, "itemCount is decremented when we removeItemAt");
+
+    is(arrangeStub.callCount, 1, "arrangeItems is called when we removeItemAt");
+    arrangeStub.restore();
+  }
+});
+
+gTests.push({
+  desc: "insertItemAt",
+  run: function() {
+     // implements an insertItemAt method, with index, title, uri.spec signature
+     // insertItemAt triggers arrangeItems
+    let grid = doc.querySelector("#grid3");
+
+    is(grid.itemCount, 2, "2 items initially");
+    is(typeof grid.insertItemAt, "function", "insertItemAt is a function on the grid");
+
+    let arrangeStub = stubMethod(grid, "arrangeItems");
+    let insertedItem = grid.insertItemAt(1, "inserted item", "http://example.com/inserted");
+
+    ok(insertedItem, "insertItemAt gives back an item");
+    is(grid.children[1], insertedItem, "item is inserted at the correct index");
+    is(insertedItem.getAttribute("label"), "inserted item", "insertItemAt creates item with the correct label");
+    is(insertedItem.getAttribute("value"), "http://example.com/inserted", "insertItemAt creates item with the correct url value");
+    is(grid.children[2].getAttribute("id"), "grid3_item2", "following item ends up at the correct index");
+    is(grid.itemCount, 3, "itemCount is incremented when we insertItemAt");
+
+    is(arrangeStub.callCount, 1, "arrangeItems is called when we insertItemAt");
+    arrangeStub.restore();
+  }
+});
+
+gTests.push({
+  desc: "getIndexOfItem",
+  run: function() {
+     // implements a getIndexOfItem method, with item (element) signature
+     // insertItemAt triggers arrangeItems
+    let grid = doc.querySelector("#grid4");
+
+    is(grid.itemCount, 2, "2 items initially");
+    is(typeof grid.getIndexOfItem, "function", "getIndexOfItem is a function on the grid");
+
+    let item = doc.getElementById("grid4_item2");
+    let badItem = doc.createElement("richgriditem");
+
+    is(grid.getIndexOfItem(item), 1, "getIndexOfItem returns the correct value for an item");
+    is(grid.getIndexOfItem(badItem), -1, "getIndexOfItem returns -1 for items it doesn't contain");
+  }
+});
+
+gTests.push({
+  desc: "selections (single)",
+  run: function() {
+     // when seltype is single,
+     //      maintains a selectedItem property
+     //      maintains a selectedIndex property
+     //     clearSelection, selectItem, toggleItemSelection methods are implemented
+     //     'select' events are implemented
+    let grid = doc.querySelector("#grid-select1");
+
+    is(typeof grid.clearSelection, "function", "clearSelection is a function on the grid");
+    is(typeof grid.selectedItems, "object", "selectedItems is a property on the grid");
+    is(typeof grid.toggleItemSelection, "function", "toggleItemSelection is function on the grid");
+    is(typeof grid.selectItem, "function", "selectItem is a function on the grid");
+
+    is(grid.itemCount, 2, "2 items initially");
+    is(grid.selectedItems.length, 0, "nothing selected initially");
+
+    grid.toggleItemSelection(grid.children[1]);
+    ok(grid.children[1].selected, "toggleItemSelection sets truthy selected prop on previously-unselected item");
+    is(grid.selectedIndex, 1, "selectedIndex is correct");
+
+    grid.toggleItemSelection(grid.children[1]);
+    is(grid.selectedIndex, -1, "selectedIndex reports correctly with nothing selected");
+
+    // item selection
+    grid.selectItem(grid.children[1]);
+    ok(grid.children[1].selected, "Item selected property is truthy after grid.selectItem");
+    ok(grid.children[1].getAttribute("selected"), "Item selected attribute is truthy after grid.selectItem");
+    ok(grid.selectedItems.length, "There are selectedItems after grid.selectItem");
+
+    // clearSelection
+    grid.selectItem(grid.children[0]);
+    grid.selectItem(grid.children[1]);
+    grid.clearSelection();
+    is(grid.selectedItems.length, 0, "Nothing selected when we clearSelection");
+    is(grid.selectedIndex, -1, "selectedIndex resets after clearSelection");
+
+    // select events
+    // in seltype=single mode, select is like the default action for the tile
+    // (think <a>, not <select multiple>)
+    let handler = {
+      handleEvent: function(aEvent) {}
+    };
+    let handlerStub = stubMethod(handler, "handleEvent");
+    doc.defaultView.addEventListener("select", handler, false);
+    info("select listener added");
+
+    info("calling selectItem, currently it is:" + grid.children[0].selected);
+    // Note: A richgrid in seltype=single mode fires "select" events from selectItem
+    grid.selectItem(grid.children[0]);
+    info("/calling selectItem, now it is:" + grid.children[0].selected);
+    yield waitForMs(0);
+
+    is(handlerStub.callCount, 1, "select event handler was called when we selected an item");
+    is(handlerStub.calledWith[0].type, "select", "handler got a select event");
+    is(handlerStub.calledWith[0].target, grid, "select event had the originating grid as the target");
+    handlerStub.restore();
+    doc.defaultView.removeEventListener("select", handler, false);
+  }
+});
+
+gTests.push({
+  desc: "selections (multiple)",
+  run: function() {
+     // when seltype is multiple,
+     //      maintains a selectedItems property
+     //      clearSelection, selectItem, toggleItemSelection methods are implemented
+     //     'selectionchange' events are implemented
+    let grid = doc.querySelector("#grid-select2");
+
+    is(typeof grid.clearSelection, "function", "clearSelection is a function on the grid");
+    is(typeof grid.selectedItems, "object", "selectedItems is a property on the grid");
+    is(typeof grid.toggleItemSelection, "function", "toggleItemSelection is function on the grid");
+    is(typeof grid.selectItem, "function", "selectItem is a function on the grid");
+
+    is(grid.itemCount, 2, "2 items initially");
+    is(grid.selectedItems.length, 0, "nothing selected initially");
+
+    grid.toggleItemSelection(grid.children[1]);
+    ok(grid.children[1].selected, "toggleItemSelection sets truthy selected prop on previously-unselected item");
+    is(grid.selectedItems.length, 1, "1 item selected when we first toggleItemSelection");
+    is(grid.selectedItems[0], grid.children[1], "the right item is selected");
+    is(grid.selectedIndex, 1, "selectedIndex is correct");
+
+    grid.toggleItemSelection(grid.children[1]);
+    is(grid.selectedItems.length, 0, "Nothing selected when we toggleItemSelection again");
+
+    // clearSelection
+    grid.children[0].selected=true;
+    grid.children[1].selected=true;
+    is(grid.selectedItems.length, 2, "Both items are selected before calling clearSelection");
+    grid.clearSelection();
+    is(grid.selectedItems.length, 0, "Nothing selected when we clearSelection");
+    ok(!(grid.children[0].selected || grid.children[1].selected), "selected properties all falsy when we clearSelection");
+
+    // selectionchange events
+    // in seltype=multiple mode, we track selected state on all items
+    // (think <select multiple> not <a>)
+    let handler = {
+      handleEvent: function(aEvent) {}
+    };
+    let handlerStub = stubMethod(handler, "handleEvent");
+    doc.defaultView.addEventListener("selectionchange", handler, false);
+    info("selectionchange listener added");
+
+    info("calling toggleItemSelection, currently it is:" + grid.children[0].selected);
+    // Note: A richgrid in seltype=single mode fires "select" events from selectItem
+    grid.toggleItemSelection(grid.children[0]);
+    info("/calling toggleItemSelection, now it is:" + grid.children[0].selected);
+    yield waitForMs(0);
+
+    is(handlerStub.callCount, 1, "selectionchange event handler was called when we selected an item");
+    is(handlerStub.calledWith[0].type, "selectionchange", "handler got a selectionchange event");
+    is(handlerStub.calledWith[0].target, grid, "select event had the originating grid as the target");
+    handlerStub.restore();
+    doc.defaultView.removeEventListener("selectionchange", handler, false);
+  }
+});
+
+     // implements a getItemAtIndex method (or grid.children[idx] ?)
+
--- a/browser/metro/base/tests/head.js
+++ b/browser/metro/base/tests/head.js
@@ -419,8 +419,22 @@ function runTests() {
       if ('function' == typeof gCurrentTest.tearDown) {
         yield Task.spawn(gCurrentTest.tearDown.bind(gCurrentTest));
       }
       info("END "+gCurrentTest.desc);
     }
     finish();
   });
 }
+
+function stubMethod(aObj, aMethod) {
+  let origFunc = aObj[aMethod];
+  let func = function() {
+    func.calledWith = Array.slice(arguments);
+    func.callCount++;
+  }
+  func.callCount = 0;
+  func.restore = function() {
+    return (aObj[aMethod] = origFunc);
+  };
+  aObj[aMethod] = func;
+  return func;
+}
\ No newline at end of file