Wed, 31 Dec 2014 06:55:50 +0100
Added tag UPSTREAM_283F7C6 for changeset ca08bd8f51b2
michael@0 | 1 | let doc; |
michael@0 | 2 | |
michael@0 | 3 | function test() { |
michael@0 | 4 | waitForExplicitFinish(); |
michael@0 | 5 | Task.spawn(function(){ |
michael@0 | 6 | info(chromeRoot + "browser_tilegrid.xul"); |
michael@0 | 7 | yield addTab(chromeRoot + "browser_tilegrid.xul"); |
michael@0 | 8 | doc = Browser.selectedTab.browser.contentWindow.document; |
michael@0 | 9 | }).then(runTests); |
michael@0 | 10 | } |
michael@0 | 11 | |
michael@0 | 12 | function _checkIfBoundByRichGrid_Item(expected, node, idx) { |
michael@0 | 13 | let binding = node.ownerDocument.defaultView.getComputedStyle(node).MozBinding; |
michael@0 | 14 | let result = ('url("chrome://browser/content/bindings/grid.xml#richgrid-item")' == binding); |
michael@0 | 15 | return (result == expected); |
michael@0 | 16 | } |
michael@0 | 17 | let isBoundByRichGrid_Item = _checkIfBoundByRichGrid_Item.bind(this, true); |
michael@0 | 18 | let isNotBoundByRichGrid_Item = _checkIfBoundByRichGrid_Item.bind(this, false); |
michael@0 | 19 | |
michael@0 | 20 | gTests.push({ |
michael@0 | 21 | desc: "richgrid binding is applied", |
michael@0 | 22 | run: function() { |
michael@0 | 23 | ok(doc, "doc got defined"); |
michael@0 | 24 | |
michael@0 | 25 | let grid = doc.querySelector("#grid1"); |
michael@0 | 26 | ok(grid, "#grid1 is found"); |
michael@0 | 27 | is(typeof grid.clearSelection, "function", "#grid1 has the binding applied"); |
michael@0 | 28 | is(grid.items.length, 2, "#grid1 has a 2 items"); |
michael@0 | 29 | is(grid.items[0].control, grid, "#grid1 item's control points back at #grid1'"); |
michael@0 | 30 | ok(Array.every(grid.items, isBoundByRichGrid_Item), "All items are bound by richgrid-item"); |
michael@0 | 31 | } |
michael@0 | 32 | }); |
michael@0 | 33 | |
michael@0 | 34 | gTests.push({ |
michael@0 | 35 | desc: "item clicks are handled", |
michael@0 | 36 | run: function() { |
michael@0 | 37 | let grid = doc.querySelector("#grid1"); |
michael@0 | 38 | is(typeof grid.handleItemClick, "function", "grid.handleItemClick is a function"); |
michael@0 | 39 | let handleStub = stubMethod(grid, 'handleItemClick'); |
michael@0 | 40 | let itemId = "grid1_item1"; // grid.items[0].getAttribute("id"); |
michael@0 | 41 | |
michael@0 | 42 | // send click to item and wait for next tick; |
michael@0 | 43 | EventUtils.sendMouseEvent({type: 'click'}, itemId, doc.defaultView); |
michael@0 | 44 | yield waitForMs(0); |
michael@0 | 45 | |
michael@0 | 46 | is(handleStub.callCount, 1, "handleItemClick was called when we clicked an item"); |
michael@0 | 47 | handleStub.restore(); |
michael@0 | 48 | |
michael@0 | 49 | // if the grid has a controller, it should be called too |
michael@0 | 50 | let gridController = { |
michael@0 | 51 | handleItemClick: function() {} |
michael@0 | 52 | }; |
michael@0 | 53 | let controllerHandleStub = stubMethod(gridController, "handleItemClick"); |
michael@0 | 54 | let origController = grid.controller; |
michael@0 | 55 | grid.controller = gridController; |
michael@0 | 56 | |
michael@0 | 57 | // send click to item and wait for next tick; |
michael@0 | 58 | EventUtils.sendMouseEvent({type: 'click'}, itemId, doc.defaultView); |
michael@0 | 59 | yield waitForMs(0); |
michael@0 | 60 | |
michael@0 | 61 | is(controllerHandleStub.callCount, 1, "controller.handleItemClick was called when we clicked an item"); |
michael@0 | 62 | is(controllerHandleStub.calledWith[0], doc.getElementById(itemId), "controller.handleItemClick was passed the grid item"); |
michael@0 | 63 | grid.controller = origController; |
michael@0 | 64 | } |
michael@0 | 65 | }); |
michael@0 | 66 | |
michael@0 | 67 | gTests.push({ |
michael@0 | 68 | desc: "arrangeItems", |
michael@0 | 69 | run: function() { |
michael@0 | 70 | // implements an arrangeItems method, with optional cols, rows signature |
michael@0 | 71 | let container = doc.getElementById("alayout"); |
michael@0 | 72 | let grid = doc.querySelector("#grid_layout"); |
michael@0 | 73 | |
michael@0 | 74 | is(typeof grid.arrangeItems, "function", "arrangeItems is a function on the grid"); |
michael@0 | 75 | |
michael@0 | 76 | ok(grid.tileHeight, "grid has truthy tileHeight value"); |
michael@0 | 77 | ok(grid.tileWidth, "grid has truthy tileWidth value"); |
michael@0 | 78 | |
michael@0 | 79 | // make the container big enough for 3 rows |
michael@0 | 80 | container.style.height = 3 * grid.tileHeight + 20 + "px"; |
michael@0 | 81 | |
michael@0 | 82 | // add some items |
michael@0 | 83 | grid.appendItem("test title", "about:blank", true); |
michael@0 | 84 | grid.appendItem("test title", "about:blank", true); |
michael@0 | 85 | grid.appendItem("test title", "about:blank", true); |
michael@0 | 86 | grid.appendItem("test title", "about:blank", true); |
michael@0 | 87 | grid.appendItem("test title", "about:blank", true); |
michael@0 | 88 | |
michael@0 | 89 | grid.arrangeItems(); |
michael@0 | 90 | // they should all fit nicely in a 3x2 grid |
michael@0 | 91 | is(grid.rowCount, 3, "rowCount is calculated correctly for a given container height and tileheight"); |
michael@0 | 92 | is(grid.columnCount, 2, "columnCount is calculated correctly for a given container maxWidth and tilewidth"); |
michael@0 | 93 | |
michael@0 | 94 | // squish the available height |
michael@0 | 95 | // should overflow (clip) a 2x2 grid |
michael@0 | 96 | |
michael@0 | 97 | let under3rowsHeight = (3 * grid.tileHeight -20) + "px"; |
michael@0 | 98 | container.style.height = under3rowsHeight; |
michael@0 | 99 | |
michael@0 | 100 | let arrangedPromise = waitForEvent(grid, "arranged"); |
michael@0 | 101 | grid.arrangeItems(); |
michael@0 | 102 | yield arrangedPromise; |
michael@0 | 103 | |
michael@0 | 104 | ok(true, "arranged event is fired when arrangeItems is called"); |
michael@0 | 105 | is(grid.rowCount, 2, "rowCount is re-calculated correctly for a given container height"); |
michael@0 | 106 | } |
michael@0 | 107 | }); |
michael@0 | 108 | |
michael@0 | 109 | gTests.push({ |
michael@0 | 110 | desc: "clearAll", |
michael@0 | 111 | run: function() { |
michael@0 | 112 | let grid = doc.getElementById("clearGrid"); |
michael@0 | 113 | grid.arrangeItems(); |
michael@0 | 114 | |
michael@0 | 115 | // grid has rows=2 so we expect at least 2 rows and 2 columns with 3 items |
michael@0 | 116 | is(typeof grid.clearAll, "function", "clearAll is a function on the grid"); |
michael@0 | 117 | is(grid.itemCount, 3, "grid has 3 items initially"); |
michael@0 | 118 | is(grid.rowCount, 2, "grid has 2 rows initially"); |
michael@0 | 119 | is(grid.columnCount, 2, "grid has 2 cols initially"); |
michael@0 | 120 | |
michael@0 | 121 | let arrangeSpy = spyOnMethod(grid, "arrangeItems"); |
michael@0 | 122 | grid.clearAll(); |
michael@0 | 123 | |
michael@0 | 124 | is(grid.itemCount, 0, "grid has 0 itemCount after clearAll"); |
michael@0 | 125 | is(grid.items.length, 0, "grid has 0 items after clearAll"); |
michael@0 | 126 | // now that we use slots, an empty grid may still have non-zero rows & columns |
michael@0 | 127 | |
michael@0 | 128 | is(arrangeSpy.callCount, 1, "arrangeItems is called once when we clearAll"); |
michael@0 | 129 | arrangeSpy.restore(); |
michael@0 | 130 | } |
michael@0 | 131 | }); |
michael@0 | 132 | |
michael@0 | 133 | gTests.push({ |
michael@0 | 134 | desc: "empty grid", |
michael@0 | 135 | run: function() { |
michael@0 | 136 | // XXX grids have minSlots and may not be ever truly empty |
michael@0 | 137 | |
michael@0 | 138 | let grid = doc.getElementById("emptyGrid"); |
michael@0 | 139 | grid.arrangeItems(); |
michael@0 | 140 | yield waitForCondition(() => !grid.isArranging); |
michael@0 | 141 | |
michael@0 | 142 | // grid has 2 rows, 6 slots, 0 items |
michael@0 | 143 | ok(grid.isBound, "binding was applied"); |
michael@0 | 144 | is(grid.itemCount, 0, "empty grid has 0 items"); |
michael@0 | 145 | // minSlots attr. creates unpopulated slots |
michael@0 | 146 | is(grid.rowCount, grid.getAttribute("rows"), "empty grid with rows-attribute has that number of rows"); |
michael@0 | 147 | is(grid.columnCount, 3, "empty grid has expected number of columns"); |
michael@0 | 148 | |
michael@0 | 149 | // remove rows attribute and allow space for the grid to find its own height |
michael@0 | 150 | // for its number of slots |
michael@0 | 151 | grid.removeAttribute("rows"); |
michael@0 | 152 | grid.parentNode.style.height = 20+(grid.tileHeight*grid.minSlots)+"px"; |
michael@0 | 153 | |
michael@0 | 154 | grid.arrangeItems(); |
michael@0 | 155 | yield waitForCondition(() => !grid.isArranging); |
michael@0 | 156 | is(grid.rowCount, grid.minSlots, "empty grid has this.minSlots rows"); |
michael@0 | 157 | is(grid.columnCount, 1, "empty grid has 1 column"); |
michael@0 | 158 | } |
michael@0 | 159 | }); |
michael@0 | 160 | |
michael@0 | 161 | gTests.push({ |
michael@0 | 162 | desc: "appendItem", |
michael@0 | 163 | run: function() { |
michael@0 | 164 | // implements an appendItem with signature title, uri, returns item element |
michael@0 | 165 | // appendItem triggers arrangeItems |
michael@0 | 166 | let grid = doc.querySelector("#emptygrid"); |
michael@0 | 167 | |
michael@0 | 168 | is(grid.itemCount, 0, "0 itemCount when empty"); |
michael@0 | 169 | is(grid.items.length, 0, "0 items when empty"); |
michael@0 | 170 | is(typeof grid.appendItem, "function", "appendItem is a function on the grid"); |
michael@0 | 171 | |
michael@0 | 172 | let arrangeStub = stubMethod(grid, "arrangeItems"); |
michael@0 | 173 | let newItem = grid.appendItem("test title", "about:blank"); |
michael@0 | 174 | |
michael@0 | 175 | ok(newItem && grid.items[0]==newItem, "appendItem gives back the item"); |
michael@0 | 176 | is(grid.itemCount, 1, "itemCount is incremented when we appendItem"); |
michael@0 | 177 | is(newItem.getAttribute("label"), "test title", "title ends up on label attribute"); |
michael@0 | 178 | is(newItem.getAttribute("value"), "about:blank", "url ends up on value attribute"); |
michael@0 | 179 | |
michael@0 | 180 | is(arrangeStub.callCount, 1, "arrangeItems is called when we appendItem"); |
michael@0 | 181 | arrangeStub.restore(); |
michael@0 | 182 | } |
michael@0 | 183 | }); |
michael@0 | 184 | |
michael@0 | 185 | gTests.push({ |
michael@0 | 186 | desc: "getItemAtIndex", |
michael@0 | 187 | run: function() { |
michael@0 | 188 | // implements a getItemAtIndex method |
michael@0 | 189 | let grid = doc.querySelector("#grid2"); |
michael@0 | 190 | is(typeof grid.getItemAtIndex, "function", "getItemAtIndex is a function on the grid"); |
michael@0 | 191 | is(grid.getItemAtIndex(0).getAttribute("id"), "grid2_item1", "getItemAtIndex retrieves the first item"); |
michael@0 | 192 | is(grid.getItemAtIndex(1).getAttribute("id"), "grid2_item2", "getItemAtIndex item at index 2"); |
michael@0 | 193 | ok(!grid.getItemAtIndex(5), "getItemAtIndex out-of-bounds index returns falsy"); |
michael@0 | 194 | } |
michael@0 | 195 | }); |
michael@0 | 196 | |
michael@0 | 197 | gTests.push({ |
michael@0 | 198 | desc: "removeItemAt", |
michael@0 | 199 | run: function() { |
michael@0 | 200 | // implements a removeItemAt method, with 'index' signature |
michael@0 | 201 | // removeItemAt triggers arrangeItems |
michael@0 | 202 | let grid = doc.querySelector("#grid2"); |
michael@0 | 203 | |
michael@0 | 204 | is(grid.itemCount, 2, "2 items initially"); |
michael@0 | 205 | is(typeof grid.removeItemAt, "function", "removeItemAt is a function on the grid"); |
michael@0 | 206 | |
michael@0 | 207 | let arrangeStub = stubMethod(grid, "arrangeItems"); |
michael@0 | 208 | let removedItem = grid.removeItemAt(0); |
michael@0 | 209 | |
michael@0 | 210 | ok(removedItem, "removeItemAt gives back an item"); |
michael@0 | 211 | is(removedItem.getAttribute("id"), "grid2_item1", "removeItemAt gives back the correct item"); |
michael@0 | 212 | is(grid.items[0].getAttribute("id"), "grid2_item2", "2nd item becomes the first item"); |
michael@0 | 213 | is(grid.itemCount, 1, "itemCount is decremented when we removeItemAt"); |
michael@0 | 214 | |
michael@0 | 215 | is(arrangeStub.callCount, 1, "arrangeItems is called when we removeItemAt"); |
michael@0 | 216 | arrangeStub.restore(); |
michael@0 | 217 | } |
michael@0 | 218 | }); |
michael@0 | 219 | |
michael@0 | 220 | gTests.push({ |
michael@0 | 221 | desc: "insertItemAt", |
michael@0 | 222 | run: function() { |
michael@0 | 223 | // implements an insertItemAt method, with index, title, uri.spec signature |
michael@0 | 224 | // insertItemAt triggers arrangeItems |
michael@0 | 225 | let grid = doc.querySelector("#grid3"); |
michael@0 | 226 | |
michael@0 | 227 | is(grid.itemCount, 2, "2 items initially"); |
michael@0 | 228 | is(typeof grid.insertItemAt, "function", "insertItemAt is a function on the grid"); |
michael@0 | 229 | |
michael@0 | 230 | let arrangeStub = stubMethod(grid, "arrangeItems"); |
michael@0 | 231 | let insertedAt0 = grid.insertItemAt(0, "inserted item 0", "http://example.com/inserted0"); |
michael@0 | 232 | let insertedAt00 = grid.insertItemAt(0, "inserted item 00", "http://example.com/inserted00"); |
michael@0 | 233 | |
michael@0 | 234 | ok(insertedAt0 && insertedAt00, "insertItemAt gives back an item"); |
michael@0 | 235 | |
michael@0 | 236 | is(insertedAt0.getAttribute("label"), "inserted item 0", "insertItemAt creates item with the correct label"); |
michael@0 | 237 | is(insertedAt0.getAttribute("value"), "http://example.com/inserted0", "insertItemAt creates item with the correct url value"); |
michael@0 | 238 | |
michael@0 | 239 | is(grid.items[0], insertedAt00, "item is inserted at the correct index"); |
michael@0 | 240 | is(grid.children[0], insertedAt00, "first item occupies the first slot"); |
michael@0 | 241 | is(grid.items[1], insertedAt0, "item is inserted at the correct index"); |
michael@0 | 242 | is(grid.children[1], insertedAt0, "next item occupies the next slot"); |
michael@0 | 243 | |
michael@0 | 244 | is(grid.items[2].getAttribute("label"), "First item", "Old first item is now at index 2"); |
michael@0 | 245 | is(grid.items[3].getAttribute("label"), "2nd item", "Old 2nd item is now at index 3"); |
michael@0 | 246 | |
michael@0 | 247 | is(grid.itemCount, 4, "itemCount is incremented when we insertItemAt"); |
michael@0 | 248 | |
michael@0 | 249 | is(arrangeStub.callCount, 2, "arrangeItems is called when we insertItemAt"); |
michael@0 | 250 | arrangeStub.restore(); |
michael@0 | 251 | } |
michael@0 | 252 | }); |
michael@0 | 253 | |
michael@0 | 254 | gTests.push({ |
michael@0 | 255 | desc: "getIndexOfItem", |
michael@0 | 256 | run: function() { |
michael@0 | 257 | // implements a getIndexOfItem method, with item (element) signature |
michael@0 | 258 | // insertItemAt triggers arrangeItems |
michael@0 | 259 | let grid = doc.querySelector("#grid4"); |
michael@0 | 260 | |
michael@0 | 261 | is(grid.itemCount, 2, "2 items initially"); |
michael@0 | 262 | is(typeof grid.getIndexOfItem, "function", "getIndexOfItem is a function on the grid"); |
michael@0 | 263 | |
michael@0 | 264 | let item = doc.getElementById("grid4_item2"); |
michael@0 | 265 | let badItem = doc.createElement("richgriditem"); |
michael@0 | 266 | |
michael@0 | 267 | is(grid.getIndexOfItem(item), 1, "getIndexOfItem returns the correct value for an item"); |
michael@0 | 268 | is(grid.getIndexOfItem(badItem), -1, "getIndexOfItem returns -1 for items it doesn't contain"); |
michael@0 | 269 | } |
michael@0 | 270 | }); |
michael@0 | 271 | |
michael@0 | 272 | gTests.push({ |
michael@0 | 273 | desc: "getItemsByUrl", |
michael@0 | 274 | run: function() { |
michael@0 | 275 | let grid = doc.querySelector("#grid5"); |
michael@0 | 276 | |
michael@0 | 277 | is(grid.itemCount, 4, "4 items total"); |
michael@0 | 278 | is(typeof grid.getItemsByUrl, "function", "getItemsByUrl is a function on the grid"); |
michael@0 | 279 | |
michael@0 | 280 | ['about:blank', 'http://bugzilla.mozilla.org/'].forEach(function(testUrl) { |
michael@0 | 281 | let items = grid.getItemsByUrl(testUrl); |
michael@0 | 282 | is(items.length, 2, "2 matching items in the test grid"); |
michael@0 | 283 | is(items.item(0).url, testUrl, "Matched item has correct url property"); |
michael@0 | 284 | is(items.item(1).url, testUrl, "Matched item has correct url property"); |
michael@0 | 285 | }); |
michael@0 | 286 | |
michael@0 | 287 | let badUrl = 'http://gopher.well.com:70/'; |
michael@0 | 288 | let items = grid.getItemsByUrl(badUrl); |
michael@0 | 289 | is(items.length, 0, "0 items matched url: "+badUrl); |
michael@0 | 290 | |
michael@0 | 291 | } |
michael@0 | 292 | }); |
michael@0 | 293 | |
michael@0 | 294 | gTests.push({ |
michael@0 | 295 | desc: "removeItem", |
michael@0 | 296 | run: function() { |
michael@0 | 297 | let grid = doc.querySelector("#grid5"); |
michael@0 | 298 | |
michael@0 | 299 | is(grid.itemCount, 4, "4 items total"); |
michael@0 | 300 | is(typeof grid.removeItem, "function", "removeItem is a function on the grid"); |
michael@0 | 301 | |
michael@0 | 302 | let arrangeStub = stubMethod(grid, "arrangeItems"); |
michael@0 | 303 | let removedFirst = grid.removeItem( grid.items[0] ); |
michael@0 | 304 | |
michael@0 | 305 | is(arrangeStub.callCount, 1, "arrangeItems is called when we removeItem"); |
michael@0 | 306 | |
michael@0 | 307 | let removed2nd = grid.removeItem( grid.items[0], true); |
michael@0 | 308 | is(removed2nd.getAttribute("label"), "2nd item", "the next item was returned"); |
michael@0 | 309 | is(grid.itemCount, 2, "2 items remain"); |
michael@0 | 310 | |
michael@0 | 311 | // callCount should still be at 1 |
michael@0 | 312 | is(arrangeStub.callCount, 1, "arrangeItems is not called when we pass the truthy skipArrange param"); |
michael@0 | 313 | |
michael@0 | 314 | let otherItem = grid.ownerDocument.querySelector("#grid6_item1"); |
michael@0 | 315 | let removedFail = grid.removeItem(otherItem); |
michael@0 | 316 | ok(!removedFail, "Falsy value returned when non-child item passed"); |
michael@0 | 317 | is(grid.itemCount, 2, "2 items remain"); |
michael@0 | 318 | |
michael@0 | 319 | // callCount should still be at 1 |
michael@0 | 320 | is(arrangeStub.callCount, 1, "arrangeItems is not called when nothing is matched"); |
michael@0 | 321 | |
michael@0 | 322 | arrangeStub.restore(); |
michael@0 | 323 | } |
michael@0 | 324 | }); |
michael@0 | 325 | |
michael@0 | 326 | gTests.push({ |
michael@0 | 327 | desc: "selections (single)", |
michael@0 | 328 | run: function() { |
michael@0 | 329 | // when seltype is single, |
michael@0 | 330 | // maintains a selectedItem property |
michael@0 | 331 | // maintains a selectedIndex property |
michael@0 | 332 | // clearSelection, selectItem, toggleItemSelection methods are implemented |
michael@0 | 333 | // 'select' events are implemented |
michael@0 | 334 | let grid = doc.querySelector("#grid-select1"); |
michael@0 | 335 | |
michael@0 | 336 | is(typeof grid.clearSelection, "function", "clearSelection is a function on the grid"); |
michael@0 | 337 | is(typeof grid.selectedItems, "object", "selectedItems is a property on the grid"); |
michael@0 | 338 | is(typeof grid.toggleItemSelection, "function", "toggleItemSelection is function on the grid"); |
michael@0 | 339 | is(typeof grid.selectItem, "function", "selectItem is a function on the grid"); |
michael@0 | 340 | |
michael@0 | 341 | is(grid.itemCount, 2, "2 items initially"); |
michael@0 | 342 | is(grid.selectedItems.length, 0, "nothing selected initially"); |
michael@0 | 343 | |
michael@0 | 344 | grid.toggleItemSelection(grid.items[1]); |
michael@0 | 345 | ok(grid.items[1].selected, "toggleItemSelection sets truthy selected prop on previously-unselected item"); |
michael@0 | 346 | is(grid.selectedIndex, 1, "selectedIndex is correct"); |
michael@0 | 347 | |
michael@0 | 348 | grid.toggleItemSelection(grid.items[1]); |
michael@0 | 349 | ok(!grid.items[1].selected, "toggleItemSelection sets falsy selected prop on previously-selected item"); |
michael@0 | 350 | is(grid.selectedIndex, -1, "selectedIndex reports correctly with nothing selected"); |
michael@0 | 351 | |
michael@0 | 352 | // item selection |
michael@0 | 353 | grid.selectItem(grid.items[1]); |
michael@0 | 354 | ok(grid.items[1].selected, "Item selected property is truthy after grid.selectItem"); |
michael@0 | 355 | ok(grid.items[1].getAttribute("selected"), "Item selected attribute is truthy after grid.selectItem"); |
michael@0 | 356 | ok(grid.selectedItems.length, "There are selectedItems after grid.selectItem"); |
michael@0 | 357 | |
michael@0 | 358 | // select events |
michael@0 | 359 | // in seltype=single mode, select is like the default action for the tile |
michael@0 | 360 | // (think <a>, not <select multiple>) |
michael@0 | 361 | let handler = { |
michael@0 | 362 | handleEvent: function(aEvent) {} |
michael@0 | 363 | }; |
michael@0 | 364 | let handlerStub = stubMethod(handler, "handleEvent"); |
michael@0 | 365 | |
michael@0 | 366 | grid.items[1].selected = true; |
michael@0 | 367 | |
michael@0 | 368 | doc.defaultView.addEventListener("select", handler, false); |
michael@0 | 369 | info("select listener added"); |
michael@0 | 370 | |
michael@0 | 371 | // clearSelection |
michael@0 | 372 | grid.clearSelection(); |
michael@0 | 373 | is(grid.selectedItems.length, 0, "Nothing selected when we clearSelection"); |
michael@0 | 374 | is(grid.selectedIndex, -1, "selectedIndex resets after clearSelection"); |
michael@0 | 375 | is(handlerStub.callCount, 0, "clearSelection should not fire a selectionchange event"); |
michael@0 | 376 | |
michael@0 | 377 | info("calling selectItem, currently it is:" + grid.items[0].selected); |
michael@0 | 378 | // Note: A richgrid in seltype=single mode fires "select" events from selectItem |
michael@0 | 379 | grid.selectItem(grid.items[0]); |
michael@0 | 380 | info("calling selectItem, now it is:" + grid.items[0].selected); |
michael@0 | 381 | yield waitForMs(0); |
michael@0 | 382 | |
michael@0 | 383 | is(handlerStub.callCount, 1, "select event handler was called when we selected an item"); |
michael@0 | 384 | is(handlerStub.calledWith[0].type, "select", "handler got a select event"); |
michael@0 | 385 | is(handlerStub.calledWith[0].target, grid, "select event had the originating grid as the target"); |
michael@0 | 386 | handlerStub.restore(); |
michael@0 | 387 | doc.defaultView.removeEventListener("select", handler, false); |
michael@0 | 388 | } |
michael@0 | 389 | }); |
michael@0 | 390 | |
michael@0 | 391 | gTests.push({ |
michael@0 | 392 | desc: "selections (multiple)", |
michael@0 | 393 | run: function() { |
michael@0 | 394 | // when seltype is multiple, |
michael@0 | 395 | // maintains a selectedItems property |
michael@0 | 396 | // clearSelection, selectItem, toggleItemSelection methods are implemented |
michael@0 | 397 | // 'selectionchange' events are implemented |
michael@0 | 398 | let grid = doc.querySelector("#grid-select2"); |
michael@0 | 399 | |
michael@0 | 400 | is(typeof grid.clearSelection, "function", "clearSelection is a function on the grid"); |
michael@0 | 401 | is(typeof grid.selectedItems, "object", "selectedItems is a property on the grid"); |
michael@0 | 402 | is(typeof grid.toggleItemSelection, "function", "toggleItemSelection is function on the grid"); |
michael@0 | 403 | is(typeof grid.selectItem, "function", "selectItem is a function on the grid"); |
michael@0 | 404 | |
michael@0 | 405 | is(grid.itemCount, 2, "2 items initially"); |
michael@0 | 406 | is(grid.selectedItems.length, 0, "nothing selected initially"); |
michael@0 | 407 | |
michael@0 | 408 | grid.toggleItemSelection(grid.items[1]); |
michael@0 | 409 | ok(grid.items[1].selected, "toggleItemSelection sets truthy selected prop on previously-unselected item"); |
michael@0 | 410 | is(grid.selectedItems.length, 1, "1 item selected when we first toggleItemSelection"); |
michael@0 | 411 | is(grid.selectedItems[0], grid.items[1], "the right item is selected"); |
michael@0 | 412 | is(grid.selectedIndex, 1, "selectedIndex is correct"); |
michael@0 | 413 | |
michael@0 | 414 | grid.toggleItemSelection(grid.items[1]); |
michael@0 | 415 | is(grid.selectedItems.length, 0, "Nothing selected when we toggleItemSelection again"); |
michael@0 | 416 | |
michael@0 | 417 | // selectionchange events |
michael@0 | 418 | // in seltype=multiple mode, we track selected state on all items |
michael@0 | 419 | // (think <select multiple> not <a>) |
michael@0 | 420 | let handler = { |
michael@0 | 421 | handleEvent: function(aEvent) {} |
michael@0 | 422 | }; |
michael@0 | 423 | let handlerStub = stubMethod(handler, "handleEvent"); |
michael@0 | 424 | doc.defaultView.addEventListener("selectionchange", handler, false); |
michael@0 | 425 | info("selectionchange listener added"); |
michael@0 | 426 | |
michael@0 | 427 | // clearSelection |
michael@0 | 428 | grid.items[0].selected=true; |
michael@0 | 429 | grid.items[1].selected=true; |
michael@0 | 430 | is(grid.selectedItems.length, 2, "Both items are selected before calling clearSelection"); |
michael@0 | 431 | grid.clearSelection(); |
michael@0 | 432 | is(grid.selectedItems.length, 0, "Nothing selected when we clearSelection"); |
michael@0 | 433 | ok(!(grid.items[0].selected || grid.items[1].selected), "selected properties all falsy when we clearSelection"); |
michael@0 | 434 | is(handlerStub.callCount, 0, "clearSelection should not fire a selectionchange event"); |
michael@0 | 435 | |
michael@0 | 436 | info("calling toggleItemSelection, currently it is:" + grid.items[0].selected); |
michael@0 | 437 | // Note: A richgrid in seltype=single mode fires "select" events from selectItem |
michael@0 | 438 | grid.toggleItemSelection(grid.items[0]); |
michael@0 | 439 | info("/calling toggleItemSelection, now it is:" + grid.items[0].selected); |
michael@0 | 440 | yield waitForMs(0); |
michael@0 | 441 | |
michael@0 | 442 | is(handlerStub.callCount, 1, "selectionchange event handler was called when we selected an item"); |
michael@0 | 443 | is(handlerStub.calledWith[0].type, "selectionchange", "handler got a selectionchange event"); |
michael@0 | 444 | is(handlerStub.calledWith[0].target, grid, "select event had the originating grid as the target"); |
michael@0 | 445 | handlerStub.restore(); |
michael@0 | 446 | doc.defaultView.removeEventListener("selectionchange", handler, false); |
michael@0 | 447 | } |
michael@0 | 448 | }); |
michael@0 | 449 | |
michael@0 | 450 | gTests.push({ |
michael@0 | 451 | desc: "selectNone", |
michael@0 | 452 | run: function() { |
michael@0 | 453 | let grid = doc.querySelector("#grid-select2"); |
michael@0 | 454 | |
michael@0 | 455 | is(typeof grid.selectNone, "function", "selectNone is a function on the grid"); |
michael@0 | 456 | |
michael@0 | 457 | is(grid.itemCount, 2, "2 items initially"); |
michael@0 | 458 | |
michael@0 | 459 | // selectNone should fire a selectionchange event |
michael@0 | 460 | let handler = { |
michael@0 | 461 | handleEvent: function(aEvent) {} |
michael@0 | 462 | }; |
michael@0 | 463 | let handlerStub = stubMethod(handler, "handleEvent"); |
michael@0 | 464 | doc.defaultView.addEventListener("selectionchange", handler, false); |
michael@0 | 465 | info("selectionchange listener added"); |
michael@0 | 466 | |
michael@0 | 467 | grid.items[0].selected=true; |
michael@0 | 468 | grid.items[1].selected=true; |
michael@0 | 469 | is(grid.selectedItems.length, 2, "Both items are selected before calling selectNone"); |
michael@0 | 470 | grid.selectNone(); |
michael@0 | 471 | |
michael@0 | 472 | is(grid.selectedItems.length, 0, "Nothing selected when we selectNone"); |
michael@0 | 473 | ok(!(grid.items[0].selected || grid.items[1].selected), "selected properties all falsy when we selectNone"); |
michael@0 | 474 | |
michael@0 | 475 | is(handlerStub.callCount, 1, "selectionchange event handler was called when we selectNone"); |
michael@0 | 476 | is(handlerStub.calledWith[0].type, "selectionchange", "handler got a selectionchange event"); |
michael@0 | 477 | is(handlerStub.calledWith[0].target, grid, "selectionchange event had the originating grid as the target"); |
michael@0 | 478 | handlerStub.restore(); |
michael@0 | 479 | doc.defaultView.removeEventListener("selectionchange", handler, false); |
michael@0 | 480 | } |
michael@0 | 481 | }); |
michael@0 | 482 | |
michael@0 | 483 | function gridSlotsSetup() { |
michael@0 | 484 | let grid = this.grid = doc.createElement("richgrid"); |
michael@0 | 485 | grid.setAttribute("minSlots", 6); |
michael@0 | 486 | doc.documentElement.appendChild(grid); |
michael@0 | 487 | is(grid.ownerDocument, doc, "created grid in the expected document"); |
michael@0 | 488 | } |
michael@0 | 489 | function gridSlotsTearDown() { |
michael@0 | 490 | this.grid && this.grid.parentNode.removeChild(this.grid); |
michael@0 | 491 | } |
michael@0 | 492 | |
michael@0 | 493 | gTests.push({ |
michael@0 | 494 | desc: "richgrid slots init", |
michael@0 | 495 | setUp: gridSlotsSetup, |
michael@0 | 496 | run: function() { |
michael@0 | 497 | let grid = this.grid; |
michael@0 | 498 | // grid is initially populated with empty slots matching the minSlots attribute |
michael@0 | 499 | is(grid.children.length, 6, "minSlots slots are created"); |
michael@0 | 500 | is(grid.itemCount, 0, "slots do not count towards itemCount"); |
michael@0 | 501 | ok(Array.every(grid.children, (node) => node.nodeName == 'richgriditem'), "slots have nodeName richgriditem"); |
michael@0 | 502 | ok(Array.every(grid.children, isNotBoundByRichGrid_Item), "slots aren't bound by the richgrid-item binding"); |
michael@0 | 503 | |
michael@0 | 504 | ok(!grid.isItem(grid.children[0]), "slot fails isItem validation"); |
michael@0 | 505 | }, |
michael@0 | 506 | tearDown: gridSlotsTearDown |
michael@0 | 507 | }); |
michael@0 | 508 | |
michael@0 | 509 | gTests.push({ |
michael@0 | 510 | desc: "richgrid using slots for items", |
michael@0 | 511 | setUp: gridSlotsSetup, // creates grid with minSlots = num. slots = 6 |
michael@0 | 512 | run: function() { |
michael@0 | 513 | let grid = this.grid; |
michael@0 | 514 | let numSlots = grid.getAttribute("minSlots"); |
michael@0 | 515 | is(grid.children.length, numSlots); |
michael@0 | 516 | // adding items occupies those slots |
michael@0 | 517 | for (let idx of [0,1,2,3,4,5,6]) { |
michael@0 | 518 | let slot = grid.children[idx]; |
michael@0 | 519 | let item = grid.appendItem("item "+idx, "about:mozilla"); |
michael@0 | 520 | if (idx < numSlots) { |
michael@0 | 521 | is(grid.children.length, numSlots); |
michael@0 | 522 | is(slot, item, "The same node is reused when an item is assigned to a slot"); |
michael@0 | 523 | } else { |
michael@0 | 524 | is(typeof slot, 'undefined'); |
michael@0 | 525 | ok(item); |
michael@0 | 526 | is(grid.children.length, grid.itemCount); |
michael@0 | 527 | } |
michael@0 | 528 | } |
michael@0 | 529 | }, |
michael@0 | 530 | tearDown: gridSlotsTearDown |
michael@0 | 531 | }); |
michael@0 | 532 | |
michael@0 | 533 | gTests.push({ |
michael@0 | 534 | desc: "richgrid assign and release slots", |
michael@0 | 535 | setUp: function(){ |
michael@0 | 536 | info("assign and release slots setUp"); |
michael@0 | 537 | this.grid = doc.getElementById("slots_grid"); |
michael@0 | 538 | this.grid.scrollIntoView(); |
michael@0 | 539 | let rect = this.grid.getBoundingClientRect(); |
michael@0 | 540 | info("slots grid at top: " + rect.top + ", window.pageYOffset: " + doc.defaultView.pageYOffset); |
michael@0 | 541 | }, |
michael@0 | 542 | run: function() { |
michael@0 | 543 | let grid = this.grid; |
michael@0 | 544 | // start with 5 of 6 slots occupied |
michael@0 | 545 | for (let idx of [0,1,2,3,4]) { |
michael@0 | 546 | let item = grid.appendItem("item "+idx, "about:mozilla"); |
michael@0 | 547 | item.setAttribute("id", "test_item_"+idx); |
michael@0 | 548 | } |
michael@0 | 549 | is(grid.itemCount, 5); |
michael@0 | 550 | is(grid.children.length, 6); // see setup, where we init with 6 slots |
michael@0 | 551 | let firstItem = grid.items[0]; |
michael@0 | 552 | |
michael@0 | 553 | ok(firstItem.ownerDocument, "item has ownerDocument"); |
michael@0 | 554 | is(doc, firstItem.ownerDocument, "item's ownerDocument is the document we expect"); |
michael@0 | 555 | |
michael@0 | 556 | is(firstItem, grid.children[0], "Item and assigned slot are one and the same"); |
michael@0 | 557 | is(firstItem.control, grid, "Item is bound and its .control points back at the grid"); |
michael@0 | 558 | |
michael@0 | 559 | // before releasing, the grid should be nofified of clicks on that slot |
michael@0 | 560 | let testWindow = grid.ownerDocument.defaultView; |
michael@0 | 561 | |
michael@0 | 562 | let rect = firstItem.getBoundingClientRect(); |
michael@0 | 563 | { |
michael@0 | 564 | let handleStub = stubMethod(grid, 'handleItemClick'); |
michael@0 | 565 | // send click to item and wait for next tick; |
michael@0 | 566 | sendElementTap(testWindow, firstItem); |
michael@0 | 567 | yield waitForMs(0); |
michael@0 | 568 | |
michael@0 | 569 | is(handleStub.callCount, 1, "handleItemClick was called when we clicked an item"); |
michael@0 | 570 | handleStub.restore(); |
michael@0 | 571 | } |
michael@0 | 572 | // _releaseSlot is semi-private, we don't expect consumers of the binding to call it |
michael@0 | 573 | // but want to be sure it does what we expect |
michael@0 | 574 | grid._releaseSlot(firstItem); |
michael@0 | 575 | |
michael@0 | 576 | is(grid.itemCount, 4, "Releasing a slot gives us one less item"); |
michael@0 | 577 | is(firstItem, grid.children[0],"Released slot is still the same node we started with"); |
michael@0 | 578 | |
michael@0 | 579 | // after releasing, the grid should NOT be nofified of clicks |
michael@0 | 580 | { |
michael@0 | 581 | let handleStub = stubMethod(grid, 'handleItemClick'); |
michael@0 | 582 | // send click to item and wait for next tick; |
michael@0 | 583 | sendElementTap(testWindow, firstItem); |
michael@0 | 584 | yield waitForMs(0); |
michael@0 | 585 | |
michael@0 | 586 | is(handleStub.callCount, 0, "handleItemClick was NOT called when we clicked a released slot"); |
michael@0 | 587 | handleStub.restore(); |
michael@0 | 588 | } |
michael@0 | 589 | |
michael@0 | 590 | ok(!firstItem.mozMatchesSelector("richgriditem[value]"), "Released slot doesn't match binding selector"); |
michael@0 | 591 | ok(isNotBoundByRichGrid_Item(firstItem), "Released slot is no longer bound"); |
michael@0 | 592 | |
michael@0 | 593 | waitForCondition(() => isNotBoundByRichGrid_Item(firstItem)); |
michael@0 | 594 | ok(true, "Slot eventually gets unbound"); |
michael@0 | 595 | is(firstItem, grid.children[0], "Released slot is still at expected index in children collection"); |
michael@0 | 596 | |
michael@0 | 597 | let firstSlot = grid.children[0]; |
michael@0 | 598 | firstItem = grid.insertItemAt(0, "New item 0", "about:blank"); |
michael@0 | 599 | ok(firstItem == grid.items[0], "insertItemAt 0 creates item at expected index"); |
michael@0 | 600 | ok(firstItem == firstSlot, "insertItemAt occupies the released slot with the new item"); |
michael@0 | 601 | is(grid.itemCount, 5); |
michael@0 | 602 | is(grid.children.length, 6); |
michael@0 | 603 | is(firstItem.control, grid,"Item is bound and its .control points back at the grid"); |
michael@0 | 604 | |
michael@0 | 605 | let nextSlotIndex = grid.itemCount; |
michael@0 | 606 | let lastItem = grid.insertItemAt(9, "New item 9", "about:blank"); |
michael@0 | 607 | // Check we don't create sparse collection of items |
michael@0 | 608 | is(lastItem, grid.children[nextSlotIndex], "Item is appended at the next index when an out of bounds index is provided"); |
michael@0 | 609 | is(grid.children.length, 6); |
michael@0 | 610 | is(grid.itemCount, 6); |
michael@0 | 611 | |
michael@0 | 612 | grid.appendItem("one more", "about:blank"); |
michael@0 | 613 | is(grid.children.length, 7); |
michael@0 | 614 | is(grid.itemCount, 7); |
michael@0 | 615 | |
michael@0 | 616 | // clearAll results in slots being emptied |
michael@0 | 617 | grid.clearAll(); |
michael@0 | 618 | is(grid.children.length, 6, "Extra slots are trimmed when we clearAll"); |
michael@0 | 619 | ok(!Array.some(grid.children, (node) => node.hasAttribute("value")), "All slots have no value attribute after clearAll") |
michael@0 | 620 | }, |
michael@0 | 621 | tearDown: gridSlotsTearDown |
michael@0 | 622 | }); |
michael@0 | 623 | |
michael@0 | 624 | gTests.push({ |
michael@0 | 625 | desc: "richgrid slot management", |
michael@0 | 626 | setUp: gridSlotsSetup, |
michael@0 | 627 | run: function() { |
michael@0 | 628 | let grid = this.grid; |
michael@0 | 629 | // populate grid with some items |
michael@0 | 630 | let numSlots = grid.getAttribute("minSlots"); |
michael@0 | 631 | for (let idx of [0,1,2,3,4,5]) { |
michael@0 | 632 | let item = grid.appendItem("item "+idx, "about:mozilla"); |
michael@0 | 633 | } |
michael@0 | 634 | |
michael@0 | 635 | is(grid.itemCount, 6, "Grid setup with 6 items"); |
michael@0 | 636 | is(grid.children.length, 6, "Full grid has the expected number of slots"); |
michael@0 | 637 | |
michael@0 | 638 | // removing an item creates a replacement slot *on the end of the stack* |
michael@0 | 639 | let item = grid.removeItemAt(0); |
michael@0 | 640 | is(item.getAttribute("label"), "item 0", "removeItemAt gives back the populated node"); |
michael@0 | 641 | is(grid.children.length, 6); |
michael@0 | 642 | is(grid.itemCount, 5); |
michael@0 | 643 | is(grid.items[0].getAttribute("label"), "item 1", "removeItemAt removes the node so the nextSibling takes its place"); |
michael@0 | 644 | ok(grid.children[5] && !grid.children[5].hasAttribute("value"), "empty slot is added at the end of the existing children"); |
michael@0 | 645 | |
michael@0 | 646 | let item1 = grid.removeItem(grid.items[0]); |
michael@0 | 647 | is(grid.children.length, 6); |
michael@0 | 648 | is(grid.itemCount, 4); |
michael@0 | 649 | is(grid.items[0].getAttribute("label"), "item 2", "removeItem removes the node so the nextSibling takes its place"); |
michael@0 | 650 | }, |
michael@0 | 651 | tearDown: gridSlotsTearDown |
michael@0 | 652 | }); |
michael@0 | 653 | |
michael@0 | 654 | gTests.push({ |
michael@0 | 655 | desc: "richgrid empty slot selection", |
michael@0 | 656 | setUp: gridSlotsSetup, |
michael@0 | 657 | run: function() { |
michael@0 | 658 | let grid = this.grid; |
michael@0 | 659 | // leave grid empty, it has 6 slots |
michael@0 | 660 | |
michael@0 | 661 | is(grid.itemCount, 0, "Grid setup with 0 items"); |
michael@0 | 662 | is(grid.children.length, 6, "Empty grid has the expected number of slots"); |
michael@0 | 663 | |
michael@0 | 664 | info("slot is initially selected: " + grid.children[0].selected); |
michael@0 | 665 | grid.selectItem(grid.children[0]); |
michael@0 | 666 | info("after selectItem, slot is selected: " + grid.children[0].selected); |
michael@0 | 667 | |
michael@0 | 668 | ok(!grid.children[0].selected, "Attempting to select an empty slot has no effect"); |
michael@0 | 669 | |
michael@0 | 670 | grid.toggleItemSelection(grid.children[0]); |
michael@0 | 671 | ok(!grid.children[0].selected, "Attempting to toggle selection on an empty slot has no effect"); |
michael@0 | 672 | |
michael@0 | 673 | }, |
michael@0 | 674 | tearDown: gridSlotsTearDown |
michael@0 | 675 | }); |