From 142e987d3c16558ef48031b085da8c9c9ebce7ac Mon Sep 17 00:00:00 2001 From: Jonathan Griffin Date: Tue, 30 Jun 2009 18:35:32 -0700 Subject: [PATCH] Bug 501235 - "add new mochitest sub-harness for docshell testing." r=bzbarsky --- docshell/test/chrome/Makefile.in | 8 + docshell/test/chrome/docshell_helpers.js | 380 +++++++++++++++++++++++ docshell/test/chrome/gen_template.pl | 37 +++ docshell/test/chrome/generic.html | 12 + docshell/test/chrome/test.template.txt | 43 +++ docshell/test/chrome/window.template.txt | 44 +++ 6 files changed, 524 insertions(+) create mode 100755 docshell/test/chrome/docshell_helpers.js create mode 100644 docshell/test/chrome/gen_template.pl create mode 100644 docshell/test/chrome/generic.html create mode 100644 docshell/test/chrome/test.template.txt create mode 100644 docshell/test/chrome/window.template.txt diff --git a/docshell/test/chrome/Makefile.in b/docshell/test/chrome/Makefile.in index b07026e7713..1f1ae8030c8 100644 --- a/docshell/test/chrome/Makefile.in +++ b/docshell/test/chrome/Makefile.in @@ -78,8 +78,16 @@ _TEST_FILES = \ test_bug456980.xul \ $(NULL) +_DOCSHELL_SUBHARNESS = \ + docshell_helpers.js \ + generic.html \ + $(NULL) + libs:: $(_HTTP_FILES) $(INSTALL) $(foreach f,$^,"$f") $(DEPTH)/_tests/testing/mochitest/tests/$(relativesrcdir) libs:: $(_TEST_FILES) $(INSTALL) $(foreach f,$^,"$f") $(DEPTH)/_tests/testing/mochitest/chrome/$(relativesrcdir) + +libs:: $(_DOCSHELL_SUBHARNESS) + $(INSTALL) $(foreach f,$^,"$f") $(DEPTH)/_tests/testing/mochitest/chrome/$(relativesrcdir) diff --git a/docshell/test/chrome/docshell_helpers.js b/docshell/test/chrome/docshell_helpers.js new file mode 100755 index 00000000000..a22c6a2582f --- /dev/null +++ b/docshell/test/chrome/docshell_helpers.js @@ -0,0 +1,380 @@ +/** + * 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 each (var import in imports) { + window[import] = window.opener.wrappedJSObject[import]; +} + +/** + * Define global constants and variables. + */ +const NAV_NONE = 0; +const NAV_BACK = 1; +const NAV_FORWARD = 2; +const NAV_URI = 3; + +var gExpectedEvents; // an array of events which are expected to be + // triggered by this navigation +var gFinalEvent; // true if the last expected event has occurred +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 + + +/** + * 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() + * + * 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. + */ +function doPageNavigation(params) { + // Parse the parameters. + let back = params.back ? params.back : false; + let forward = params.forward ? params.forward : 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; + let preventBFCache = (typeof[params.preventBFCache] == "undefined") ? + false : params.preventBFCache; + + // Do some sanity checking on arguments. + if (back && forward) + throw "Can't specify both back and forward"; + if (back && uri) + throw "Can't specify both back and a uri"; + if (forward && uri) + throw "Can't specify both forward and a uri"; + if (!back && !forward && !uri) + throw "Must specify back or foward or uri"; + if (params.onNavComplete && eventsToListenFor.length == 0) + throw "Can't use onNavComplete when eventsToListenFor == []"; + if (params.preventBFCache && eventsToListenFor.length == 0) + throw "Can't use preventBFCache when eventsToListenFor == []"; + for each (let anEventType in eventsToListenFor) { + let eventFound = false; + if ( (anEventType == "pageshow") && (!gExpectedEvents) ) + eventFound = true; + for each (let anExpectedEvent in gExpectedEvents) { + if (anExpectedEvent.type == anEventType) + eventFound = true; + } + if (!eventFound) + throw "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 ? true : false; + + // Add an event listener for each type of event in the .eventsToListenFor + // property of the input parameters. + for each (let eventType in 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; + TestWindow.getBrowser().loadURI(uri); + } + else { + throw "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 each (let eventType in 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(); +} + +/** + * 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 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") + { + setTimeout(function() { gFinalEvent = true; }, 0); + 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.title + " had an unexpected value"); + } + + // If we're out of expected events, let doPageNavigation() return. + if (gExpectedEvents.length == 0) + setTimeout(function() { gFinalEvent = true; }, 0); +} + +/** + * End a test. + */ +function finish() { + // Work around bug 467960. + var history = TestWindow.getBrowser().webNavigation.sessionHistory; + history.PurgeHistory(history.count); + + // Close the test window and signal the framework that the test is done. + window.close(); + window.opener.wrappedJSObject.SimpleTest.finish(); +} + +/** + * 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 ? true : false; + 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); +} + +/** + * 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) { + netscape.security.PrivilegeManager.enablePrivilege("UniversalXPConnect"); + var prefs = Components.classes["@mozilla.org/preferences-service;1"] + .getService(Components.interfaces.nsIPrefBranch); + if (typeof(enable) == "boolean") { + if (enable) + prefs.setIntPref("browser.sessionhistory.max_total_viewers", -1); + else + prefs.setIntPref("browser.sessionhistory.max_total_viewers", 0); + } + else if (typeof(enable) == "number") { + prefs.setIntPref("browser.sessionhistory.max_total_viewers", enable); + } +} + +/** + * Returns the full HTTP url for a file in the mochitest docshell test + * directory. + */ +function getHttpUrl(filename) { + return "http://localhost:8888/chrome/docshell/test/chrome/" + 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; +} diff --git a/docshell/test/chrome/gen_template.pl b/docshell/test/chrome/gen_template.pl new file mode 100644 index 00000000000..97ead4ead9e --- /dev/null +++ b/docshell/test/chrome/gen_template.pl @@ -0,0 +1,37 @@ +#!/usr/bin/perl +# +# gen_template.pl +# Makes docshell test case templates. +# Takes one argument: +# +# -b : a bugnumber +# +# e.g.: perl gen_template.pl -b 303267 +# +# Writes test case template files for bug 303267 to the current directory. +use FindBin; +use Getopt::Long; +GetOptions("b=i"=> \$bug_number); + +$template = "$FindBin::RealBin/test.template.txt"; + +open(IN,$template) or die("Failed to open input file for reading."); +open(OUT, ">>test_bug" . $bug_number . ".xul") or die("Failed to open output file for appending."); +while((defined(IN)) && ($line = )) { + $line =~ s/{BUGNUMBER}/$bug_number/g; + print OUT $line; +} +close(IN); +close(OUT); + +$template = "$FindBin::RealBin/window.template.txt"; + +open(IN,$template) or die("Failed to open input file for reading."); +open(OUT, ">>bug" . $bug_number . "_window.xul") or die("Failed to open output file for appending."); +while((defined(IN)) && ($line = )) { + $line =~ s/{BUGNUMBER}/$bug_number/g; + print OUT $line; +} +close(IN); +close(OUT); + diff --git a/docshell/test/chrome/generic.html b/docshell/test/chrome/generic.html new file mode 100644 index 00000000000..569a78c05ad --- /dev/null +++ b/docshell/test/chrome/generic.html @@ -0,0 +1,12 @@ + + + + generic page + + + +
+ A generic page which can be used any time a test needs to load an arbitrary page via http. +
+ + diff --git a/docshell/test/chrome/test.template.txt b/docshell/test/chrome/test.template.txt new file mode 100644 index 00000000000..f994eb6dd2d --- /dev/null +++ b/docshell/test/chrome/test.template.txt @@ -0,0 +1,43 @@ + + + + + + + Test for Bug {BUGNUMBER} + + + + + + Mozilla Bug {BUGNUMBER} +

+ +
+
+ + + + +
diff --git a/docshell/test/chrome/window.template.txt b/docshell/test/chrome/window.template.txt new file mode 100644 index 00000000000..4f0d7f51f54 --- /dev/null +++ b/docshell/test/chrome/window.template.txt @@ -0,0 +1,44 @@ + + + + + + + + + + +