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