michael@0: /* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ michael@0: /* vim: set ts=2 et sw=2 tw=80: */ michael@0: /* Any copyright is dedicated to the Public Domain. michael@0: http://creativecommons.org/publicdomain/zero/1.0/ */ michael@0: michael@0: "use strict"; michael@0: michael@0: ////////////////////////////////////////////////////////////////////////// michael@0: // Test helpers michael@0: michael@0: let TopSitesTestHelper = { michael@0: get grid() { michael@0: return Browser.selectedBrowser.contentDocument.getElementById("start-topsites-grid"); michael@0: }, michael@0: get document() { michael@0: return Browser.selectedBrowser.contentDocument; michael@0: }, michael@0: setup: function() { michael@0: return Task.spawn(function(){ michael@0: if (BrowserUI.isStartTabVisible) michael@0: return; michael@0: michael@0: yield addTab("about:start"); michael@0: michael@0: yield waitForCondition(() => BrowserUI.isStartTabVisible); michael@0: }); michael@0: }, michael@0: mockLinks: function th_mockLinks(aLinks) { michael@0: // create link objects where index. corresponds to grid position michael@0: // falsy values are set to null michael@0: let links = (typeof aLinks == "string") ? michael@0: aLinks.split(/\s*,\s*/) : aLinks; michael@0: michael@0: links = links.map(function (id) { michael@0: return (id) ? {url: "http://example.com/#" + id, title: id} : null; michael@0: }); michael@0: return links; michael@0: }, michael@0: siteFromNode: function th_siteFromNode(aNode) { michael@0: return { michael@0: url: aNode.getAttribute("value"), michael@0: title: aNode.getAttribute("label") michael@0: } michael@0: }, michael@0: clearHistory: function th_clearHistory() { michael@0: PlacesUtils.history.removeAllPages(); michael@0: }, michael@0: fillHistory: function th_fillHistory(aLinks) { michael@0: return Task.spawn(function(){ michael@0: let numLinks = aLinks.length; michael@0: let transitionLink = Ci.nsINavHistoryService.TRANSITION_LINK; michael@0: michael@0: let updateDeferred = Promise.defer(); michael@0: michael@0: for (let link of aLinks.reverse()) { michael@0: let place = { michael@0: uri: Util.makeURI(link.url), michael@0: title: link.title, michael@0: visits: [{visitDate: Date.now() * 1000, transitionType: transitionLink}] michael@0: }; michael@0: try { michael@0: PlacesUtils.asyncHistory.updatePlaces(place, { michael@0: handleError: function (aError) { michael@0: ok(false, "couldn't add visit to history"); michael@0: throw new Task.Result(aError); michael@0: }, michael@0: handleResult: function () {}, michael@0: handleCompletion: function () { michael@0: if(--numLinks <= 0) { michael@0: updateDeferred.resolve(true); michael@0: } michael@0: } michael@0: }); michael@0: } catch(e) { michael@0: ok(false, "because: " + e); michael@0: } michael@0: } michael@0: return updateDeferred.promise; michael@0: }); michael@0: }, michael@0: michael@0: /** michael@0: * Allows to specify the list of pinned links (that have a fixed position in michael@0: * the grid. michael@0: * @param aLinksPattern the pattern (see below) michael@0: * michael@0: * Example: setPinnedLinks("foo,,bar") michael@0: * Result: 'http://example.com/#foo' is pinned in the first cell. 'http://example.com/#bar' is michael@0: * pinned in the third cell. michael@0: */ michael@0: setPinnedLinks: function th_setPinnedLinks(aLinks) { michael@0: let links = TopSitesTestHelper.mockLinks(aLinks); michael@0: michael@0: // (we trust that NewTabUtils works, and test our consumption of it) michael@0: // clear all existing pins michael@0: Array.forEach(NewTabUtils.pinnedLinks.links, function(aLink){ michael@0: if(aLink) michael@0: NewTabUtils.pinnedLinks.unpin(aLink); michael@0: }); michael@0: michael@0: links.forEach(function(aLink, aIndex){ michael@0: if(aLink) { michael@0: NewTabUtils.pinnedLinks.pin(aLink, aIndex); michael@0: } michael@0: }); michael@0: NewTabUtils.pinnedLinks.save(); michael@0: }, michael@0: michael@0: /** michael@0: * Allows to provide a list of links that is used to construct the grid. michael@0: * @param aLinksPattern the pattern (see below) michael@0: * @param aPinnedLinksPattern the pattern (see below) michael@0: * michael@0: * Example: setLinks("dougal,florence,zebedee") michael@0: * Result: [{url: "http://example.com/#dougal", title: "dougal"}, michael@0: * {url: "http://example.com/#florence", title: "florence"} michael@0: * {url: "http://example.com/#zebedee", title: "zebedee"}] michael@0: * Example: setLinks("dougal,florence,zebedee","dougal,,zebedee") michael@0: * Result: http://example.com/#dougal is pinned at index 0, http://example.com/#florence at index 2 michael@0: */ michael@0: michael@0: setLinks: function th_setLinks(aLinks, aPinnedLinks) { michael@0: let links = TopSitesTestHelper.mockLinks(aLinks); michael@0: if (links.filter(function(aLink){ michael@0: return !aLink; michael@0: }).length) { michael@0: throw new Error("null link objects in setLinks"); michael@0: } michael@0: michael@0: return Task.spawn(function() { michael@0: TopSitesTestHelper.clearHistory(); michael@0: michael@0: yield Task.spawn(TopSitesTestHelper.fillHistory(links)); michael@0: michael@0: if(aPinnedLinks) { michael@0: TopSitesTestHelper.setPinnedLinks(aPinnedLinks); michael@0: } michael@0: michael@0: // reset the TopSites state, have it update its cache with the new data fillHistory put there michael@0: yield TopSites.prepareCache(true); michael@0: }); michael@0: }, michael@0: michael@0: updatePagesAndWait: function th_updatePagesAndWait() { michael@0: let deferredUpdate = Promise.defer(); michael@0: let updater = { michael@0: update: function() { michael@0: NewTabUtils.allPages.unregister(updater); michael@0: deferredUpdate.resolve(true); michael@0: } michael@0: }; michael@0: NewTabUtils.allPages.register(updater); michael@0: setTimeout(function() { michael@0: NewTabUtils.allPages.update(); michael@0: }, 0); michael@0: return deferredUpdate.promise; michael@0: }, michael@0: michael@0: tearDown: function th_tearDown() { michael@0: TopSitesTestHelper.clearHistory(); michael@0: } michael@0: }; michael@0: michael@0: michael@0: ////////////////////////////////////////////////////////////////////////// michael@0: michael@0: michael@0: function test() { michael@0: registerCleanupFunction(TopSitesTestHelper.tearDown); michael@0: runTests(); michael@0: } michael@0: michael@0: gTests.push({ michael@0: desc: "TopSites dependencies", michael@0: run: function() { michael@0: ok(NewTabUtils, "NewTabUtils is truthy"); michael@0: ok(TopSites, "TopSites is truthy"); michael@0: } michael@0: }); michael@0: michael@0: gTests.push({ michael@0: desc: "load and display top sites", michael@0: setUp: function() { michael@0: yield TopSitesTestHelper.setup(); michael@0: let grid = TopSitesTestHelper.grid; michael@0: michael@0: // setup - set history to known state michael@0: yield TopSitesTestHelper.setLinks("brian,dougal,dylan,ermintrude,florence,moose,sgtsam,train,zebedee,zeebad"); michael@0: michael@0: let arrangedPromise = waitForEvent(grid, "arranged"); michael@0: yield TopSitesTestHelper.updatePagesAndWait(); michael@0: // pause until the update has fired and the view is finishd updating michael@0: yield arrangedPromise; michael@0: }, michael@0: run: function() { michael@0: let grid = TopSitesTestHelper.grid; michael@0: let items = grid.items; michael@0: is(items.length, 8, "should be 8 topsites"); // i.e. not 10 michael@0: if(items.length) { michael@0: let firstitem = items[0]; michael@0: is( michael@0: firstitem.getAttribute("label"), michael@0: "brian", michael@0: "first item label should be 'brian': " + firstitem.getAttribute("label") michael@0: ); michael@0: is( michael@0: firstitem.getAttribute("value"), michael@0: "http://example.com/#brian", michael@0: "first item url should be 'http://example.com/#brian': " + firstitem.getAttribute("url") michael@0: ); michael@0: } michael@0: } michael@0: }); michael@0: michael@0: gTests.push({ michael@0: desc: "pinned sites", michael@0: pins: "dangermouse,zebedee,,,dougal", michael@0: setUp: function() { michael@0: yield TopSitesTestHelper.setup(); michael@0: // setup - set history to known state michael@0: yield TopSitesTestHelper.setLinks( michael@0: "brian,dougal,dylan,ermintrude,florence,moose,sgtsam,train,zebedee,zeebad", michael@0: this.pins michael@0: ); michael@0: // pause until the update has fired and the view is finishd updating michael@0: let arrangedPromise = waitForEvent(TopSitesTestHelper.grid, "arranged"); michael@0: yield TopSitesTestHelper.updatePagesAndWait(); michael@0: yield arrangedPromise; michael@0: }, michael@0: run: function() { michael@0: // test that pinned state of each site as rendered matches our expectations michael@0: let pins = this.pins.split(","); michael@0: let items = TopSitesTestHelper.grid.items; michael@0: is(items.length, 8, "should be 8 topsites in the grid"); michael@0: michael@0: is(TopSitesTestHelper.document.querySelectorAll("#start-topsites-grid > [pinned]").length, 3, "should be 3 children with 'pinned' attribute"); michael@0: try { michael@0: Array.forEach(items, function(aItem, aIndex){ michael@0: // pinned state should agree with the pins array michael@0: is( michael@0: aItem.hasAttribute("pinned"), !!pins[aIndex], michael@0: "site at index " + aIndex + " was " +aItem.hasAttribute("pinned") michael@0: +", should agree with " + !!pins[aIndex] michael@0: ); michael@0: if (pins[aIndex]) { michael@0: is( michael@0: aItem.getAttribute("label"), michael@0: pins[aIndex], michael@0: "pinned site has correct label: " + pins[aIndex] +"=="+ aItem.getAttribute("label") michael@0: ); michael@0: } michael@0: }, this); michael@0: } catch(e) { michael@0: ok(false, this.desc + ": Test of pinned state on items failed"); michael@0: info("because: " + e.message + "\n" + e.stack); michael@0: } michael@0: } michael@0: }); michael@0: michael@0: gTests.push({ michael@0: desc: "pin site", michael@0: setUp: function() { michael@0: yield TopSitesTestHelper.setup(); michael@0: // setup - set history to known state michael@0: yield TopSitesTestHelper.setLinks("sgtsam,train,zebedee,zeebad", []); // nothing initially pinned michael@0: // pause until the update has fired and the view is finishd updating michael@0: let arrangedPromise = waitForEvent(TopSitesTestHelper.grid, "arranged"); michael@0: yield TopSitesTestHelper.updatePagesAndWait(); michael@0: yield arrangedPromise; michael@0: }, michael@0: run: function() { michael@0: // pin a site michael@0: // test that site is pinned as expected michael@0: // and that sites fill positions around it michael@0: let grid = TopSitesTestHelper.grid, michael@0: items = grid.items; michael@0: is(items.length, 4, this.desc + ": should be 4 topsites"); michael@0: michael@0: let tile = grid.items[2], michael@0: url = tile.getAttribute("value"), michael@0: title = tile.getAttribute("label"); michael@0: michael@0: info(this.desc + ": pinning site at index 2"); michael@0: TopSites.pinSites([{ michael@0: url: url, michael@0: title: title michael@0: }], [2]); michael@0: michael@0: // pinning shouldn't require re-arranging - just wait for isUpdating flag to flip michael@0: yield waitForCondition(function(){ michael@0: return !grid.controller.isUpdating; michael@0: }); michael@0: michael@0: let thirdTile = grid.items[2]; michael@0: ok( thirdTile.hasAttribute("pinned"), thirdTile.getAttribute("value")+ " should look pinned" ); michael@0: michael@0: // visit some more sites michael@0: yield TopSitesTestHelper.fillHistory( TopSitesTestHelper.mockLinks("brian,dougal,dylan,ermintrude,florence,moose") ); michael@0: michael@0: // force flush and repopulation of links cache michael@0: yield TopSites.prepareCache(true); michael@0: // pause until the update has fired and the view is finishd updating michael@0: let arrangedPromise = waitForEvent(grid, "arranged"); michael@0: yield TopSitesTestHelper.updatePagesAndWait(); michael@0: yield arrangedPromise; michael@0: michael@0: // check zebedee is still pinned at index 2 michael@0: is( items[2].getAttribute("label"), "zebedee", "Pinned site remained at its index" ); michael@0: ok( items[2].hasAttribute("pinned"), "3rd site should still look pinned" ); michael@0: } michael@0: }); michael@0: michael@0: gTests.push({ michael@0: desc: "unpin site", michael@0: pins: ",zebedee", michael@0: setUp: function() { michael@0: yield TopSitesTestHelper.setup(); michael@0: // setup - set history to known state michael@0: yield TopSitesTestHelper.setLinks( michael@0: "brian,dougal,dylan,ermintrude,florence,moose,sgtsam,train,zebedee,zeebad", michael@0: this.pins michael@0: ); michael@0: // pause until the update has fired and the view is finishd updating michael@0: let arrangedPromise = waitForEvent(TopSitesTestHelper.grid, "arranged"); michael@0: yield TopSitesTestHelper.updatePagesAndWait(); michael@0: yield arrangedPromise; michael@0: }, michael@0: run: function() { michael@0: // unpin a pinned site michael@0: // test that sites are unpinned as expected michael@0: let grid = TopSitesTestHelper.grid, michael@0: items = grid.items; michael@0: is(items.length, 8, this.desc + ": should be 8 topsites"); michael@0: let site = { michael@0: url: items[1].getAttribute("value"), michael@0: title: items[1].getAttribute("label") michael@0: }; michael@0: // verify assumptions before unpinning this site michael@0: ok( NewTabUtils.pinnedLinks.isPinned(site), "2nd item is pinned" ); michael@0: ok( items[1].hasAttribute("pinned"), "2nd item has pinned attribute" ); michael@0: michael@0: // unpinning shouldn't require re-arranging - just wait for isUpdating flag to flip michael@0: TopSites.unpinSites([site]); michael@0: michael@0: yield waitForCondition(function(){ michael@0: return !grid.controller.isUpdating; michael@0: }); michael@0: michael@0: let secondTile = grid.items[1]; michael@0: ok( !secondTile.hasAttribute("pinned"), "2nd item should no longer be marked as pinned" ); michael@0: ok( !NewTabUtils.pinnedLinks.isPinned(site), "2nd item should no longer be pinned" ); michael@0: } michael@0: }); michael@0: michael@0: gTests.push({ michael@0: desc: "block/unblock sites", michael@0: setUp: function() { michael@0: yield TopSitesTestHelper.setup(); michael@0: // setup - set topsites to known state michael@0: yield TopSitesTestHelper.setLinks( michael@0: "brian,dougal,dylan,ermintrude,florence,moose,sgtsam,train,zebedee,zeebad,basic,coral", michael@0: ",dougal" michael@0: ); michael@0: // pause until the update has fired and the view is finishd updating michael@0: let arrangedPromise = waitForEvent(TopSitesTestHelper.grid, "arranged"); michael@0: yield TopSitesTestHelper.updatePagesAndWait(); michael@0: yield arrangedPromise; michael@0: }, michael@0: run: function() { michael@0: try { michael@0: // block a site michael@0: // test that sites are removed from the grid as expected michael@0: let grid = TopSitesTestHelper.grid, michael@0: items = grid.items; michael@0: is(items.length, 8, this.desc + ": should be 8 topsites"); michael@0: michael@0: let brianSite = TopSitesTestHelper.siteFromNode(items[0]); michael@0: let dougalSite = TopSitesTestHelper.siteFromNode(items[1]); michael@0: let dylanSite = TopSitesTestHelper.siteFromNode(items[2]); michael@0: michael@0: let arrangedPromise = waitForEvent(grid, "arranged"); michael@0: // we'll block brian (he's not pinned) michael@0: TopSites.hideSites([brianSite]); michael@0: // pause until the update has fired and the view is finished updating michael@0: yield arrangedPromise; michael@0: michael@0: // verify brian is blocked and removed from the grid michael@0: ok( (new Site(brianSite)).blocked, "Site has truthy blocked property" ); michael@0: ok( NewTabUtils.blockedLinks.isBlocked(brianSite), "Site was blocked" ); michael@0: is( grid.querySelectorAll("[value='"+brianSite.url+"']").length, 0, "Blocked site was removed from grid"); michael@0: michael@0: // make sure the empty slot was backfilled michael@0: is(items.length, 8, this.desc + ": should be 8 topsites"); michael@0: michael@0: arrangedPromise = waitForEvent(grid, "arranged"); michael@0: // block dougal,dylan. dougal is currently pinned at index 1 michael@0: TopSites.hideSites([dougalSite, dylanSite]); michael@0: // pause until the update has fired and the view is finished updating michael@0: yield arrangedPromise; michael@0: michael@0: // verify dougal is blocked and removed from the grid michael@0: ok( (new Site(dougalSite)).blocked, "Site has truthy blocked property" ); michael@0: ok( NewTabUtils.blockedLinks.isBlocked(dougalSite), "Site was blocked" ); michael@0: ok( !NewTabUtils.pinnedLinks.isPinned(dougalSite), "Blocked Site is no longer pinned" ); michael@0: is( grid.querySelectorAll("[value='"+dougalSite.url+"']").length, 0, "Blocked site was removed from grid"); michael@0: michael@0: // verify dylan is blocked and removed from the grid michael@0: ok( (new Site(dylanSite)).blocked, "Site has truthy blocked property" ); michael@0: ok( NewTabUtils.blockedLinks.isBlocked(dylanSite), "Site was blocked" ); michael@0: ok( !NewTabUtils.pinnedLinks.isPinned(dylanSite), "Blocked Site is no longer pinned" ); michael@0: is( grid.querySelectorAll("[value='"+dylanSite.url+"']").length, 0, "Blocked site was removed from grid"); michael@0: michael@0: // make sure the empty slots were backfilled michael@0: is(items.length, 8, this.desc + ": should be 8 topsites"); michael@0: michael@0: arrangedPromise = waitForEvent(grid, "arranged"); michael@0: TopSites.restoreSites([brianSite, dougalSite, dylanSite]); michael@0: // pause until the update has fired and the view is finished updating michael@0: yield arrangedPromise; michael@0: michael@0: // verify brian, dougal and dyland are unblocked and back in the grid michael@0: ok( !NewTabUtils.blockedLinks.isBlocked(brianSite), "site was unblocked" ); michael@0: is( grid.querySelectorAll("[value='"+brianSite.url+"']").length, 1, "Unblocked site is back in the grid"); michael@0: michael@0: ok( !NewTabUtils.blockedLinks.isBlocked(dougalSite), "site was unblocked" ); michael@0: is( grid.querySelectorAll("[value='"+dougalSite.url+"']").length, 1, "Unblocked site is back in the grid"); michael@0: // ..and that a previously pinned site is re-pinned after being blocked, then restored michael@0: ok( NewTabUtils.pinnedLinks.isPinned(dougalSite), "Restoring previously pinned site makes it pinned again" ); michael@0: is( grid.items[1].getAttribute("value"), dougalSite.url, "Blocked Site restored to pinned index" ); michael@0: michael@0: ok( !NewTabUtils.blockedLinks.isBlocked(dylanSite), "site was unblocked" ); michael@0: is( grid.querySelectorAll("[value='"+dylanSite.url+"']").length, 1, "Unblocked site is back in the grid"); michael@0: michael@0: } catch(ex) { michael@0: michael@0: ok(false, this.desc+": Caught exception in test: " + ex); michael@0: info("trace: " + ex.stack); michael@0: } michael@0: } michael@0: }); michael@0: michael@0: gTests.push({ michael@0: desc: "delete and restore site tiles", michael@0: pins: "brian", michael@0: setUp: function() { michael@0: yield TopSitesTestHelper.setup(); michael@0: // setup - set history to known state michael@0: yield TopSitesTestHelper.setLinks( michael@0: "brian,dougal,dylan,ermintrude", michael@0: this.pins michael@0: ); michael@0: // pause until the update has fired and the view is finishd updating michael@0: let arrangedPromise = waitForEvent(TopSitesTestHelper.grid, "arranged"); michael@0: yield TopSitesTestHelper.updatePagesAndWait(); michael@0: yield arrangedPromise; michael@0: }, michael@0: run: function() { michael@0: // delete a both pinned and unpinned sites michael@0: // test that sites are removed from the grid michael@0: let grid = TopSitesTestHelper.grid, michael@0: items = grid.items; michael@0: is(items.length, 4, this.desc + ": should be 4 topsites"); michael@0: michael@0: let brianTile = grid.querySelector('richgriditem[value$="brian"]'); michael@0: let dougalTile = grid.querySelector('richgriditem[value$="dougal"]') michael@0: michael@0: // verify assumptions before deleting sites michael@0: ok( brianTile, "Tile for Brian was created"); michael@0: ok( dougalTile, "Tile for Dougal was created"); michael@0: michael@0: let brianSite = TopSitesTestHelper.siteFromNode(brianTile); michael@0: let dougalSite = TopSitesTestHelper.siteFromNode(dougalTile); michael@0: ok( NewTabUtils.pinnedLinks.isPinned( brianSite ), "Brian tile is pinned" ); michael@0: michael@0: // select the 2 tiles michael@0: grid.toggleItemSelection(brianTile); michael@0: grid.toggleItemSelection(dougalTile); michael@0: is(grid.selectedItems.length, 2, "2 tiles were selected"); michael@0: michael@0: // pause until the update has fired and the view is finishd updating michael@0: let arrangedPromise = waitForEvent(grid, "arranged"); michael@0: michael@0: // raise a mock context-action event to trigger deletion of the selection michael@0: let event = document.createEvent("Events"); michael@0: event.action = "delete"; michael@0: event.initEvent("context-action", true, true); // is cancelable michael@0: grid.dispatchEvent(event); michael@0: michael@0: yield arrangedPromise; michael@0: michael@0: // those sites are blocked and their tiles have been removed from the grid? michael@0: ok( !grid.querySelector('richgriditem[value="'+brianSite.value+'"]')); michael@0: ok( !grid.querySelector('richgriditem[value="'+dougalSite.value+'"]')); michael@0: ok( NewTabUtils.blockedLinks.isBlocked(brianSite), "Brian site was blocked" ); michael@0: ok( NewTabUtils.blockedLinks.isBlocked(dougalSite), "Dougal site was blocked" ); michael@0: // with the tiles deleted, selection should be empty michael@0: is( grid.selectedItems.length, 0, "Gris selection is empty after deletion" ); michael@0: michael@0: // raise a mock context-action event to trigger restore michael@0: arrangedPromise = waitForEvent(grid, "arranged"); michael@0: event = document.createEvent("Events"); michael@0: event.action = "restore"; michael@0: event.initEvent("context-action", true, true); // is cancelable michael@0: grid.dispatchEvent(event); michael@0: michael@0: yield arrangedPromise; michael@0: brianTile = grid.querySelector('richgriditem[value$="brian"]'); michael@0: dougalTile = grid.querySelector('richgriditem[value$="dougal"]'); michael@0: michael@0: // those tiles have been restored to the grid? michael@0: ok( brianTile, "First tile was restored to the grid" ); michael@0: ok( dougalTile, "2nd tile was restored to the grid" ); michael@0: michael@0: is(grid.selectedItems.length, 2, "2 tiles are still selected"); michael@0: is( grid.selectedItems[0], brianTile, "Brian is still selected" ); michael@0: is( grid.selectedItems[1], dougalTile, "Dougal is still selected" ); michael@0: ok( NewTabUtils.pinnedLinks.isPinned( brianSite ), "Brian tile is still pinned" ); michael@0: ok( !NewTabUtils.blockedLinks.isBlocked(brianSite), "Brian site was unblocked" ); michael@0: ok( !NewTabUtils.blockedLinks.isBlocked(dougalSite), "Dougal site was unblocked" ); michael@0: michael@0: } michael@0: });