if (!window.opener && window.arguments) { window.opener = window.arguments[0]; } /** * Import common SimpleTest methods so that they're usable in this window. */ var imports = [ "SimpleTest", "is", "isnot", "ok", "onerror", "todo", "todo_is", "todo_isnot", ]; for (var name of imports) { window[name] = window.opener.wrappedJSObject[name]; } const { BrowserTestUtils } = ChromeUtils.import( "resource://testing-common/BrowserTestUtils.jsm" ); var { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm"); // Some functions assume chrome-harness.js has been loaded. /* import-globals-from ../../../testing/mochitest/chrome-harness.js */ /** * Define global constants and variables. */ const NAV_NONE = 0; const NAV_BACK = 1; const NAV_FORWARD = 2; const NAV_URI = 3; const NAV_RELOAD = 4; var gExpectedEvents; // an array of events which are expected to // be triggered by this navigation var gUnexpectedEvents; // an array of event names which are NOT expected // to be triggered by this navigation var gFinalEvent; // true if the last expected event has fired var gUrisNotInBFCache = []; // an array of uri's which shouldn't be stored // in the bfcache var gNavType = NAV_NONE; // defines the most recent navigation type // executed by doPageNavigation var gOrigMaxTotalViewers = undefined; // original value of max_total_viewers, // to be restored at end of test var gExtractedPath = null; // used to cache file path for extracting files from a .jar file /** * The doPageNavigation() function performs page navigations asynchronously, * listens for specified events, and compares actual events with a list of * expected events. When all expected events have occurred, an optional * callback can be notified. The parameter passed to this function is an * object with the following properties: * * uri: if !undefined, the browser will navigate to this uri * * back: if true, the browser will execute goBack() * * forward: if true, the browser will execute goForward() * * reload: if true, the browser will execute reload() * * eventsToListenFor: an array containing one or more of the following event * types to listen for: "pageshow", "pagehide", "onload", * "onunload". If this property is undefined, only a * single "pageshow" events will be listened for. If this * property is explicitly empty, [], then no events will * be listened for. * * expectedEvents: an array of one or more expectedEvent objects, * corresponding to the events which are expected to be * fired for this navigation. Each object has the * following properties: * * type: one of the event type strings * title (optional): the title of the window the * event belongs to * persisted (optional): the event's expected * .persisted attribute * * This function will verify that events with the * specified properties are fired in the same order as * specified in the array. If .title or .persisted * properties for an expectedEvent are undefined, those * properties will not be verified for that particular * event. * * This property is ignored if eventsToListenFor is * undefined or []. * * preventBFCache: if true, an unload handler will be added to the loaded * page to prevent it from being bfcached. This property * has no effect when eventsToListenFor is []. * * onNavComplete: a callback which is notified after all expected events * have occurred, or after a timeout has elapsed. This * callback is not notified if eventsToListenFor is []. * * There must be an expectedEvent object for each event of the types in * eventsToListenFor which is triggered by this navigation. For example, if * eventsToListenFor = [ "pagehide", "pageshow" ], then expectedEvents * must contain an object for each pagehide and pageshow event which occurs as * a result of this navigation. */ // eslint-disable-next-line complexity function doPageNavigation(params) { // Parse the parameters. let back = params.back ? params.back : false; let forward = params.forward ? params.forward : false; let reload = params.reload ? params.reload : false; let uri = params.uri ? params.uri : false; let eventsToListenFor = typeof params.eventsToListenFor != "undefined" ? params.eventsToListenFor : ["pageshow"]; gExpectedEvents = typeof params.eventsToListenFor == "undefined" || eventsToListenFor.length == 0 ? undefined : params.expectedEvents; gUnexpectedEvents = typeof params.eventsToListenFor == "undefined" || eventsToListenFor.length == 0 ? undefined : params.unexpectedEvents; let preventBFCache = typeof [params.preventBFCache] == "undefined" ? false : params.preventBFCache; let waitOnly = typeof params.waitForEventsOnly == "boolean" && params.waitForEventsOnly; // Do some sanity checking on arguments. if (back && forward) { throw new Error("Can't specify both back and forward"); } if (back && uri) { throw new Error("Can't specify both back and a uri"); } if (forward && uri) { throw new Error("Can't specify both forward and a uri"); } if (reload && (forward || back || uri)) { throw new Error("Can't specify reload and another navigation type"); } if (!back && !forward && !uri && !reload && !waitOnly) { throw new Error("Must specify back or foward or reload or uri"); } if (params.onNavComplete && eventsToListenFor.length == 0) { throw new Error("Can't use onNavComplete when eventsToListenFor == []"); } if (params.preventBFCache && eventsToListenFor.length == 0) { throw new Error("Can't use preventBFCache when eventsToListenFor == []"); } if (params.preventBFCache && waitOnly) { throw new Error("Can't prevent bfcaching when only waiting for events"); } if (waitOnly && typeof params.onNavComplete == "undefined") { throw new Error( "Must specify onNavComplete when specifying waitForEventsOnly" ); } if (waitOnly && (back || forward || reload || uri)) { throw new Error( "Can't specify a navigation type when using waitForEventsOnly" ); } for (let anEventType of eventsToListenFor) { let eventFound = false; if (anEventType == "pageshow" && !gExpectedEvents) { eventFound = true; } if (gExpectedEvents) { for (let anExpectedEvent of gExpectedEvents) { if (anExpectedEvent.type == anEventType) { eventFound = true; } } } if (gUnexpectedEvents) { for (let anExpectedEventType of gUnexpectedEvents) { if (anExpectedEventType == anEventType) { eventFound = true; } } } if (!eventFound) { throw new Error( `Event type ${anEventType} is specified in ` + "eventsToListenFor, but not in expectedEvents" ); } } // If the test explicitly sets .eventsToListenFor to [], don't wait for any // events. gFinalEvent = eventsToListenFor.length == 0; // Add an event listener for each type of event in the .eventsToListenFor // property of the input parameters. for (let eventType of eventsToListenFor) { dump("TEST: registering a listener for " + eventType + " events\n"); TestWindow.getBrowser().addEventListener( eventType, pageEventListener, true ); } // Perform the specified navigation. if (back) { gNavType = NAV_BACK; TestWindow.getBrowser().goBack(); } else if (forward) { gNavType = NAV_FORWARD; TestWindow.getBrowser().goForward(); } else if (uri) { gNavType = NAV_URI; BrowserTestUtils.loadURI(TestWindow.getBrowser(), uri); } else if (reload) { gNavType = NAV_RELOAD; TestWindow.getBrowser().reload(); } else if (waitOnly) { gNavType = NAV_NONE; } else { throw new Error("No valid navigation type passed to doPageNavigation!"); } // If we're listening for events and there is an .onNavComplete callback, // wait for all events to occur, and then call doPageNavigation_complete(). if (eventsToListenFor.length > 0 && params.onNavComplete) { waitForTrue( function() { return gFinalEvent; }, function() { doPageNavigation_complete( eventsToListenFor, params.onNavComplete, preventBFCache ); } ); } } /** * Finish doPageNavigation(), by removing event listeners, adding an unload * handler if appropriate, and calling the onNavComplete callback. This * function is called after all the expected events for this navigation have * occurred. */ function doPageNavigation_complete( eventsToListenFor, onNavComplete, preventBFCache ) { // Unregister our event listeners. dump("TEST: removing event listeners\n"); for (let eventType of eventsToListenFor) { TestWindow.getBrowser().removeEventListener( eventType, pageEventListener, true ); } // If the .preventBFCache property was set, add an empty unload handler to // prevent the page from being bfcached. let uri = TestWindow.getBrowser().currentURI.spec; if (preventBFCache) { TestWindow.getWindow().addEventListener( "unload", function() { dump( "TEST: Called dummy unload function to prevent page from " + "being bfcached.\n" ); }, true ); // Save the current uri in an array of uri's which shouldn't be // stored in the bfcache, for later verification. if (!(uri in gUrisNotInBFCache)) { gUrisNotInBFCache.push(uri); } } else if (gNavType == NAV_URI) { // If we're navigating to a uri and .preventBFCache was not // specified, splice it out of gUrisNotInBFCache if it's there. gUrisNotInBFCache.forEach(function(element, index, array) { if (element == uri) { array.splice(index, 1); } }, this); } // Notify the callback now that we're done. onNavComplete.call(); } /** * Allows a test to wait for page navigation events, and notify a * callback when they've all been received. This works exactly the * same as doPageNavigation(), except that no navigation is initiated. */ function waitForPageEvents(params) { params.waitForEventsOnly = true; doPageNavigation(params); } /** * The event listener which listens for expectedEvents. */ function pageEventListener(event) { try { dump( "TEST: eventListener received a " + event.type + " event for page " + event.originalTarget.title + ", persisted=" + event.persisted + "\n" ); } catch (e) { // Ignore any exception. } // If this page shouldn't be in the bfcache because it was previously // loaded with .preventBFCache, make sure that its pageshow event // has .persisted = false, even if the test doesn't explicitly test // for .persisted. if ( event.type == "pageshow" && (gNavType == NAV_BACK || gNavType == NAV_FORWARD) ) { let uri = TestWindow.getBrowser().currentURI.spec; if (uri in gUrisNotInBFCache) { ok( !event.persisted, "pageshow event has .persisted = false, even " + "though it was loaded with .preventBFCache previously\n" ); } } if (typeof gUnexpectedEvents != "undefined") { is( gUnexpectedEvents.indexOf(event.type), -1, "Should not get unexpected event " + event.type ); } // If no expected events were specified, mark the final event as having been // triggered when a pageshow event is fired; this will allow // doPageNavigation() to return. if (typeof gExpectedEvents == "undefined" && event.type == "pageshow") { waitForNextPaint(function() { gFinalEvent = true; }); return; } // If there are explicitly no expected events, but we receive one, it's an // error. if (gExpectedEvents.length == 0) { ok(false, "Unexpected event (" + event.type + ") occurred"); return; } // Grab the next expected event, and compare its attributes against the // actual event. let expected = gExpectedEvents.shift(); is( event.type, expected.type, "A " + expected.type + " event was expected, but a " + event.type + " event occurred" ); if (typeof expected.title != "undefined") { ok( event.originalTarget instanceof HTMLDocument, "originalTarget for last " + event.type + " event not an HTMLDocument" ); is( event.originalTarget.title, expected.title, "A " + event.type + " event was expected for page " + expected.title + ", but was fired for page " + event.originalTarget.title ); } if (typeof expected.persisted != "undefined") { is( event.persisted, expected.persisted, "The persisted property of the " + event.type + " event on page " + event.originalTarget.location + " had an unexpected value" ); } if ("visibilityState" in expected) { is( event.originalTarget.visibilityState, expected.visibilityState, "The visibilityState property of the document on page " + event.originalTarget.location + " had an unexpected value" ); } if ("hidden" in expected) { is( event.originalTarget.hidden, expected.hidden, "The hidden property of the document on page " + event.originalTarget.location + " had an unexpected value" ); } // If we're out of expected events, let doPageNavigation() return. if (gExpectedEvents.length == 0) { waitForNextPaint(function() { gFinalEvent = true; }); } } /** * End a test. */ function finish() { // Work around bug 467960. let history; if (SpecialPowers.Services.appinfo.sessionHistoryInParent) { history = TestWindow.getBrowser().browsingContext?.sessionHistory; } else { history = TestWindow.getBrowser().webNavigation.sessionHistory .legacySHistory; } if (history) { history.purgeHistory(history.count); } // If the test changed the value of max_total_viewers via a call to // enableBFCache(), then restore it now. if (typeof gOrigMaxTotalViewers != "undefined") { Services.prefs.setIntPref( "browser.sessionhistory.max_total_viewers", gOrigMaxTotalViewers ); } // Close the test window and signal the framework that the test is done. let opener = window.opener; let SimpleTest = opener.wrappedJSObject.SimpleTest; // Wait for the window to be closed before finishing the test Services.ww.registerNotification(function observer(subject, topic, data) { if (topic == "domwindowclosed") { Services.ww.unregisterNotification(observer); SimpleTest.waitForFocus(SimpleTest.finish, opener); } }); window.close(); } /** * Helper function which waits until another function returns true, or until a * timeout occurs, and then notifies a callback. * * Parameters: * * fn: a function which is evaluated repeatedly, and when it turns true, * the onWaitComplete callback is notified. * * onWaitComplete: a callback which will be notified when fn() returns * true, or when a timeout occurs. * * timeout: a timeout, in seconds or ms, after which waitForTrue() will * fail an assertion and then return, even if the fn function never * returns true. If timeout is undefined, waitForTrue() will never * time out. */ function waitForTrue(fn, onWaitComplete, timeout) { var start = new Date().valueOf(); if (typeof timeout != "undefined") { // If timeoutWait is less than 500, assume it represents seconds, and // convert to ms. if (timeout < 500) { timeout *= 1000; } } // Loop until the test function returns true, or until a timeout occurs, // if a timeout is defined. var intervalid; intervalid = setInterval(function() { var timeoutHit = false; if (typeof timeout != "undefined") { timeoutHit = new Date().valueOf() - start >= timeout; if (timeoutHit) { ok(false, "Timed out waiting for condition"); } } if (timeoutHit || fn.call()) { // Stop calling the test function and notify the callback. clearInterval(intervalid); onWaitComplete.call(); } }, 20); } function waitForNextPaint(cb) { requestAnimationFrame(_ => requestAnimationFrame(cb)); } /** * Enable or disable the bfcache. * * Parameters: * * enable: if true, set max_total_viewers to -1 (the default); if false, set * to 0 (disabled), if a number, set it to that specific number */ function enableBFCache(enable) { // If this is the first time the test called enableBFCache(), // store the original value of max_total_viewers, so it can // be restored at the end of the test. if (typeof gOrigMaxTotalViewers == "undefined") { gOrigMaxTotalViewers = Services.prefs.getIntPref( "browser.sessionhistory.max_total_viewers" ); } if (typeof enable == "boolean") { if (enable) { Services.prefs.setIntPref("browser.sessionhistory.max_total_viewers", -1); } else { Services.prefs.setIntPref("browser.sessionhistory.max_total_viewers", 0); } } else if (typeof enable == "number") { Services.prefs.setIntPref( "browser.sessionhistory.max_total_viewers", enable ); } } /* * get http root for local tests. Use a single extractJarToTmp instead of * extracting for each test. * Returns a file://path if we have a .jar file */ function getHttpRoot() { var location = window.location.href; location = getRootDirectory(location); var jar = getJar(location); if (jar != null) { if (gExtractedPath == null) { var resolved = extractJarToTmp(jar); gExtractedPath = resolved.path; } } else { return null; } return "file://" + gExtractedPath + "/"; } /** * Returns the full HTTP url for a file in the mochitest docshell test * directory. */ function getHttpUrl(filename) { var root = getHttpRoot(); if (root == null) { root = "http://mochi.test:8888/chrome/docshell/test/chrome/"; } return root + filename; } /** * A convenience object with methods that return the current test window, * browser, and document. */ var TestWindow = {}; TestWindow.getWindow = function() { return document.getElementById("content").contentWindow; }; TestWindow.getBrowser = function() { return document.getElementById("content"); }; TestWindow.getDocument = function() { return document.getElementById("content").contentDocument; };