michael@0: let doc; michael@0: michael@0: function test() { michael@0: waitForExplicitFinish(); michael@0: Task.spawn(function(){ michael@0: info(chromeRoot + "browser_tilegrid.xul"); michael@0: yield addTab(chromeRoot + "browser_tilegrid.xul"); michael@0: doc = Browser.selectedTab.browser.contentWindow.document; michael@0: }).then(runTests); michael@0: } michael@0: michael@0: function _checkIfBoundByRichGrid_Item(expected, node, idx) { michael@0: let binding = node.ownerDocument.defaultView.getComputedStyle(node).MozBinding; michael@0: let result = ('url("chrome://browser/content/bindings/grid.xml#richgrid-item")' == binding); michael@0: return (result == expected); michael@0: } michael@0: let isBoundByRichGrid_Item = _checkIfBoundByRichGrid_Item.bind(this, true); michael@0: let isNotBoundByRichGrid_Item = _checkIfBoundByRichGrid_Item.bind(this, false); michael@0: michael@0: gTests.push({ michael@0: desc: "richgrid binding is applied", michael@0: run: function() { michael@0: ok(doc, "doc got defined"); michael@0: michael@0: let grid = doc.querySelector("#grid1"); michael@0: ok(grid, "#grid1 is found"); michael@0: is(typeof grid.clearSelection, "function", "#grid1 has the binding applied"); michael@0: is(grid.items.length, 2, "#grid1 has a 2 items"); michael@0: is(grid.items[0].control, grid, "#grid1 item's control points back at #grid1'"); michael@0: ok(Array.every(grid.items, isBoundByRichGrid_Item), "All items are bound by richgrid-item"); michael@0: } michael@0: }); michael@0: michael@0: gTests.push({ michael@0: desc: "item clicks are handled", michael@0: run: function() { michael@0: let grid = doc.querySelector("#grid1"); michael@0: is(typeof grid.handleItemClick, "function", "grid.handleItemClick is a function"); michael@0: let handleStub = stubMethod(grid, 'handleItemClick'); michael@0: let itemId = "grid1_item1"; // grid.items[0].getAttribute("id"); michael@0: michael@0: // send click to item and wait for next tick; michael@0: EventUtils.sendMouseEvent({type: 'click'}, itemId, doc.defaultView); michael@0: yield waitForMs(0); michael@0: michael@0: is(handleStub.callCount, 1, "handleItemClick was called when we clicked an item"); michael@0: handleStub.restore(); michael@0: michael@0: // if the grid has a controller, it should be called too michael@0: let gridController = { michael@0: handleItemClick: function() {} michael@0: }; michael@0: let controllerHandleStub = stubMethod(gridController, "handleItemClick"); michael@0: let origController = grid.controller; michael@0: grid.controller = gridController; michael@0: michael@0: // send click to item and wait for next tick; michael@0: EventUtils.sendMouseEvent({type: 'click'}, itemId, doc.defaultView); michael@0: yield waitForMs(0); michael@0: michael@0: is(controllerHandleStub.callCount, 1, "controller.handleItemClick was called when we clicked an item"); michael@0: is(controllerHandleStub.calledWith[0], doc.getElementById(itemId), "controller.handleItemClick was passed the grid item"); michael@0: grid.controller = origController; michael@0: } michael@0: }); michael@0: michael@0: gTests.push({ michael@0: desc: "arrangeItems", michael@0: run: function() { michael@0: // implements an arrangeItems method, with optional cols, rows signature michael@0: let container = doc.getElementById("alayout"); michael@0: let grid = doc.querySelector("#grid_layout"); michael@0: michael@0: is(typeof grid.arrangeItems, "function", "arrangeItems is a function on the grid"); michael@0: michael@0: ok(grid.tileHeight, "grid has truthy tileHeight value"); michael@0: ok(grid.tileWidth, "grid has truthy tileWidth value"); michael@0: michael@0: // make the container big enough for 3 rows michael@0: container.style.height = 3 * grid.tileHeight + 20 + "px"; michael@0: michael@0: // add some items michael@0: grid.appendItem("test title", "about:blank", true); michael@0: grid.appendItem("test title", "about:blank", true); michael@0: grid.appendItem("test title", "about:blank", true); michael@0: grid.appendItem("test title", "about:blank", true); michael@0: grid.appendItem("test title", "about:blank", true); michael@0: michael@0: grid.arrangeItems(); michael@0: // they should all fit nicely in a 3x2 grid michael@0: is(grid.rowCount, 3, "rowCount is calculated correctly for a given container height and tileheight"); michael@0: is(grid.columnCount, 2, "columnCount is calculated correctly for a given container maxWidth and tilewidth"); michael@0: michael@0: // squish the available height michael@0: // should overflow (clip) a 2x2 grid michael@0: michael@0: let under3rowsHeight = (3 * grid.tileHeight -20) + "px"; michael@0: container.style.height = under3rowsHeight; michael@0: michael@0: let arrangedPromise = waitForEvent(grid, "arranged"); michael@0: grid.arrangeItems(); michael@0: yield arrangedPromise; michael@0: michael@0: ok(true, "arranged event is fired when arrangeItems is called"); michael@0: is(grid.rowCount, 2, "rowCount is re-calculated correctly for a given container height"); michael@0: } michael@0: }); michael@0: michael@0: gTests.push({ michael@0: desc: "clearAll", michael@0: run: function() { michael@0: let grid = doc.getElementById("clearGrid"); michael@0: grid.arrangeItems(); michael@0: michael@0: // grid has rows=2 so we expect at least 2 rows and 2 columns with 3 items michael@0: is(typeof grid.clearAll, "function", "clearAll is a function on the grid"); michael@0: is(grid.itemCount, 3, "grid has 3 items initially"); michael@0: is(grid.rowCount, 2, "grid has 2 rows initially"); michael@0: is(grid.columnCount, 2, "grid has 2 cols initially"); michael@0: michael@0: let arrangeSpy = spyOnMethod(grid, "arrangeItems"); michael@0: grid.clearAll(); michael@0: michael@0: is(grid.itemCount, 0, "grid has 0 itemCount after clearAll"); michael@0: is(grid.items.length, 0, "grid has 0 items after clearAll"); michael@0: // now that we use slots, an empty grid may still have non-zero rows & columns michael@0: michael@0: is(arrangeSpy.callCount, 1, "arrangeItems is called once when we clearAll"); michael@0: arrangeSpy.restore(); michael@0: } michael@0: }); michael@0: michael@0: gTests.push({ michael@0: desc: "empty grid", michael@0: run: function() { michael@0: // XXX grids have minSlots and may not be ever truly empty michael@0: michael@0: let grid = doc.getElementById("emptyGrid"); michael@0: grid.arrangeItems(); michael@0: yield waitForCondition(() => !grid.isArranging); michael@0: michael@0: // grid has 2 rows, 6 slots, 0 items michael@0: ok(grid.isBound, "binding was applied"); michael@0: is(grid.itemCount, 0, "empty grid has 0 items"); michael@0: // minSlots attr. creates unpopulated slots michael@0: is(grid.rowCount, grid.getAttribute("rows"), "empty grid with rows-attribute has that number of rows"); michael@0: is(grid.columnCount, 3, "empty grid has expected number of columns"); michael@0: michael@0: // remove rows attribute and allow space for the grid to find its own height michael@0: // for its number of slots michael@0: grid.removeAttribute("rows"); michael@0: grid.parentNode.style.height = 20+(grid.tileHeight*grid.minSlots)+"px"; michael@0: michael@0: grid.arrangeItems(); michael@0: yield waitForCondition(() => !grid.isArranging); michael@0: is(grid.rowCount, grid.minSlots, "empty grid has this.minSlots rows"); michael@0: is(grid.columnCount, 1, "empty grid has 1 column"); michael@0: } michael@0: }); michael@0: michael@0: gTests.push({ michael@0: desc: "appendItem", michael@0: run: function() { michael@0: // implements an appendItem with signature title, uri, returns item element michael@0: // appendItem triggers arrangeItems michael@0: let grid = doc.querySelector("#emptygrid"); michael@0: michael@0: is(grid.itemCount, 0, "0 itemCount when empty"); michael@0: is(grid.items.length, 0, "0 items when empty"); michael@0: is(typeof grid.appendItem, "function", "appendItem is a function on the grid"); michael@0: michael@0: let arrangeStub = stubMethod(grid, "arrangeItems"); michael@0: let newItem = grid.appendItem("test title", "about:blank"); michael@0: michael@0: ok(newItem && grid.items[0]==newItem, "appendItem gives back the item"); michael@0: is(grid.itemCount, 1, "itemCount is incremented when we appendItem"); michael@0: is(newItem.getAttribute("label"), "test title", "title ends up on label attribute"); michael@0: is(newItem.getAttribute("value"), "about:blank", "url ends up on value attribute"); michael@0: michael@0: is(arrangeStub.callCount, 1, "arrangeItems is called when we appendItem"); michael@0: arrangeStub.restore(); michael@0: } michael@0: }); michael@0: michael@0: gTests.push({ michael@0: desc: "getItemAtIndex", michael@0: run: function() { michael@0: // implements a getItemAtIndex method michael@0: let grid = doc.querySelector("#grid2"); michael@0: is(typeof grid.getItemAtIndex, "function", "getItemAtIndex is a function on the grid"); michael@0: is(grid.getItemAtIndex(0).getAttribute("id"), "grid2_item1", "getItemAtIndex retrieves the first item"); michael@0: is(grid.getItemAtIndex(1).getAttribute("id"), "grid2_item2", "getItemAtIndex item at index 2"); michael@0: ok(!grid.getItemAtIndex(5), "getItemAtIndex out-of-bounds index returns falsy"); michael@0: } michael@0: }); michael@0: michael@0: gTests.push({ michael@0: desc: "removeItemAt", michael@0: run: function() { michael@0: // implements a removeItemAt method, with 'index' signature michael@0: // removeItemAt triggers arrangeItems michael@0: let grid = doc.querySelector("#grid2"); michael@0: michael@0: is(grid.itemCount, 2, "2 items initially"); michael@0: is(typeof grid.removeItemAt, "function", "removeItemAt is a function on the grid"); michael@0: michael@0: let arrangeStub = stubMethod(grid, "arrangeItems"); michael@0: let removedItem = grid.removeItemAt(0); michael@0: michael@0: ok(removedItem, "removeItemAt gives back an item"); michael@0: is(removedItem.getAttribute("id"), "grid2_item1", "removeItemAt gives back the correct item"); michael@0: is(grid.items[0].getAttribute("id"), "grid2_item2", "2nd item becomes the first item"); michael@0: is(grid.itemCount, 1, "itemCount is decremented when we removeItemAt"); michael@0: michael@0: is(arrangeStub.callCount, 1, "arrangeItems is called when we removeItemAt"); michael@0: arrangeStub.restore(); michael@0: } michael@0: }); michael@0: michael@0: gTests.push({ michael@0: desc: "insertItemAt", michael@0: run: function() { michael@0: // implements an insertItemAt method, with index, title, uri.spec signature michael@0: // insertItemAt triggers arrangeItems michael@0: let grid = doc.querySelector("#grid3"); michael@0: michael@0: is(grid.itemCount, 2, "2 items initially"); michael@0: is(typeof grid.insertItemAt, "function", "insertItemAt is a function on the grid"); michael@0: michael@0: let arrangeStub = stubMethod(grid, "arrangeItems"); michael@0: let insertedAt0 = grid.insertItemAt(0, "inserted item 0", "http://example.com/inserted0"); michael@0: let insertedAt00 = grid.insertItemAt(0, "inserted item 00", "http://example.com/inserted00"); michael@0: michael@0: ok(insertedAt0 && insertedAt00, "insertItemAt gives back an item"); michael@0: michael@0: is(insertedAt0.getAttribute("label"), "inserted item 0", "insertItemAt creates item with the correct label"); michael@0: is(insertedAt0.getAttribute("value"), "http://example.com/inserted0", "insertItemAt creates item with the correct url value"); michael@0: michael@0: is(grid.items[0], insertedAt00, "item is inserted at the correct index"); michael@0: is(grid.children[0], insertedAt00, "first item occupies the first slot"); michael@0: is(grid.items[1], insertedAt0, "item is inserted at the correct index"); michael@0: is(grid.children[1], insertedAt0, "next item occupies the next slot"); michael@0: michael@0: is(grid.items[2].getAttribute("label"), "First item", "Old first item is now at index 2"); michael@0: is(grid.items[3].getAttribute("label"), "2nd item", "Old 2nd item is now at index 3"); michael@0: michael@0: is(grid.itemCount, 4, "itemCount is incremented when we insertItemAt"); michael@0: michael@0: is(arrangeStub.callCount, 2, "arrangeItems is called when we insertItemAt"); michael@0: arrangeStub.restore(); michael@0: } michael@0: }); michael@0: michael@0: gTests.push({ michael@0: desc: "getIndexOfItem", michael@0: run: function() { michael@0: // implements a getIndexOfItem method, with item (element) signature michael@0: // insertItemAt triggers arrangeItems michael@0: let grid = doc.querySelector("#grid4"); michael@0: michael@0: is(grid.itemCount, 2, "2 items initially"); michael@0: is(typeof grid.getIndexOfItem, "function", "getIndexOfItem is a function on the grid"); michael@0: michael@0: let item = doc.getElementById("grid4_item2"); michael@0: let badItem = doc.createElement("richgriditem"); michael@0: michael@0: is(grid.getIndexOfItem(item), 1, "getIndexOfItem returns the correct value for an item"); michael@0: is(grid.getIndexOfItem(badItem), -1, "getIndexOfItem returns -1 for items it doesn't contain"); michael@0: } michael@0: }); michael@0: michael@0: gTests.push({ michael@0: desc: "getItemsByUrl", michael@0: run: function() { michael@0: let grid = doc.querySelector("#grid5"); michael@0: michael@0: is(grid.itemCount, 4, "4 items total"); michael@0: is(typeof grid.getItemsByUrl, "function", "getItemsByUrl is a function on the grid"); michael@0: michael@0: ['about:blank', 'http://bugzilla.mozilla.org/'].forEach(function(testUrl) { michael@0: let items = grid.getItemsByUrl(testUrl); michael@0: is(items.length, 2, "2 matching items in the test grid"); michael@0: is(items.item(0).url, testUrl, "Matched item has correct url property"); michael@0: is(items.item(1).url, testUrl, "Matched item has correct url property"); michael@0: }); michael@0: michael@0: let badUrl = 'http://gopher.well.com:70/'; michael@0: let items = grid.getItemsByUrl(badUrl); michael@0: is(items.length, 0, "0 items matched url: "+badUrl); michael@0: michael@0: } michael@0: }); michael@0: michael@0: gTests.push({ michael@0: desc: "removeItem", michael@0: run: function() { michael@0: let grid = doc.querySelector("#grid5"); michael@0: michael@0: is(grid.itemCount, 4, "4 items total"); michael@0: is(typeof grid.removeItem, "function", "removeItem is a function on the grid"); michael@0: michael@0: let arrangeStub = stubMethod(grid, "arrangeItems"); michael@0: let removedFirst = grid.removeItem( grid.items[0] ); michael@0: michael@0: is(arrangeStub.callCount, 1, "arrangeItems is called when we removeItem"); michael@0: michael@0: let removed2nd = grid.removeItem( grid.items[0], true); michael@0: is(removed2nd.getAttribute("label"), "2nd item", "the next item was returned"); michael@0: is(grid.itemCount, 2, "2 items remain"); michael@0: michael@0: // callCount should still be at 1 michael@0: is(arrangeStub.callCount, 1, "arrangeItems is not called when we pass the truthy skipArrange param"); michael@0: michael@0: let otherItem = grid.ownerDocument.querySelector("#grid6_item1"); michael@0: let removedFail = grid.removeItem(otherItem); michael@0: ok(!removedFail, "Falsy value returned when non-child item passed"); michael@0: is(grid.itemCount, 2, "2 items remain"); michael@0: michael@0: // callCount should still be at 1 michael@0: is(arrangeStub.callCount, 1, "arrangeItems is not called when nothing is matched"); michael@0: michael@0: arrangeStub.restore(); michael@0: } michael@0: }); michael@0: michael@0: gTests.push({ michael@0: desc: "selections (single)", michael@0: run: function() { michael@0: // when seltype is single, michael@0: // maintains a selectedItem property michael@0: // maintains a selectedIndex property michael@0: // clearSelection, selectItem, toggleItemSelection methods are implemented michael@0: // 'select' events are implemented michael@0: let grid = doc.querySelector("#grid-select1"); michael@0: michael@0: is(typeof grid.clearSelection, "function", "clearSelection is a function on the grid"); michael@0: is(typeof grid.selectedItems, "object", "selectedItems is a property on the grid"); michael@0: is(typeof grid.toggleItemSelection, "function", "toggleItemSelection is function on the grid"); michael@0: is(typeof grid.selectItem, "function", "selectItem is a function on the grid"); michael@0: michael@0: is(grid.itemCount, 2, "2 items initially"); michael@0: is(grid.selectedItems.length, 0, "nothing selected initially"); michael@0: michael@0: grid.toggleItemSelection(grid.items[1]); michael@0: ok(grid.items[1].selected, "toggleItemSelection sets truthy selected prop on previously-unselected item"); michael@0: is(grid.selectedIndex, 1, "selectedIndex is correct"); michael@0: michael@0: grid.toggleItemSelection(grid.items[1]); michael@0: ok(!grid.items[1].selected, "toggleItemSelection sets falsy selected prop on previously-selected item"); michael@0: is(grid.selectedIndex, -1, "selectedIndex reports correctly with nothing selected"); michael@0: michael@0: // item selection michael@0: grid.selectItem(grid.items[1]); michael@0: ok(grid.items[1].selected, "Item selected property is truthy after grid.selectItem"); michael@0: ok(grid.items[1].getAttribute("selected"), "Item selected attribute is truthy after grid.selectItem"); michael@0: ok(grid.selectedItems.length, "There are selectedItems after grid.selectItem"); michael@0: michael@0: // select events michael@0: // in seltype=single mode, select is like the default action for the tile michael@0: // (think , not not ) michael@0: let handler = { michael@0: handleEvent: function(aEvent) {} michael@0: }; michael@0: let handlerStub = stubMethod(handler, "handleEvent"); michael@0: doc.defaultView.addEventListener("selectionchange", handler, false); michael@0: info("selectionchange listener added"); michael@0: michael@0: // clearSelection michael@0: grid.items[0].selected=true; michael@0: grid.items[1].selected=true; michael@0: is(grid.selectedItems.length, 2, "Both items are selected before calling clearSelection"); michael@0: grid.clearSelection(); michael@0: is(grid.selectedItems.length, 0, "Nothing selected when we clearSelection"); michael@0: ok(!(grid.items[0].selected || grid.items[1].selected), "selected properties all falsy when we clearSelection"); michael@0: is(handlerStub.callCount, 0, "clearSelection should not fire a selectionchange event"); michael@0: michael@0: info("calling toggleItemSelection, currently it is:" + grid.items[0].selected); michael@0: // Note: A richgrid in seltype=single mode fires "select" events from selectItem michael@0: grid.toggleItemSelection(grid.items[0]); michael@0: info("/calling toggleItemSelection, now it is:" + grid.items[0].selected); michael@0: yield waitForMs(0); michael@0: michael@0: is(handlerStub.callCount, 1, "selectionchange event handler was called when we selected an item"); michael@0: is(handlerStub.calledWith[0].type, "selectionchange", "handler got a selectionchange event"); michael@0: is(handlerStub.calledWith[0].target, grid, "select event had the originating grid as the target"); michael@0: handlerStub.restore(); michael@0: doc.defaultView.removeEventListener("selectionchange", handler, false); michael@0: } michael@0: }); michael@0: michael@0: gTests.push({ michael@0: desc: "selectNone", michael@0: run: function() { michael@0: let grid = doc.querySelector("#grid-select2"); michael@0: michael@0: is(typeof grid.selectNone, "function", "selectNone is a function on the grid"); michael@0: michael@0: is(grid.itemCount, 2, "2 items initially"); michael@0: michael@0: // selectNone should fire a selectionchange event michael@0: let handler = { michael@0: handleEvent: function(aEvent) {} michael@0: }; michael@0: let handlerStub = stubMethod(handler, "handleEvent"); michael@0: doc.defaultView.addEventListener("selectionchange", handler, false); michael@0: info("selectionchange listener added"); michael@0: michael@0: grid.items[0].selected=true; michael@0: grid.items[1].selected=true; michael@0: is(grid.selectedItems.length, 2, "Both items are selected before calling selectNone"); michael@0: grid.selectNone(); michael@0: michael@0: is(grid.selectedItems.length, 0, "Nothing selected when we selectNone"); michael@0: ok(!(grid.items[0].selected || grid.items[1].selected), "selected properties all falsy when we selectNone"); michael@0: michael@0: is(handlerStub.callCount, 1, "selectionchange event handler was called when we selectNone"); michael@0: is(handlerStub.calledWith[0].type, "selectionchange", "handler got a selectionchange event"); michael@0: is(handlerStub.calledWith[0].target, grid, "selectionchange event had the originating grid as the target"); michael@0: handlerStub.restore(); michael@0: doc.defaultView.removeEventListener("selectionchange", handler, false); michael@0: } michael@0: }); michael@0: michael@0: function gridSlotsSetup() { michael@0: let grid = this.grid = doc.createElement("richgrid"); michael@0: grid.setAttribute("minSlots", 6); michael@0: doc.documentElement.appendChild(grid); michael@0: is(grid.ownerDocument, doc, "created grid in the expected document"); michael@0: } michael@0: function gridSlotsTearDown() { michael@0: this.grid && this.grid.parentNode.removeChild(this.grid); michael@0: } michael@0: michael@0: gTests.push({ michael@0: desc: "richgrid slots init", michael@0: setUp: gridSlotsSetup, michael@0: run: function() { michael@0: let grid = this.grid; michael@0: // grid is initially populated with empty slots matching the minSlots attribute michael@0: is(grid.children.length, 6, "minSlots slots are created"); michael@0: is(grid.itemCount, 0, "slots do not count towards itemCount"); michael@0: ok(Array.every(grid.children, (node) => node.nodeName == 'richgriditem'), "slots have nodeName richgriditem"); michael@0: ok(Array.every(grid.children, isNotBoundByRichGrid_Item), "slots aren't bound by the richgrid-item binding"); michael@0: michael@0: ok(!grid.isItem(grid.children[0]), "slot fails isItem validation"); michael@0: }, michael@0: tearDown: gridSlotsTearDown michael@0: }); michael@0: michael@0: gTests.push({ michael@0: desc: "richgrid using slots for items", michael@0: setUp: gridSlotsSetup, // creates grid with minSlots = num. slots = 6 michael@0: run: function() { michael@0: let grid = this.grid; michael@0: let numSlots = grid.getAttribute("minSlots"); michael@0: is(grid.children.length, numSlots); michael@0: // adding items occupies those slots michael@0: for (let idx of [0,1,2,3,4,5,6]) { michael@0: let slot = grid.children[idx]; michael@0: let item = grid.appendItem("item "+idx, "about:mozilla"); michael@0: if (idx < numSlots) { michael@0: is(grid.children.length, numSlots); michael@0: is(slot, item, "The same node is reused when an item is assigned to a slot"); michael@0: } else { michael@0: is(typeof slot, 'undefined'); michael@0: ok(item); michael@0: is(grid.children.length, grid.itemCount); michael@0: } michael@0: } michael@0: }, michael@0: tearDown: gridSlotsTearDown michael@0: }); michael@0: michael@0: gTests.push({ michael@0: desc: "richgrid assign and release slots", michael@0: setUp: function(){ michael@0: info("assign and release slots setUp"); michael@0: this.grid = doc.getElementById("slots_grid"); michael@0: this.grid.scrollIntoView(); michael@0: let rect = this.grid.getBoundingClientRect(); michael@0: info("slots grid at top: " + rect.top + ", window.pageYOffset: " + doc.defaultView.pageYOffset); michael@0: }, michael@0: run: function() { michael@0: let grid = this.grid; michael@0: // start with 5 of 6 slots occupied michael@0: for (let idx of [0,1,2,3,4]) { michael@0: let item = grid.appendItem("item "+idx, "about:mozilla"); michael@0: item.setAttribute("id", "test_item_"+idx); michael@0: } michael@0: is(grid.itemCount, 5); michael@0: is(grid.children.length, 6); // see setup, where we init with 6 slots michael@0: let firstItem = grid.items[0]; michael@0: michael@0: ok(firstItem.ownerDocument, "item has ownerDocument"); michael@0: is(doc, firstItem.ownerDocument, "item's ownerDocument is the document we expect"); michael@0: michael@0: is(firstItem, grid.children[0], "Item and assigned slot are one and the same"); michael@0: is(firstItem.control, grid, "Item is bound and its .control points back at the grid"); michael@0: michael@0: // before releasing, the grid should be nofified of clicks on that slot michael@0: let testWindow = grid.ownerDocument.defaultView; michael@0: michael@0: let rect = firstItem.getBoundingClientRect(); michael@0: { michael@0: let handleStub = stubMethod(grid, 'handleItemClick'); michael@0: // send click to item and wait for next tick; michael@0: sendElementTap(testWindow, firstItem); michael@0: yield waitForMs(0); michael@0: michael@0: is(handleStub.callCount, 1, "handleItemClick was called when we clicked an item"); michael@0: handleStub.restore(); michael@0: } michael@0: // _releaseSlot is semi-private, we don't expect consumers of the binding to call it michael@0: // but want to be sure it does what we expect michael@0: grid._releaseSlot(firstItem); michael@0: michael@0: is(grid.itemCount, 4, "Releasing a slot gives us one less item"); michael@0: is(firstItem, grid.children[0],"Released slot is still the same node we started with"); michael@0: michael@0: // after releasing, the grid should NOT be nofified of clicks michael@0: { michael@0: let handleStub = stubMethod(grid, 'handleItemClick'); michael@0: // send click to item and wait for next tick; michael@0: sendElementTap(testWindow, firstItem); michael@0: yield waitForMs(0); michael@0: michael@0: is(handleStub.callCount, 0, "handleItemClick was NOT called when we clicked a released slot"); michael@0: handleStub.restore(); michael@0: } michael@0: michael@0: ok(!firstItem.mozMatchesSelector("richgriditem[value]"), "Released slot doesn't match binding selector"); michael@0: ok(isNotBoundByRichGrid_Item(firstItem), "Released slot is no longer bound"); michael@0: michael@0: waitForCondition(() => isNotBoundByRichGrid_Item(firstItem)); michael@0: ok(true, "Slot eventually gets unbound"); michael@0: is(firstItem, grid.children[0], "Released slot is still at expected index in children collection"); michael@0: michael@0: let firstSlot = grid.children[0]; michael@0: firstItem = grid.insertItemAt(0, "New item 0", "about:blank"); michael@0: ok(firstItem == grid.items[0], "insertItemAt 0 creates item at expected index"); michael@0: ok(firstItem == firstSlot, "insertItemAt occupies the released slot with the new item"); michael@0: is(grid.itemCount, 5); michael@0: is(grid.children.length, 6); michael@0: is(firstItem.control, grid,"Item is bound and its .control points back at the grid"); michael@0: michael@0: let nextSlotIndex = grid.itemCount; michael@0: let lastItem = grid.insertItemAt(9, "New item 9", "about:blank"); michael@0: // Check we don't create sparse collection of items michael@0: is(lastItem, grid.children[nextSlotIndex], "Item is appended at the next index when an out of bounds index is provided"); michael@0: is(grid.children.length, 6); michael@0: is(grid.itemCount, 6); michael@0: michael@0: grid.appendItem("one more", "about:blank"); michael@0: is(grid.children.length, 7); michael@0: is(grid.itemCount, 7); michael@0: michael@0: // clearAll results in slots being emptied michael@0: grid.clearAll(); michael@0: is(grid.children.length, 6, "Extra slots are trimmed when we clearAll"); michael@0: ok(!Array.some(grid.children, (node) => node.hasAttribute("value")), "All slots have no value attribute after clearAll") michael@0: }, michael@0: tearDown: gridSlotsTearDown michael@0: }); michael@0: michael@0: gTests.push({ michael@0: desc: "richgrid slot management", michael@0: setUp: gridSlotsSetup, michael@0: run: function() { michael@0: let grid = this.grid; michael@0: // populate grid with some items michael@0: let numSlots = grid.getAttribute("minSlots"); michael@0: for (let idx of [0,1,2,3,4,5]) { michael@0: let item = grid.appendItem("item "+idx, "about:mozilla"); michael@0: } michael@0: michael@0: is(grid.itemCount, 6, "Grid setup with 6 items"); michael@0: is(grid.children.length, 6, "Full grid has the expected number of slots"); michael@0: michael@0: // removing an item creates a replacement slot *on the end of the stack* michael@0: let item = grid.removeItemAt(0); michael@0: is(item.getAttribute("label"), "item 0", "removeItemAt gives back the populated node"); michael@0: is(grid.children.length, 6); michael@0: is(grid.itemCount, 5); michael@0: is(grid.items[0].getAttribute("label"), "item 1", "removeItemAt removes the node so the nextSibling takes its place"); michael@0: ok(grid.children[5] && !grid.children[5].hasAttribute("value"), "empty slot is added at the end of the existing children"); michael@0: michael@0: let item1 = grid.removeItem(grid.items[0]); michael@0: is(grid.children.length, 6); michael@0: is(grid.itemCount, 4); michael@0: is(grid.items[0].getAttribute("label"), "item 2", "removeItem removes the node so the nextSibling takes its place"); michael@0: }, michael@0: tearDown: gridSlotsTearDown michael@0: }); michael@0: michael@0: gTests.push({ michael@0: desc: "richgrid empty slot selection", michael@0: setUp: gridSlotsSetup, michael@0: run: function() { michael@0: let grid = this.grid; michael@0: // leave grid empty, it has 6 slots michael@0: michael@0: is(grid.itemCount, 0, "Grid setup with 0 items"); michael@0: is(grid.children.length, 6, "Empty grid has the expected number of slots"); michael@0: michael@0: info("slot is initially selected: " + grid.children[0].selected); michael@0: grid.selectItem(grid.children[0]); michael@0: info("after selectItem, slot is selected: " + grid.children[0].selected); michael@0: michael@0: ok(!grid.children[0].selected, "Attempting to select an empty slot has no effect"); michael@0: michael@0: grid.toggleItemSelection(grid.children[0]); michael@0: ok(!grid.children[0].selected, "Attempting to toggle selection on an empty slot has no effect"); michael@0: michael@0: }, michael@0: tearDown: gridSlotsTearDown michael@0: });