/* Any copyright is dedicated to the Public Domain. http://creativecommons.org/publicdomain/zero/1.0/ */ const PREF_NEWTAB_ENABLED = "browser.newtabpage.enabled"; Services.prefs.setBoolPref(PREF_NEWTAB_ENABLED, true); // Opens and closes a new tab to clear any existing preloaded ones. This is // necessary to prevent any left-over activity-stream preloaded new tabs from // affecting these tests. BrowserOpenTab(); const initialTab = gBrowser.selectedTab; gBrowser.removeTab(initialTab); ChromeUtils.import("resource://gre/modules/XPCOMUtils.jsm"); XPCOMUtils.defineLazyModuleGetters(this, { NewTabUtils: "resource://gre/modules/NewTabUtils.jsm", PlacesTestUtils: "resource://testing-common/PlacesTestUtils.jsm", Sanitizer: "resource:///modules/Sanitizer.jsm", }); 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 add_task(async function setupWindowSize() { let [oldSize, curWidth, curHeight] = await ContentTask.spawn(gBrowser.selectedBrowser, requiredSize, (requiredSizeArg) => { var oldSizeVar = {}; Object.keys(requiredSizeArg).forEach(prop => { info([prop, content[prop], requiredSizeArg[prop]]); if (content[prop] < requiredSizeArg[prop]) { oldSizeVar[prop] = content[prop]; info("Changing browser " + prop + " from " + oldSizeVar[prop] + " to " + requiredSizeArg[prop]); content[prop] = requiredSizeArg[prop]; } }); return [oldSizeVar, content.outerWidth, content.outerHeight]; }); 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 < curHeight) { info("Warning: Browser outer height is now " + curHeight + ", which is larger than the " + "available screen height, " + screenHeight + ". That may cause problems."); } if (screenWidth < curWidth) { info("Warning: Browser outer width is now " + curWidth + ", 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]); ContentTask.spawn(gBrowser.selectedBrowser, oldSize, (oldSizeArg) => { Object.keys(oldSizeArg).forEach(prop => { if (oldSizeArg[prop]) { content[prop] = oldSizeArg[prop]; } }); }); }); }); registerCleanupFunction(function() { Services.prefs.clearUserPref(PREF_NEWTAB_ENABLED); }); function pushPrefs(...aPrefs) { return SpecialPowers.pushPrefEnv({"set": aPrefs}); } add_task(async function setup() { registerCleanupFunction(function() { return new Promise(resolve => { function cleanupAndFinish() { PlacesUtils.history.clear().then(() => { whenPagesUpdated().then(resolve); NewTabUtils.restore(); }); } let callbacks = NewTabUtils.links._populateCallbacks; let numCallbacks = callbacks.length; if (numCallbacks) callbacks.splice(0, numCallbacks, cleanupAndFinish); else cleanupAndFinish(); }); }); await whenPagesUpdated(); }); /** 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() }, async function(args) { let cell = content.gGrid.cells[args.index]; // eslint-disable-next-line no-eval 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() { PlacesUtils.history.clear().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() {}, handleCompletion() { 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; }); } Services.prefs.setStringPref("browser.newtabpage.pinned", JSON.stringify(links)); 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'. */ async function addNewTabPageTab() { let tab = await BrowserTestUtils.openNewForegroundTab(gWindow.gBrowser, "about:newtab", false); let browser = tab.linkedBrowser; // Wait for the document to become visible in case it was preloaded. await waitForCondition(() => !browser.contentDocument.hidden); await 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,,4p") * 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/'. */ async function checkGrid(pattern) { let length = pattern.split(",").length; await ContentTask.spawn(gWindow.gBrowser.selectedBrowser, { length, pattern }, async 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. */ async function simulateExternalDrop(aDestIndex) { let pagesUpdatedPromise = whenPagesUpdated(); await ContentTask.spawn(gWindow.gBrowser.selectedBrowser, aDestIndex, async function(dropIndex) { return new Promise(resolve => { const url = "data:text/html;charset=utf-8," + "link"; 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() { content.setTimeout(iframeLoaded, 0); }, {once: true}); 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); }); }); await 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, {}, async 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(); } }; 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, async function(contentTheme) { 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-" + contentTheme).click(); observer.disconnect(); if (opened == panel.hasAttribute("open")) { resolve(); } }); }); observer.observe(panel, options); }); } let opened = panelOpened(true); customizeButton.click(); await opened; let closed = panelOpened(false); customizeButton.click(); await closed; }); } /** * Reports presence of a scrollbar */ function hasScrollbar() { return ContentTask.spawn(gWindow.gBrowser.selectedBrowser, {}, async function() { let docElement = content.document.documentElement; return docElement.scrollHeight > docElement.clientHeight; }); }