Bug 886173 - Preserve playbackRate across pause/play. r=cpearce
[gecko.git] / testing / mochitest / browser-test.js
blob59600a159fe4853bb484378f61bbb18b7d882c6d
1 // Test timeout (seconds)
2 var gTimeoutSeconds = 30;
3 var gConfig;
5 if (Cc === undefined) {
6   var Cc = Components.classes;
7   var Ci = Components.interfaces;
8   var Cu = Components.utils;
11 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
13 XPCOMUtils.defineLazyModuleGetter(this, "Services",
14   "resource://gre/modules/Services.jsm");
16 window.addEventListener("load", testOnLoad, false);
18 function testOnLoad() {
19   window.removeEventListener("load", testOnLoad, false);
21   gConfig = readConfig();
22   if (gConfig.testRoot == "browser" ||
23       gConfig.testRoot == "metro" ||
24       gConfig.testRoot == "webapprtChrome") {
25     // Make sure to launch the test harness for the first opened window only
26     var prefs = Services.prefs;
27     if (prefs.prefHasUserValue("testing.browserTestHarness.running"))
28       return;
30     prefs.setBoolPref("testing.browserTestHarness.running", true);
32     if (prefs.prefHasUserValue("testing.browserTestHarness.timeout"))
33       gTimeoutSeconds = prefs.getIntPref("testing.browserTestHarness.timeout");
35     var sstring = Cc["@mozilla.org/supports-string;1"].
36                   createInstance(Ci.nsISupportsString);
37     sstring.data = location.search;
39     Services.ww.openWindow(window, "chrome://mochikit/content/browser-harness.xul", "browserTest",
40                            "chrome,centerscreen,dialog=no,resizable,titlebar,toolbar=no,width=800,height=600", sstring);
41   } else {
42     // This code allows us to redirect without requiring specialpowers for chrome and a11y tests.
43     function messageHandler(m) {
44       messageManager.removeMessageListener("chromeEvent", messageHandler);
45       var url = m.json.data;
47       // Window is the [ChromeWindow] for messageManager, so we need content.window 
48       // Currently chrome tests are run in a content window instead of a ChromeWindow
49       var webNav = content.window.QueryInterface(Components.interfaces.nsIInterfaceRequestor)
50                          .getInterface(Components.interfaces.nsIWebNavigation);
51       webNav.loadURI(url, null, null, null, null);
52     }
54     var listener = 'data:,function doLoad(e) { var data=e.getData("data");removeEventListener("contentEvent", function (e) { doLoad(e); }, false, true);sendAsyncMessage("chromeEvent", {"data":data}); };addEventListener("contentEvent", function (e) { doLoad(e); }, false, true);';
55     messageManager.loadFrameScript(listener, true);
56     messageManager.addMessageListener("chromeEvent", messageHandler);
57   }
60 function Tester(aTests, aDumper, aCallback) {
61   this.dumper = aDumper;
62   this.tests = aTests;
63   this.callback = aCallback;
64   this.openedWindows = {};
65   this.openedURLs = {};
67   this._scriptLoader = Services.scriptloader;
68   this._scriptLoader.loadSubScript("chrome://mochikit/content/tests/SimpleTest/EventUtils.js", this.EventUtils);
69   var simpleTestScope = {};
70   this._scriptLoader.loadSubScript("chrome://mochikit/content/tests/SimpleTest/specialpowersAPI.js", simpleTestScope);
71   this._scriptLoader.loadSubScript("chrome://mochikit/content/tests/SimpleTest/SpecialPowersObserverAPI.js", simpleTestScope);
72   this._scriptLoader.loadSubScript("chrome://mochikit/content/tests/SimpleTest/ChromePowers.js", simpleTestScope);
73   this._scriptLoader.loadSubScript("chrome://mochikit/content/tests/SimpleTest/SimpleTest.js", simpleTestScope);
74   this._scriptLoader.loadSubScript("chrome://mochikit/content/chrome-harness.js", simpleTestScope);
75   this.SimpleTest = simpleTestScope.SimpleTest;
77 Tester.prototype = {
78   EventUtils: {},
79   SimpleTest: {},
81   repeat: 0,
82   runUntilFailure: false,
83   checker: null,
84   currentTestIndex: -1,
85   lastStartTime: null,
86   openedWindows: null,
88   get currentTest() {
89     return this.tests[this.currentTestIndex];
90   },
91   get done() {
92     return this.currentTestIndex == this.tests.length - 1;
93   },
95   start: function Tester_start() {
96     // Check whether this window is ready to run tests.
97     if (window.BrowserChromeTest) {
98       BrowserChromeTest.runWhenReady(this.actuallyStart.bind(this));
99       return;
100     }
101     this.actuallyStart();
102   },
104   actuallyStart: function Tester_actuallyStart() {
105     //if testOnLoad was not called, then gConfig is not defined
106     if (!gConfig)
107       gConfig = readConfig();
109     if (gConfig.runUntilFailure)
110       this.runUntilFailure = true;
112     if (gConfig.repeat)
113       this.repeat = gConfig.repeat;
115     this.dumper.dump("*** Start BrowserChrome Test Results ***\n");
116     Services.console.registerListener(this);
117     Services.obs.addObserver(this, "chrome-document-global-created", false);
118     Services.obs.addObserver(this, "content-document-global-created", false);
119     this._globalProperties = Object.keys(window);
120     this._globalPropertyWhitelist = [
121       "navigator", "constructor", "top",
122       "Application",
123       "__SS_tabsToRestore", "__SSi",
124       "webConsoleCommandController",
125     ];
127     if (this.tests.length)
128       this.nextTest();
129     else
130       this.finish();
131   },
133   waitForWindowsState: function Tester_waitForWindowsState(aCallback) {
134     let timedOut = this.currentTest && this.currentTest.timedOut;
135     let baseMsg = timedOut ? "Found a {elt} after previous test timed out"
136                            : this.currentTest ? "Found an unexpected {elt} at the end of test run"
137                                               : "Found an unexpected {elt}";
139     if (this.currentTest && window.gBrowser && gBrowser.tabs.length > 1) {
140       while (gBrowser.tabs.length > 1) {
141         let lastTab = gBrowser.tabContainer.lastChild;
142         let msg = baseMsg.replace("{elt}", "tab") +
143                   ": " + lastTab.linkedBrowser.currentURI.spec;
144         this.currentTest.addResult(new testResult(false, msg, "", false));
145         gBrowser.removeTab(lastTab);
146       }
147     }
149     this.dumper.dump("TEST-INFO | checking window state\n");
150     let windowsEnum = Services.wm.getEnumerator(null);
151     while (windowsEnum.hasMoreElements()) {
152       let win = windowsEnum.getNext();
153       if (win != window && !win.closed &&
154           win.document.documentElement.getAttribute("id") != "browserTestHarness") {
155         let type = win.document.documentElement.getAttribute("windowtype");
156         switch (type) {
157         case "navigator:browser":
158           type = "browser window";
159           break;
160         case null:
161           type = "unknown window";
162           break;
163         }
164         let msg = baseMsg.replace("{elt}", type);
165         if (this.currentTest)
166           this.currentTest.addResult(new testResult(false, msg, "", false));
167         else
168           this.dumper.dump("TEST-UNEXPECTED-FAIL | (browser-test.js) | " + msg + "\n");
170         win.close();
171       }
172     }
174     // Make sure the window is raised before each test.
175     this.SimpleTest.waitForFocus(aCallback);
176   },
178   finish: function Tester_finish(aSkipSummary) {
179     var passCount = this.tests.reduce(function(a, f) a + f.passCount, 0);
180     var failCount = this.tests.reduce(function(a, f) a + f.failCount, 0);
181     var todoCount = this.tests.reduce(function(a, f) a + f.todoCount, 0);
183     if (failCount > 0 && this.runUntilFailure)
184       this.repeat = 0;
186     if (this.repeat > 0) {
187       --this.repeat;
188       this.currentTestIndex = -1;
189       this.nextTest();
190     }
191     else{
192       Services.console.unregisterListener(this);
193       Services.obs.removeObserver(this, "chrome-document-global-created");
194       Services.obs.removeObserver(this, "content-document-global-created");
195   
196       this.dumper.dump("\nINFO TEST-START | Shutdown\n");
197       if (this.tests.length) {
198         this.dumper.dump("Browser Chrome Test Summary\n");
199   
200         this.dumper.dump("\tPassed: " + passCount + "\n" +
201                          "\tFailed: " + failCount + "\n" +
202                          "\tTodo: " + todoCount + "\n");
203       } else {
204         this.dumper.dump("TEST-UNEXPECTED-FAIL | (browser-test.js) | " +
205                          "No tests to run. Did you pass an invalid --test-path?\n");
206       }
207   
208       this.dumper.dump("\n*** End BrowserChrome Test Results ***\n");
209   
210       this.dumper.done();
211   
212       // Tests complete, notify the callback and return
213       this.callback(this.tests);
214       this.callback = null;
215       this.tests = null;
216       this.openedWindows = null;
217     }
218   },
220   observe: function Tester_observe(aSubject, aTopic, aData) {
221     if (!aTopic) {
222       this.onConsoleMessage(aSubject);
223     } else if (this.currentTest) {
224       this.onDocumentCreated(aSubject);
225     }
226   },
228   onDocumentCreated: function Tester_onDocumentCreated(aWindow) {
229     let utils = aWindow.QueryInterface(Ci.nsIInterfaceRequestor)
230                        .getInterface(Ci.nsIDOMWindowUtils);
231     let outerID = utils.outerWindowID;
232     let innerID = utils.currentInnerWindowID;
234     if (!(outerID in this.openedWindows)) {
235       this.openedWindows[outerID] = this.currentTest;
236     }
237     this.openedWindows[innerID] = this.currentTest;
239     let url = aWindow.location.href || "about:blank";
240     this.openedURLs[outerID] = this.openedURLs[innerID] = url;
241   },
243   onConsoleMessage: function Tester_onConsoleMessage(aConsoleMessage) {
244     // Ignore empty messages.
245     if (!aConsoleMessage.message)
246       return;
248     try {
249       var msg = "Console message: " + aConsoleMessage.message;
250       if (this.currentTest)
251         this.currentTest.addResult(new testMessage(msg));
252       else
253         this.dumper.dump("TEST-INFO | (browser-test.js) | " + msg.replace(/\n$/, "") + "\n");
254     } catch (ex) {
255       // Swallow exception so we don't lead to another error being reported,
256       // throwing us into an infinite loop
257     }
258   },
260   nextTest: function Tester_nextTest() {
261     if (this.currentTest) {
262       // Run cleanup functions for the current test before moving on to the
263       // next one.
264       let testScope = this.currentTest.scope;
265       while (testScope.__cleanupFunctions.length > 0) {
266         let func = testScope.__cleanupFunctions.shift();
267         try {
268           func.apply(testScope);
269         }
270         catch (ex) {
271           this.currentTest.addResult(new testResult(false, "Cleanup function threw an exception", ex, false));
272         }
273       };
275       let winUtils = window.QueryInterface(Ci.nsIInterfaceRequestor)
276                            .getInterface(Ci.nsIDOMWindowUtils);
277       if (winUtils.isTestControllingRefreshes) {
278         this.currentTest.addResult(new testResult(false, "test left refresh driver under test control", "", false));
279         winUtils.restoreNormalRefresh();
280       }
282       if (this.SimpleTest.isExpectingUncaughtException()) {
283         this.currentTest.addResult(new testResult(false, "expectUncaughtException was called but no uncaught exception was detected!", "", false));
284       }
286       Object.keys(window).forEach(function (prop) {
287         if (parseInt(prop) == prop) {
288           // This is a string which when parsed as an integer and then
289           // stringified gives the original string.  As in, this is in fact a
290           // string representation of an integer, so an index into
291           // window.frames.  Skip those.
292           return;
293         }
294         if (this._globalProperties.indexOf(prop) == -1) {
295           this._globalProperties.push(prop);
296           if (this._globalPropertyWhitelist.indexOf(prop) == -1)
297             this.currentTest.addResult(new testResult(false, "leaked window property: " + prop, "", false));
298         }
299       }, this);
301       // Clear document.popupNode.  The test could have set it to a custom value
302       // for its own purposes, nulling it out it will go back to the default
303       // behavior of returning the last opened popup.
304       document.popupNode = null;
306       // Note the test run time
307       let time = Date.now() - this.lastStartTime;
308       this.dumper.dump("INFO TEST-END | " + this.currentTest.path + " | finished in " + time + "ms\n");
309       this.currentTest.setDuration(time);
311       testScope.destroy();
312       this.currentTest.scope = null;
313     }
315     // Check the window state for the current test before moving to the next one.
316     // This also causes us to check before starting any tests, since nextTest()
317     // is invoked to start the tests.
318     this.waitForWindowsState((function () {
319       if (this.done) {
320         // Many tests randomly add and remove tabs, resulting in the original
321         // tab being replaced by a new one. The last test in the suite doing this
322         // will erroneously be blamed for leaking this new tab's DOM window and
323         // docshell until shutdown. We can prevent this by removing this tab now
324         // that all tests are done.
325         if (window.gBrowser) {
326           gBrowser.addTab();
327           gBrowser.removeCurrentTab();
328         }
330         // Schedule GC and CC runs before finishing in order to detect
331         // DOM windows leaked by our tests or the tested code.
333         let checkForLeakedGlobalWindows = aCallback => {
334           Cu.schedulePreciseGC(() => {
335             let analyzer = new CCAnalyzer();
336             analyzer.run(() => {
337               let results = [];
338               for (let obj of analyzer.find("nsGlobalWindow ")) {
339                 let m = obj.name.match(/^nsGlobalWindow #(\d+)/);
340                 if (m && m[1] in this.openedWindows)
341                   results.push({ name: obj.name, url: m[1] });
342               }
343               aCallback(results);
344             });
345           });
346         };
348         let reportLeaks = aResults => {
349           for (let result of aResults) {
350             let test = this.openedWindows[result.url];
351             let msg = "leaked until shutdown [" + result.name +
352                       " " + (this.openedURLs[result.url] || "NULL") + "]";
353             test.addResult(new testResult(false, msg, "", false));
354           }
355         };
357         checkForLeakedGlobalWindows(aResults => {
358           if (aResults.length == 0) {
359             this.finish();
360             return;
361           }
362           // After the first check, if there are reported leaked windows, sleep
363           // for a while, to allow off-main-thread work to complete and free up
364           // main-thread objects.  Then check again.
365           setTimeout(() => {
366             checkForLeakedGlobalWindows(aResults => {
367               reportLeaks(aResults);
368               this.finish();
369             });
370           }, 1000);
371         });
373         return;
374       }
376       this.currentTestIndex++;
377       this.execTest();
378     }).bind(this));
379   },
381   execTest: function Tester_execTest() {
382     this.dumper.dump("TEST-START | " + this.currentTest.path + "\n");
384     this.SimpleTest.reset();
386     // Load the tests into a testscope
387     this.currentTest.scope = new testScope(this, this.currentTest);
389     // Import utils in the test scope.
390     this.currentTest.scope.EventUtils = this.EventUtils;
391     this.currentTest.scope.SimpleTest = this.SimpleTest;
392     this.currentTest.scope.gTestPath = this.currentTest.path;
394     // Override SimpleTest methods with ours.
395     ["ok", "is", "isnot", "ise", "todo", "todo_is", "todo_isnot", "info"].forEach(function(m) {
396       this.SimpleTest[m] = this[m];
397     }, this.currentTest.scope);
399     //load the tools to work with chrome .jar and remote
400     try {
401       this._scriptLoader.loadSubScript("chrome://mochikit/content/chrome-harness.js", this.currentTest.scope);
402     } catch (ex) { /* no chrome-harness tools */ }
404     // Import head.js script if it exists.
405     var currentTestDirPath =
406       this.currentTest.path.substr(0, this.currentTest.path.lastIndexOf("/"));
407     var headPath = currentTestDirPath + "/head.js";
408     try {
409       this._scriptLoader.loadSubScript(headPath, this.currentTest.scope);
410     } catch (ex) {
411       // Ignore if no head.js exists, but report all other errors.  Note this
412       // will also ignore an existing head.js attempting to import a missing
413       // module - see bug 755558 for why this strategy is preferred anyway.
414       if (ex.toString() != 'Error opening input stream (invalid filename?)') {
415        this.currentTest.addResult(new testResult(false, "head.js import threw an exception", ex, false));
416       }
417     }
419     // Import the test script.
420     try {
421       this._scriptLoader.loadSubScript(this.currentTest.path,
422                                        this.currentTest.scope);
424       // Run the test
425       this.lastStartTime = Date.now();
426       if ("generatorTest" in this.currentTest.scope) {
427         if ("test" in this.currentTest.scope)
428           throw "Cannot run both a generator test and a normal test at the same time.";
430         // This test is a generator. It will not finish immediately.
431         this.currentTest.scope.waitForExplicitFinish();
432         var result = this.currentTest.scope.generatorTest();
433         this.currentTest.scope.__generator = result;
434         result.next();
435       } else {
436         this.currentTest.scope.test();
437       }
438     } catch (ex) {
439       var isExpected = !!this.SimpleTest.isExpectingUncaughtException();
440       if (!this.SimpleTest.isIgnoringAllUncaughtExceptions()) {
441         this.currentTest.addResult(new testResult(isExpected, "Exception thrown", ex, false));
442         this.SimpleTest.expectUncaughtException(false);
443       } else {
444         this.currentTest.addResult(new testMessage("Exception thrown: " + ex));
445       }
446       this.currentTest.scope.finish();
447     }
449     // If the test ran synchronously, move to the next test, otherwise the test
450     // will trigger the next test when it is done.
451     if (this.currentTest.scope.__done) {
452       this.nextTest();
453     }
454     else {
455       var self = this;
456       this.currentTest.scope.__waitTimer = setTimeout(function() {
457         if (--self.currentTest.scope.__timeoutFactor > 0) {
458           // We were asked to wait a bit longer.
459           self.currentTest.scope.info(
460             "Longer timeout required, waiting longer...  Remaining timeouts: " +
461             self.currentTest.scope.__timeoutFactor);
462           self.currentTest.scope.__waitTimer =
463             setTimeout(arguments.callee, gTimeoutSeconds * 1000);
464           return;
465         }
466         self.currentTest.addResult(new testResult(false, "Test timed out", "", false));
467         self.currentTest.timedOut = true;
468         self.currentTest.scope.__waitTimer = null;
469         self.nextTest();
470       }, gTimeoutSeconds * 1000);
471     }
472   },
474   QueryInterface: function(aIID) {
475     if (aIID.equals(Ci.nsIConsoleListener) ||
476         aIID.equals(Ci.nsISupports))
477       return this;
479     throw Components.results.NS_ERROR_NO_INTERFACE;
480   }
483 function testResult(aCondition, aName, aDiag, aIsTodo, aStack) {
484   this.msg = aName || "";
486   this.info = false;
487   this.pass = !!aCondition;
488   this.todo = aIsTodo;
490   if (this.pass) {
491     if (aIsTodo)
492       this.result = "TEST-KNOWN-FAIL";
493     else
494       this.result = "TEST-PASS";
495   } else {
496     if (aDiag) {
497       if (typeof aDiag == "object" && "fileName" in aDiag) {
498         // we have an exception - print filename and linenumber information
499         this.msg += " at " + aDiag.fileName + ":" + aDiag.lineNumber;
500       }
501       this.msg += " - " + aDiag;
502     }
503     if (aStack) {
504       this.msg += "\nStack trace:\n";
505       var frame = aStack;
506       while (frame) {
507         this.msg += "    " + frame + "\n";
508         frame = frame.caller;
509       }
510     }
511     if (aIsTodo)
512       this.result = "TEST-UNEXPECTED-PASS";
513     else
514       this.result = "TEST-UNEXPECTED-FAIL";
515   }
518 function testMessage(aName) {
519   this.msg = aName || "";
520   this.info = true;
521   this.result = "TEST-INFO";
524 // Need to be careful adding properties to this object, since its properties
525 // cannot conflict with global variables used in tests.
526 function testScope(aTester, aTest) {
527   this.__tester = aTester;
529   var self = this;
530   this.ok = function test_ok(condition, name, diag, stack) {
531     aTest.addResult(new testResult(condition, name, diag, false,
532                                    stack ? stack : Components.stack.caller));
533   };
534   this.is = function test_is(a, b, name) {
535     self.ok(a == b, name, "Got " + a + ", expected " + b, false,
536             Components.stack.caller);
537   };
538   this.isnot = function test_isnot(a, b, name) {
539     self.ok(a != b, name, "Didn't expect " + a + ", but got it", false,
540             Components.stack.caller);
541   };
542   this.ise = function test_ise(a, b, name) {
543     self.ok(a === b, name, "Got " + a + ", strictly expected " + b, false,
544             Components.stack.caller);
545   };
546   this.todo = function test_todo(condition, name, diag, stack) {
547     aTest.addResult(new testResult(!condition, name, diag, true,
548                                    stack ? stack : Components.stack.caller));
549   };
550   this.todo_is = function test_todo_is(a, b, name) {
551     self.todo(a == b, name, "Got " + a + ", expected " + b,
552               Components.stack.caller);
553   };
554   this.todo_isnot = function test_todo_isnot(a, b, name) {
555     self.todo(a != b, name, "Didn't expect " + a + ", but got it",
556               Components.stack.caller);
557   };
558   this.info = function test_info(name) {
559     aTest.addResult(new testMessage(name));
560   };
562   this.executeSoon = function test_executeSoon(func) {
563     Services.tm.mainThread.dispatch({
564       run: function() {
565         func();
566       }
567     }, Ci.nsIThread.DISPATCH_NORMAL);
568   };
570   this.nextStep = function test_nextStep(arg) {
571     if (self.__done) {
572       aTest.addResult(new testResult(false, "nextStep was called too many times", "", false));
573       return;
574     }
576     if (!self.__generator) {
577       aTest.addResult(new testResult(false, "nextStep called with no generator", "", false));
578       self.finish();
579       return;
580     }
582     try {
583       self.__generator.send(arg);
584     } catch (ex if ex instanceof StopIteration) {
585       // StopIteration means test is finished.
586       self.finish();
587     } catch (ex) {
588       var isExpected = !!self.SimpleTest.isExpectingUncaughtException();
589       if (!self.SimpleTest.isIgnoringAllUncaughtExceptions()) {
590         aTest.addResult(new testResult(isExpected, "Exception thrown", ex, false));
591         self.SimpleTest.expectUncaughtException(false);
592       } else {
593         aTest.addResult(new testMessage("Exception thrown: " + ex));
594       }
595       self.finish();
596     }
597   };
599   this.waitForExplicitFinish = function test_waitForExplicitFinish() {
600     self.__done = false;
601   };
603   this.waitForFocus = function test_waitForFocus(callback, targetWindow, expectBlankPage) {
604     self.SimpleTest.waitForFocus(callback, targetWindow, expectBlankPage);
605   };
607   this.waitForClipboard = function test_waitForClipboard(expected, setup, success, failure, flavor) {
608     self.SimpleTest.waitForClipboard(expected, setup, success, failure, flavor);
609   };
611   this.registerCleanupFunction = function test_registerCleanupFunction(aFunction) {
612     self.__cleanupFunctions.push(aFunction);
613   };
615   this.requestLongerTimeout = function test_requestLongerTimeout(aFactor) {
616     self.__timeoutFactor = aFactor;
617   };
619   this.copyToProfile = function test_copyToProfile(filename) {
620     self.SimpleTest.copyToProfile(filename);
621   };
623   this.expectUncaughtException = function test_expectUncaughtException(aExpecting) {
624     self.SimpleTest.expectUncaughtException(aExpecting);
625   };
627   this.ignoreAllUncaughtExceptions = function test_ignoreAllUncaughtExceptions(aIgnoring) {
628     self.SimpleTest.ignoreAllUncaughtExceptions(aIgnoring);
629   };
631   this.finish = function test_finish() {
632     self.__done = true;
633     if (self.__waitTimer) {
634       self.executeSoon(function() {
635         if (self.__done && self.__waitTimer) {
636           clearTimeout(self.__waitTimer);
637           self.__waitTimer = null;
638           self.__tester.nextTest();
639         }
640       });
641     }
642   };
644 testScope.prototype = {
645   __done: true,
646   __generator: null,
647   __waitTimer: null,
648   __cleanupFunctions: [],
649   __timeoutFactor: 1,
651   EventUtils: {},
652   SimpleTest: {},
654   destroy: function test_destroy() {
655     for (let prop in this)
656       delete this[prop];
657   }