diff options
Diffstat (limited to 'browser/base/content/test/newtab/head.js')
-rw-r--r-- | browser/base/content/test/newtab/head.js | 552 |
1 files changed, 552 insertions, 0 deletions
diff --git a/browser/base/content/test/newtab/head.js b/browser/base/content/test/newtab/head.js new file mode 100644 index 000000000..d702103a0 --- /dev/null +++ b/browser/base/content/test/newtab/head.js @@ -0,0 +1,552 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +const PREF_NEWTAB_ENABLED = "browser.newtabpage.enabled"; +const PREF_NEWTAB_DIRECTORYSOURCE = "browser.newtabpage.directory.source"; + +Services.prefs.setBoolPref(PREF_NEWTAB_ENABLED, true); + +var tmp = {}; +Cu.import("resource://gre/modules/NewTabUtils.jsm", tmp); +Cu.import("resource:///modules/DirectoryLinksProvider.jsm", tmp); +Cu.import("resource://testing-common/PlacesTestUtils.jsm", tmp); +Cc["@mozilla.org/moz/jssubscript-loader;1"] + .getService(Ci.mozIJSSubScriptLoader) + .loadSubScript("chrome://browser/content/sanitize.js", tmp); +var {NewTabUtils, Sanitizer, DirectoryLinksProvider, PlacesTestUtils} = tmp; + +var gWindow = window; + +// Default to dummy/empty directory links +var gDirectorySource = 'data:application/json,{"test":1}'; +var gOrigDirectorySource; + +// The tests assume all 3 rows and all 3 columns of sites are shown, but the +// window may be too small to actually show everything. Resize it if necessary. +var requiredSize = {}; +requiredSize.innerHeight = + 40 + 32 + // undo container + bottom margin + 44 + 32 + // search bar + bottom margin + (3 * (180 + 32)) + // 3 rows * (tile height + title and bottom margin) + 100; // breathing room +requiredSize.innerWidth = + (3 * (290 + 20)) + // 3 cols * (tile width + side margins) + 100; // breathing room + +var oldSize = {}; +Object.keys(requiredSize).forEach(prop => { + info([prop, gBrowser.contentWindow[prop], requiredSize[prop]]); + if (gBrowser.contentWindow[prop] < requiredSize[prop]) { + oldSize[prop] = gBrowser.contentWindow[prop]; + info("Changing browser " + prop + " from " + oldSize[prop] + " to " + + requiredSize[prop]); + gBrowser.contentWindow[prop] = requiredSize[prop]; + } +}); + +var screenHeight = {}; +var screenWidth = {}; +Cc["@mozilla.org/gfx/screenmanager;1"]. + getService(Ci.nsIScreenManager). + primaryScreen. + GetAvailRectDisplayPix({}, {}, screenWidth, screenHeight); +screenHeight = screenHeight.value; +screenWidth = screenWidth.value; + +if (screenHeight < gBrowser.contentWindow.outerHeight) { + info("Warning: Browser outer height is now " + + gBrowser.contentWindow.outerHeight + ", which is larger than the " + + "available screen height, " + screenHeight + + ". That may cause problems."); +} + +if (screenWidth < gBrowser.contentWindow.outerWidth) { + info("Warning: Browser outer width is now " + + gBrowser.contentWindow.outerWidth + ", which is larger than the " + + "available screen width, " + screenWidth + + ". That may cause problems."); +} + +registerCleanupFunction(function () { + while (gWindow.gBrowser.tabs.length > 1) + gWindow.gBrowser.removeTab(gWindow.gBrowser.tabs[1]); + + Object.keys(oldSize).forEach(prop => { + if (oldSize[prop]) { + gBrowser.contentWindow[prop] = oldSize[prop]; + } + }); + + // Stop any update timers to prevent unexpected updates in later tests + let timer = NewTabUtils.allPages._scheduleUpdateTimeout; + if (timer) { + clearTimeout(timer); + delete NewTabUtils.allPages._scheduleUpdateTimeout; + } + + Services.prefs.clearUserPref(PREF_NEWTAB_ENABLED); + Services.prefs.setCharPref(PREF_NEWTAB_DIRECTORYSOURCE, gOrigDirectorySource); + + return watchLinksChangeOnce(); +}); + +function pushPrefs(...aPrefs) { + return new Promise(resolve => + SpecialPowers.pushPrefEnv({"set": aPrefs}, resolve)); +} + +/** + * Resolves promise when directory links are downloaded and written to disk + */ +function watchLinksChangeOnce() { + return new Promise(resolve => { + let observer = { + onManyLinksChanged: () => { + DirectoryLinksProvider.removeObserver(observer); + resolve(); + } + }; + observer.onDownloadFail = observer.onManyLinksChanged; + DirectoryLinksProvider.addObserver(observer); + }); +} + +add_task(function* setup() { + registerCleanupFunction(function() { + return new Promise(resolve => { + function cleanupAndFinish() { + PlacesTestUtils.clearHistory().then(() => { + whenPagesUpdated().then(resolve); + NewTabUtils.restore(); + }); + } + + let callbacks = NewTabUtils.links._populateCallbacks; + let numCallbacks = callbacks.length; + + if (numCallbacks) + callbacks.splice(0, numCallbacks, cleanupAndFinish); + else + cleanupAndFinish(); + }); + }); + + let promiseReady = Task.spawn(function*() { + yield watchLinksChangeOnce(); + yield whenPagesUpdated(); + }); + + // Save the original directory source (which is set globally for tests) + gOrigDirectorySource = Services.prefs.getCharPref(PREF_NEWTAB_DIRECTORYSOURCE); + Services.prefs.setCharPref(PREF_NEWTAB_DIRECTORYSOURCE, gDirectorySource); + yield promiseReady; +}); + +/** Perform an action on a cell within the newtab page. + * @param aIndex index of cell + * @param aFn function to call in child process or tab. + * @returns result of calling the function. + */ +function performOnCell(aIndex, aFn) { + return ContentTask.spawn(gWindow.gBrowser.selectedBrowser, + { index: aIndex, fn: aFn.toString() }, function* (args) { + let cell = content.gGrid.cells[args.index]; + return eval(args.fn)(cell); + }); +} + +/** + * Allows to provide a list of links that is used to construct the grid. + * @param aLinksPattern the pattern (see below) + * + * Example: setLinks("-1,0,1,2,3") + * Result: [{url: "http://example.com/", title: "site#-1"}, + * {url: "http://example0.com/", title: "site#0"}, + * {url: "http://example1.com/", title: "site#1"}, + * {url: "http://example2.com/", title: "site#2"}, + * {url: "http://example3.com/", title: "site#3"}] + */ +function setLinks(aLinks) { + return new Promise(resolve => { + let links = aLinks; + + if (typeof links == "string") { + links = aLinks.split(/\s*,\s*/).map(function (id) { + return {url: "http://example" + (id != "-1" ? id : "") + ".com/", + title: "site#" + id}; + }); + } + + // Call populateCache() once to make sure that all link fetching that is + // currently in progress has ended. We clear the history, fill it with the + // given entries and call populateCache() now again to make sure the cache + // has the desired contents. + NewTabUtils.links.populateCache(function () { + PlacesTestUtils.clearHistory().then(() => { + fillHistory(links).then(() => { + NewTabUtils.links.populateCache(function () { + NewTabUtils.allPages.update(); + resolve(); + }, true); + }); + }); + }); + }); +} + +function fillHistory(aLinks) { + return new Promise(resolve => { + let numLinks = aLinks.length; + if (!numLinks) { + executeSoon(resolve); + return; + } + + let transitionLink = Ci.nsINavHistoryService.TRANSITION_LINK; + + // Important: To avoid test failures due to clock jitter on Windows XP, call + // Date.now() once here, not each time through the loop. + let now = Date.now() * 1000; + + for (let i = 0; i < aLinks.length; i++) { + let link = aLinks[i]; + let place = { + uri: makeURI(link.url), + title: link.title, + // Links are secondarily sorted by visit date descending, so decrease the + // visit date as we progress through the array so that links appear in the + // grid in the order they're present in the array. + visits: [{visitDate: now - i, transitionType: transitionLink}] + }; + + PlacesUtils.asyncHistory.updatePlaces(place, { + handleError: () => ok(false, "couldn't add visit to history"), + handleResult: function () {}, + handleCompletion: function () { + if (--numLinks == 0) { + resolve(); + } + } + }); + } + }); +} + +/** + * Allows to specify the list of pinned links (that have a fixed position in + * the grid. + * @param aLinksPattern the pattern (see below) + * + * Example: setPinnedLinks("3,,1") + * Result: 'http://example3.com/' is pinned in the first cell. 'http://example1.com/' is + * pinned in the third cell. + */ +function setPinnedLinks(aLinks) { + let links = aLinks; + + if (typeof links == "string") { + links = aLinks.split(/\s*,\s*/).map(function (id) { + if (id) + return {url: "http://example" + (id != "-1" ? id : "") + ".com/", + title: "site#" + id, + type: "history"}; + return undefined; + }); + } + + let string = Cc["@mozilla.org/supports-string;1"] + .createInstance(Ci.nsISupportsString); + string.data = JSON.stringify(links); + Services.prefs.setComplexValue("browser.newtabpage.pinned", + Ci.nsISupportsString, string); + + NewTabUtils.pinnedLinks.resetCache(); + NewTabUtils.allPages.update(); +} + +/** + * Restore the grid state. + */ +function restore() { + return new Promise(resolve => { + whenPagesUpdated().then(resolve); + NewTabUtils.restore(); + }); +} + +/** + * Wait until a given condition becomes true. + */ +function waitForCondition(aConditionFn, aMaxTries=50, aCheckInterval=100) { + return new Promise((resolve, reject) => { + let tries = 0; + + function tryNow() { + tries++; + + if (aConditionFn()) { + resolve(); + } else if (tries < aMaxTries) { + tryAgain(); + } else { + reject("Condition timed out: " + aConditionFn.toSource()); + } + } + + function tryAgain() { + setTimeout(tryNow, aCheckInterval); + } + + tryAgain(); + }); +} + +/** + * Creates a new tab containing 'about:newtab'. + */ +function* addNewTabPageTab() { + let tab = yield BrowserTestUtils.openNewForegroundTab(gWindow.gBrowser, "about:newtab", false); + let browser = tab.linkedBrowser; + + // Wait for the document to become visible in case it was preloaded. + yield waitForCondition(() => !browser.contentDocument.hidden) + + yield new Promise(resolve => { + if (NewTabUtils.allPages.enabled) { + // Continue when the link cache has been populated. + NewTabUtils.links.populateCache(function () { + whenSearchInitDone().then(resolve); + }); + } else { + resolve(); + } + }); + + return tab; +} + +/** + * Compares the current grid arrangement with the given pattern. + * @param the pattern (see below) + * + * Example: checkGrid("3p,2,,1p") + * Result: We expect the first cell to contain the pinned site 'http://example3.com/'. + * The second cell contains 'http://example2.com/'. The third cell is empty. + * The fourth cell contains the pinned site 'http://example4.com/'. + */ +function* checkGrid(pattern) { + let length = pattern.split(",").length; + + yield ContentTask.spawn(gWindow.gBrowser.selectedBrowser, + { length, pattern }, function* (args) { + let grid = content.wrappedJSObject.gGrid; + + let sites = grid.sites.slice(0, args.length); + let foundPattern = sites.map(function (aSite) { + if (!aSite) + return ""; + + let pinned = aSite.isPinned(); + let hasPinnedAttr = aSite.node.hasAttribute("pinned"); + + if (pinned != hasPinnedAttr) + ok(false, "invalid state (site.isPinned() != site[pinned])"); + + return aSite.url.replace(/^http:\/\/example(\d+)\.com\/$/, "$1") + (pinned ? "p" : ""); + }); + + Assert.equal(foundPattern, args.pattern, "grid status = " + args.pattern); + }); +} + +/** + * Blocks a site from the grid. + * @param aIndex The cell index. + */ +function blockCell(aIndex) { + return new Promise(resolve => { + whenPagesUpdated().then(resolve); + performOnCell(aIndex, cell => { + return cell.site.block(); + }); + }); +} + +/** + * Pins a site on a given position. + * @param aIndex The cell index. + * @param aPinIndex The index the defines where the site should be pinned. + */ +function pinCell(aIndex) { + performOnCell(aIndex, cell => { + cell.site.pin(); + }); +} + +/** + * Unpins the given cell's site. + * @param aIndex The cell index. + */ +function unpinCell(aIndex) { + return new Promise(resolve => { + whenPagesUpdated().then(resolve); + performOnCell(aIndex, cell => { + cell.site.unpin(); + }); + }); +} + +/** + * Simulates a drag and drop operation. Instead of rearranging a site that is + * is already contained in the newtab grid, this is used to simulate dragging + * an external link onto the grid e.g. the text from the URL bar. + * @param aDestIndex The cell index of the drop target. + */ +function* simulateExternalDrop(aDestIndex) { + let pagesUpdatedPromise = whenPagesUpdated(); + + yield ContentTask.spawn(gWindow.gBrowser.selectedBrowser, aDestIndex, function*(dropIndex) { + return new Promise(resolve => { + const url = "data:text/html;charset=utf-8," + + "<a id='link' href='http://example99.com/'>link</a>"; + + let doc = content.document; + let iframe = doc.createElement("iframe"); + + function iframeLoaded() { + let dataTransfer = new iframe.contentWindow.DataTransfer("dragstart", false); + dataTransfer.mozSetDataAt("text/x-moz-url", "http://example99.com/", 0); + + let event = content.document.createEvent("DragEvent"); + event.initDragEvent("drop", true, true, content, 0, 0, 0, 0, 0, + false, false, false, false, 0, null, dataTransfer); + + let target = content.gGrid.cells[dropIndex].node; + target.dispatchEvent(event); + + iframe.remove(); + + resolve(); + } + + iframe.addEventListener("load", function onLoad() { + iframe.removeEventListener("load", onLoad); + content.setTimeout(iframeLoaded, 0); + }); + + iframe.setAttribute("src", url); + iframe.style.width = "50px"; + iframe.style.height = "50px"; + iframe.style.position = "absolute"; + iframe.style.zIndex = 50; + + // the frame has to be attached to a visible element + let margin = doc.getElementById("newtab-search-container"); + margin.appendChild(iframe); + }); + }); + + yield pagesUpdatedPromise; +} + +/** + * Resumes testing when all pages have been updated. + */ +function whenPagesUpdated() { + return new Promise(resolve => { + let page = { + observe: _ => _, + + update() { + NewTabUtils.allPages.unregister(this); + executeSoon(resolve); + } + }; + + NewTabUtils.allPages.register(page); + registerCleanupFunction(function () { + NewTabUtils.allPages.unregister(page); + }); + }); +} + +/** + * Waits for the response to the page's initial search state request. + */ +function whenSearchInitDone() { + return ContentTask.spawn(gWindow.gBrowser.selectedBrowser, {}, function*() { + return new Promise(resolve => { + if (content.gSearch) { + let searchController = content.gSearch._contentSearchController; + if (searchController.defaultEngine) { + resolve(); + return; + } + } + + let eventName = "ContentSearchService"; + content.addEventListener(eventName, function onEvent(event) { + if (event.detail.type == "State") { + content.removeEventListener(eventName, onEvent); + let resolver = function() { + // Wait for the search controller to receive the event, then resolve. + if (content.gSearch._contentSearchController.defaultEngine) { + resolve(); + return; + } + } + content.setTimeout(resolver, 0); + } + }); + }); + }); +} + +/** + * Changes the newtab customization option and waits for the panel to open and close + * + * @param {string} aTheme + * Can be any of("blank"|"classic"|"enhanced") + */ +function customizeNewTabPage(aTheme) { + return ContentTask.spawn(gWindow.gBrowser.selectedBrowser, aTheme, function*(aTheme) { + + let document = content.document; + let panel = document.getElementById("newtab-customize-panel"); + let customizeButton = document.getElementById("newtab-customize-button"); + + function panelOpened(opened) { + return new Promise( (resolve) => { + let options = {attributes: true, oldValue: true}; + let observer = new content.MutationObserver(function(mutations) { + mutations.forEach(function(mutation) { + document.getElementById("newtab-customize-" + aTheme).click(); + observer.disconnect(); + if (opened == panel.hasAttribute("open")) { + resolve(); + } + }); + }); + observer.observe(panel, options); + }); + } + + let opened = panelOpened(true); + customizeButton.click(); + yield opened; + + let closed = panelOpened(false); + customizeButton.click(); + yield closed; + }); +} + +/** + * Reports presence of a scrollbar + */ +function hasScrollbar() { + return ContentTask.spawn(gWindow.gBrowser.selectedBrowser, {}, function* () { + let docElement = content.document.documentElement; + return docElement.scrollHeight > docElement.clientHeight; + }); +} |