Bug 1280300 - Support navigation by fragment; r?automatedtester draft
authorAndreas Tolfsen <ato@mozilla.com>
Tue, 19 Jul 2016 18:47:33 +0100
changeset 396178 174550b902263503c05fae4d7f463d4337040ea3
parent 394995 ffac2798999c5b84f1b4605a1280994bb665a406
child 527148 a8f2fb56ca06f8fbe1aa3fc022c0d48333d187e1
push id24943
push userbmo:ato@mozilla.com
push dateWed, 03 Aug 2016 13:58:30 +0000
reviewersautomatedtester
bugs1280300
milestone51.0a1
Bug 1280300 - Support navigation by fragment; r?automatedtester Adds support for navigating to a fragment on the currenty visible document without waiting for a DOM event that the document has been fully loaded. This addresses https://github.com/mozilla/geckodriver/issues/150. MozReview-Commit-ID: 7uiPT5cjGQE
testing/marionette/harness/marionette/tests/unit/test_navigation.py
testing/marionette/jar.mn
testing/marionette/listener.js
testing/marionette/navigate.js
testing/marionette/test_navigate.js
testing/marionette/unit.ini
--- a/testing/marionette/harness/marionette/tests/unit/test_navigation.py
+++ b/testing/marionette/harness/marionette/tests/unit/test_navigation.py
@@ -1,17 +1,24 @@
 # This Source Code Form is subject to the terms of the Mozilla Public
 # License, v. 2.0. If a copy of the MPL was not distributed with this
 # file, You can obtain one at http://mozilla.org/MPL/2.0/.
 
+import time
+import urllib
+
 from marionette import MarionetteTestCase
 from marionette_driver.errors import MarionetteException, TimeoutException
 from marionette_driver.by import By
 
 
+def inline(doc):
+    return "data:text/html;charset=utf-8,%s" % urllib.quote(doc)
+
+
 class TestNavigate(MarionetteTestCase):
     def setUp(self):
         MarionetteTestCase.setUp(self)
         self.marionette.execute_script("window.location.href = 'about:blank'")
         self.assertEqual("about:blank", self.location_href)
         self.test_doc = self.marionette.absolute_url("test.html")
         self.iframe_doc = self.marionette.absolute_url("test_iframe.html")
 
@@ -55,30 +62,32 @@ class TestNavigate(MarionetteTestCase):
 
     def test_go_forward(self):
         self.marionette.navigate(self.test_doc)
         self.assertNotEqual("about:blank", self.location_href)
         self.assertEqual("Marionette Test", self.marionette.title)
         self.marionette.navigate("about:blank")
         self.assertEqual("about:blank", self.location_href)
         self.marionette.go_back()
-        self.assertNotEqual("about:blank", self.location_href)
+        self.assertEqual(self.test_doc, self.location_href)
         self.assertEqual("Marionette Test", self.marionette.title)
         self.marionette.go_forward()
         self.assertEqual("about:blank", self.location_href)
 
     def test_refresh(self):
         self.marionette.navigate(self.test_doc)
         self.assertEqual("Marionette Test", self.marionette.title)
         self.assertTrue(self.marionette.execute_script(
             """var elem = window.document.createElement('div'); elem.id = 'someDiv';
             window.document.body.appendChild(elem); return true;"""))
         self.assertFalse(self.marionette.execute_script(
             "return window.document.getElementById('someDiv') == undefined"))
         self.marionette.refresh()
+        # TODO(ato): Bug 1291320
+        time.sleep(0.2)
         self.assertEqual("Marionette Test", self.marionette.title)
         self.assertTrue(self.marionette.execute_script(
             "return window.document.getElementById('someDiv') == undefined"))
 
     """ Disabled due to Bug 977899
     def test_navigate_frame(self):
         self.marionette.navigate(self.marionette.absolute_url("test_iframe.html"))
         self.marionette.switch_to_frame(0)
@@ -90,17 +99,17 @@ class TestNavigate(MarionetteTestCase):
 
     def test_should_not_error_if_nonexistent_url_used(self):
         try:
             self.marionette.navigate("thisprotocoldoesnotexist://")
             self.fail("Should have thrown a MarionetteException")
         except TimeoutException:
             self.fail("The socket shouldn't have timed out when navigating to a non-existent URL")
         except MarionetteException as e:
-            self.assertIn("Error loading page", str(e))
+            self.assertIn("Reached error page", str(e))
         except Exception as e:
             import traceback
             print traceback.format_exc()
             self.fail("Should have thrown a MarionetteException instead of %s" % type(e))
 
     def test_should_navigate_to_requested_about_page(self):
         self.marionette.navigate("about:neterror")
         self.assertEqual(self.marionette.get_url(), "about:neterror")
@@ -127,11 +136,18 @@ class TestNavigate(MarionetteTestCase):
             print traceback.format_exc()
             self.fail("Should have thrown a TimeoutException instead of %s" % type(e))
 
     def test_navigate_iframe(self):
         self.marionette.navigate(self.iframe_doc)
         self.assertTrue('test_iframe.html' in self.marionette.get_url())
         self.assertTrue(self.marionette.find_element(By.ID, "test_iframe"))
 
+    def test_fragment(self):
+        doc = inline("<p id=foo>")
+        self.marionette.navigate(doc)
+        self.marionette.execute_script("window.visited = true", sandbox=None)
+        self.marionette.navigate("%s#foo" % doc)
+        self.assertTrue(self.marionette.execute_script("return window.visited", sandbox=None))
+
     @property
     def location_href(self):
         return self.marionette.execute_script("return window.location.href")
--- a/testing/marionette/jar.mn
+++ b/testing/marionette/jar.mn
@@ -20,16 +20,17 @@ marionette.jar:
   content/dispatcher.js (dispatcher.js)
   content/modal.js (modal.js)
   content/proxy.js (proxy.js)
   content/capture.js (capture.js)
   content/cookies.js (cookies.js)
   content/atom.js (atom.js)
   content/evaluate.js (evaluate.js)
   content/logging.js (logging.js)
+  content/navigate.js (navigate.js)
 #ifdef ENABLE_TESTS
   content/test.xul  (harness/marionette/chrome/test.xul)
   content/test2.xul  (harness/marionette/chrome/test2.xul)
   content/test_dialog.xul  (harness/marionette/chrome/test_dialog.xul)
   content/test_nested_iframe.xul  (harness/marionette/chrome/test_nested_iframe.xul)
   content/test_anonymous_content.xul  (harness/marionette/chrome/test_anonymous_content.xul)
 #endif
 
--- a/testing/marionette/listener.js
+++ b/testing/marionette/listener.js
@@ -18,16 +18,17 @@ Cu.import("chrome://marionette/content/a
 Cu.import("chrome://marionette/content/capture.js");
 Cu.import("chrome://marionette/content/cookies.js");
 Cu.import("chrome://marionette/content/element.js");
 Cu.import("chrome://marionette/content/error.js");
 Cu.import("chrome://marionette/content/evaluate.js");
 Cu.import("chrome://marionette/content/event.js");
 Cu.import("chrome://marionette/content/interaction.js");
 Cu.import("chrome://marionette/content/logging.js");
+Cu.import("chrome://marionette/content/navigate.js");
 Cu.import("chrome://marionette/content/proxy.js");
 Cu.import("chrome://marionette/content/simpletest.js");
 
 Cu.import("resource://gre/modules/FileUtils.jsm");
 Cu.import("resource://gre/modules/Task.jsm");
 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
 
 Cu.importGlobalProperties(["URL"]);
@@ -876,97 +877,115 @@ function multiAction(args, maxLen) {
   setDispatch(concurrentEvent, pendingTouches);
 }
 
 /*
  * This implements the latter part of a get request (for the case we need to resume one
  * when a remoteness update happens in the middle of a navigate request). This is most of
  * of the work of a navigate request, but doesn't assume DOMContentLoaded is yet to fire.
  */
-function pollForReadyState(msg, start, callback) {
+function pollForReadyState(msg, start = undefined, callback = undefined) {
   let {pageTimeout, url, command_id} = msg.json;
-  start = start ? start : new Date().getTime();
-
+  if (!start) {
+    start = new Date().getTime();
+  }
   if (!callback) {
     callback = () => {};
   }
 
-  let end = null;
-  function checkLoad() {
+  let checkLoad = function() {
     navTimer.cancel();
-    end = new Date().getTime();
-    let aboutErrorRegex = /about:.+(error)\?/;
-    let elapse = end - start;
+
     let doc = curContainer.frame.document;
-    if (pageTimeout == null || elapse <= pageTimeout) {
+    let now = new Date().getTime();
+    if (pageTimeout == null || (now - start) <= pageTimeout) {
+      // document fully loaded
       if (doc.readyState == "complete") {
         callback();
         sendOk(command_id);
+
+      // we have reached an error url without requesting it
       } else if (doc.readyState == "interactive" &&
-                 aboutErrorRegex.exec(doc.baseURI) &&
-                 !doc.baseURI.startsWith(url)) {
-        // We have reached an error url without requesting it.
+          /about:.+(error)\?/.exec(doc.baseURI) &&
+          !doc.baseURI.startsWith(url)) {
         callback();
-        sendError(new UnknownError("Error loading page"), command_id);
-      } else if (doc.readyState == "interactive" &&
-                 doc.baseURI.startsWith("about:")) {
+        sendError(new UnknownError("Reached error page: " + doc.baseURI), command_id);
+
+      // return early for about: urls
+      } else if (doc.readyState == "interactive" && doc.baseURI.startsWith("about:")) {
         callback();
         sendOk(command_id);
+
+      // document not fully loaded
       } else {
         navTimer.initWithCallback(checkLoad, 100, Ci.nsITimer.TYPE_ONE_SHOT);
       }
+
     } else {
       callback();
       sendError(new TimeoutError("Error loading page, timed out (checkLoad)"), command_id);
     }
-  }
+  };
   checkLoad();
 }
 
 /**
  * Navigate to the given URL.  The operation will be performed on the
  * current browsing context, which means it handles the case where we
  * navigate within an iframe.  All other navigation is handled by the
  * driver (in chrome space).
  */
 function get(msg) {
   let start = new Date().getTime();
-  let requestedURL = new URL(msg.json.url).toString();
+  let command_id = msg.json.command_id;
+
   let docShell = curContainer.frame
-                             .document
-                             .defaultView
-                             .QueryInterface(Ci.nsIInterfaceRequestor)
-                             .getInterface(Ci.nsIWebNavigation)
-                             .QueryInterface(Ci.nsIDocShell);
+      .document
+      .defaultView
+      .QueryInterface(Ci.nsIInterfaceRequestor)
+      .getInterface(Ci.nsIWebNavigation)
+      .QueryInterface(Ci.nsIDocShell);
   let webProgress = docShell.QueryInterface(Ci.nsIInterfaceRequestor)
-                            .getInterface(Ci.nsIWebProgress);
+      .getInterface(Ci.nsIWebProgress);
   let sawLoad = false;
 
+  let requestedURL;
+  let loadEventExpected = false;
+  try {
+    requestedURL = new URL(msg.json.url).toString();
+    let curURL = curContainer.frame.location;
+    loadEventExpected = navigate.isLoadEventExpected(curURL, requestedURL);
+  } catch (e) {
+    sendError(new InvalidArgumentError("Malformed URL: " + e.message), command_id);
+    return;
+  }
+
   // It's possible that a site we're being sent to will end up redirecting
   // us before we end up on a page that fires DOMContentLoaded. We can ensure
   // This loadListener ensures that we don't send a success signal back to
   // the caller until we've seen the load of the requested URL attempted
   // on this frame.
   let loadListener = {
-    QueryInterface: XPCOMUtils.generateQI([Ci.nsIWebProgressListener,
-                                           Ci.nsISupportsWeakReference]),
+    QueryInterface: XPCOMUtils.generateQI(
+        [Ci.nsIWebProgressListener, Ci.nsISupportsWeakReference]),
+
     onStateChange(webProgress, request, state, status) {
       if (!(request instanceof Ci.nsIChannel)) {
         return;
       }
 
       let isDocument = state & Ci.nsIWebProgressListener.STATE_IS_DOCUMENT;
       let isStart = state & Ci.nsIWebProgressListener.STATE_START;
       let loadedURL = request.URI.spec;
       // We have to look at the originalURL because for about: pages,
       // the loadedURL is what the about: page resolves to, and is
       // not the one that was requested.
       let originalURL = request.originalURI.spec;
       let isRequestedURL = loadedURL == requestedURL ||
-                           originalURL == requestedURL;
+          originalURL == requestedURL;
 
       if (isDocument && isStart && isRequestedURL) {
         // We started loading the requested document. This document
         // might not be the one that ends up firing DOMContentLoaded
         // (if it, for example, redirects), but because we've started
         // loading this URL, we know that any future DOMContentLoaded's
         // are fair game to tell the Marionette client about.
         sawLoad = true;
@@ -974,26 +993,25 @@ function get(msg) {
     },
 
     onLocationChange() {},
     onProgressChange() {},
     onStatusChange() {},
     onSecurityChange() {},
   };
 
-  webProgress.addProgressListener(loadListener,
-                                  Ci.nsIWebProgress.NOTIFY_STATE_DOCUMENT);
+  webProgress.addProgressListener(
+      loadListener, Ci.nsIWebProgress.NOTIFY_STATE_DOCUMENT);
 
   // Prevent DOMContentLoaded events from frames from invoking this
   // code, unless the event is coming from the frame associated with
   // the current window (i.e. someone has used switch_to_frame).
   onDOMContentLoaded = function onDOMContentLoaded(event) {
-    let correctFrame =
-      !event.originalTarget.defaultView.frameElement ||
-      event.originalTarget.defaultView.frameElement == curContainer.frame.frameElement;
+    let frameEl = event.originalTarget.defaultView.frameElement;
+    let correctFrame = !frameEl || frameEl == curContainer.frame.frameElement;
 
     // If the page we're at fired DOMContentLoaded and appears
     // to be the one we asked to load, then we definitely
     // saw the load occur. We need this because for error
     // pages, like about:neterror for unsupported protocols,
     // we don't end up opening a channel that our
     // WebProgressListener can monitor.
     if (curContainer.frame.location == requestedURL) {
@@ -1007,39 +1025,46 @@ function get(msg) {
     if (correctFrame && sawLoad && loadedNonAboutBlank) {
       webProgress.removeProgressListener(loadListener);
       pollForReadyState(msg, start, () => {
         removeEventListener("DOMContentLoaded", onDOMContentLoaded, false);
       });
     }
   };
 
-  function timerFunc() {
-    removeEventListener("DOMContentLoaded", onDOMContentLoaded, false);
-    webProgress.removeProgressListener(loadListener);
-    sendError(new TimeoutError("Error loading page, timed out (onDOMContentLoaded)"), msg.json.command_id);
-  }
-  if (msg.json.pageTimeout != null) {
-    navTimer.initWithCallback(timerFunc, msg.json.pageTimeout, Ci.nsITimer.TYPE_ONE_SHOT);
+  if (msg.json.pageTimeout) {
+    let onTimeout = function() {
+      if (loadEventExpected) {
+        removeEventListener("DOMContentLoaded", onDOMContentLoaded, false);
+      }
+      webProgress.removeProgressListener(loadListener);
+      sendError(new TimeoutError("Error loading page, timed out (onDOMContentLoaded)"), command_id);
+    }
+    navTimer.initWithCallback(onTimeout, msg.json.pageTimeout, Ci.nsITimer.TYPE_ONE_SHOT);
   }
-  addEventListener("DOMContentLoaded", onDOMContentLoaded, false);
-  if (isB2G) {
-    curContainer.frame.location = requestedURL;
-  } else {
-    // We need to move to the top frame before navigating
-    sendSyncMessage("Marionette:switchedToFrame", { frameValue: null });
+
+  // in Firefox we need to move to the top frame before navigating
+  if (!isB2G) {
+    sendSyncMessage("Marionette:switchedToFrame", {frameValue: null});
     curContainer.frame = content;
-    curContainer.frame.location = requestedURL;
+  }
+
+  if (loadEventExpected) {
+    addEventListener("DOMContentLoaded", onDOMContentLoaded, false);
+  }
+  curContainer.frame.location = requestedURL;
+  if (!loadEventExpected) {
+    sendOk(command_id);
   }
 }
 
- /**
- * Cancel the polling and remove the event listener associated with a current
- * navigation request in case we're interupted by an onbeforeunload handler
- * and navigation doesn't complete.
+/**
+ * Cancel the polling and remove the event listener associated with a
+ * current navigation request in case we're interupted by an onbeforeunload
+ * handler and navigation doesn't complete.
  */
 function cancelRequest() {
   navTimer.cancel();
   if (onDOMContentLoaded) {
     removeEventListener("DOMContentLoaded", onDOMContentLoaded, false);
   }
 }
 
new file mode 100644
--- /dev/null
+++ b/testing/marionette/navigate.js
@@ -0,0 +1,124 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+const {classes: Cc, interfaces: Ci, utils: Cu, results: Cr} = Components;
+
+Cu.importGlobalProperties(["URL"]);
+
+this.EXPORTED_SYMBOLS = ["navigate"];
+
+this.navigate = {};
+
+/**
+ * Determines if we expect to get a DOM load event (DOMContentLoaded)
+ * on navigating to the |future| URL.
+ *
+ * @param {string} current
+ *     URL the browser is currently visiting.
+ * @param {string=} future
+ *     Destination URL, if known.
+ *
+ * @return {boolean}
+ *     Full page load would be expected if future is followed.
+ *
+ * @throws TypeError
+ *     If |current| is not defined, or any of |current| or |future|
+ *     are invalid URLs.
+ */
+navigate.isLoadEventExpected = function(current, future = undefined) {
+  if (typeof current == "undefined") {
+    throw TypeError("Expected at least one URL");
+  }
+
+  // assume we will go somewhere exciting
+  if (typeof future == "undefined") {
+    return true;
+  }
+
+  let cur = new navigate.IdempotentURL(current);
+  let fut = new navigate.IdempotentURL(future);
+
+  // assume javascript:<whatever> will modify current document
+  // but this is not an entirely safe assumption to make,
+  // considering it could be used to set window.location
+  if (fut.protocol == "javascript:") {
+    return false;
+  }
+
+  // navigating to same url, but with any hash
+  if (cur.origin == fut.origin &&
+      cur.pathname == fut.pathname &&
+      fut.hash != "") {
+    return false;
+  }
+
+  return true;
+};
+
+/**
+ * Sane URL implementation that normalises URL fragments (hashes) and
+ * path names for "data:" URLs, and makes them idempotent.
+ *
+ * At the time of writing this, the web is approximately 10 000 days (or
+ * ~27.39 years) old.  One should think that by this point we would have
+ * solved URLs.  The following code is prudent example that we have not.
+ *
+ * When a URL with a fragment identifier but no explicit name for the
+ * fragment is given, i.e. "#", the {@code hash} property a {@code URL}
+ * object computes is an empty string.  This is incidentally the same as
+ * the default value of URLs without fragments, causing a lot of confusion.
+ *
+ * This means that the URL "http://a/#b" produces a hash of "#b", but that
+ * "http://a/#" produces "".  This implementation rectifies this behaviour
+ * by returning the actual full fragment, which is "#".
+ *
+ * "data:" URLs that contain fragments, which if they have the same origin
+ * and path name are not meant to cause a page reload on navigation,
+ * confusingly adds the fragment to the {@code pathname} property.
+ * This implementation remedies this behaviour by trimming it off.
+ *
+ * The practical result of this is that while {@code URL} objects are
+ * not idempotent, the returned URL elements from this implementation
+ * guarantees that |url.hash == url.hash|.
+ *
+ * @param {string|URL} o
+ *     Object to make an URL of.
+ *
+ * @return {navigate.IdempotentURL}
+ *     Considered by some to be a somewhat saner URL.
+ *
+ * @throws TypeError
+ *     If |o| is not a valid type or if is a string that cannot be parsed
+ *     as a URL.
+ */
+navigate.IdempotentURL = function(o) {
+  let url = new URL(o);
+
+  let hash = url.hash;
+  if (hash == "" && url.href[url.href.length - 1] == "#") {
+    hash = "#";
+  }
+
+  let pathname = url.pathname;
+  if (url.protocol == "data:" && hash != "") {
+    pathname = pathname.substring(0, pathname.length - hash.length);
+  }
+
+  return {
+    hash: hash,
+    host: url.host,
+    hostname: url.hostname,
+    href: url.href,
+    origin: url.origin,
+    password: url.password,
+    pathname: pathname,
+    port: url.port,
+    protocol: url.protocol,
+    search: url.search,
+    searchParams: url.searchParams,
+    username: url.username,
+  };
+};
new file mode 100644
--- /dev/null
+++ b/testing/marionette/test_navigate.js
@@ -0,0 +1,67 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+const {utils: Cu} = Components;
+
+Cu.importGlobalProperties(["URL"]);
+
+Cu.import("chrome://marionette/content/navigate.js");
+
+add_test(function test_isLoadEventExpected() {
+  Assert.throws(() => navigate.isLoadEventExpected(undefined),
+      /Expected at least one URL/);
+
+  equal(true, navigate.isLoadEventExpected("http://a/"));
+  equal(true, navigate.isLoadEventExpected("http://a/", "http://a/"));
+  equal(true, navigate.isLoadEventExpected("http://a/", "http://a/b"));
+  equal(true, navigate.isLoadEventExpected("http://a/", "http://b"));
+  equal(true, navigate.isLoadEventExpected("http://a/", "data:text/html;charset=utf-8,foo"));
+  equal(true, navigate.isLoadEventExpected("about:blank", "http://a/"));
+  equal(true, navigate.isLoadEventExpected("http://a/", "about:blank"));
+  equal(true, navigate.isLoadEventExpected("http://a/", "https://a/"));
+
+  equal(false, navigate.isLoadEventExpected("http://a/", "javascript:whatever"));
+  equal(false, navigate.isLoadEventExpected("http://a/", "http://a/#"));
+  equal(false, navigate.isLoadEventExpected("http://a/", "http://a/#b"));
+  equal(false, navigate.isLoadEventExpected("http://a/#b", "http://a/#b"));
+  equal(false, navigate.isLoadEventExpected("http://a/#b", "http://a/#c"));
+  equal(false, navigate.isLoadEventExpected("data:text/html;charset=utf-8,foo", "data:text/html;charset=utf-8,foo#bar"));
+  equal(false, navigate.isLoadEventExpected("data:text/html;charset=utf-8,foo", "data:text/html;charset=utf-8,foo#"));
+
+  run_next_test();
+});
+
+add_test(function test_IdempotentURL() {
+  Assert.throws(() => new navigate.IdempotentURL(undefined));
+  Assert.throws(() => new navigate.IdempotentURL(true));
+  Assert.throws(() => new navigate.IdempotentURL({}));
+  Assert.throws(() => new navigate.IdempotentURL(42));
+
+  // propagated URL properties
+  let u1 = new URL("http://a/b");
+  let u2 = new navigate.IdempotentURL(u1);
+  equal(u1.host, u2.host);
+  equal(u1.hostname, u2.hostname);
+  equal(u1.href, u2.href);
+  equal(u1.origin, u2.origin);
+  equal(u1.password, u2.password);
+  equal(u1.port, u2.port);
+  equal(u1.protocol, u2.protocol);
+  equal(u1.search, u2.search);
+  equal(u1.username, u2.username);
+
+  // specialisations
+  equal("#b", new navigate.IdempotentURL("http://a/#b").hash);
+  equal("#", new navigate.IdempotentURL("http://a/#").hash);
+  equal("", new navigate.IdempotentURL("http://a/").hash);
+  equal("#bar", new navigate.IdempotentURL("data:text/html;charset=utf-8,foo#bar").hash);
+  equal("#", new navigate.IdempotentURL("data:text/html;charset=utf-8,foo#").hash);
+  equal("", new navigate.IdempotentURL("data:text/html;charset=utf-8,foo").hash);
+
+  equal("/", new navigate.IdempotentURL("http://a/").pathname);
+  equal("/", new navigate.IdempotentURL("http://a/#b").pathname);
+  equal("text/html;charset=utf-8,foo", new navigate.IdempotentURL("data:text/html;charset=utf-8,foo#bar").pathname);
+
+  run_next_test();
+});
--- a/testing/marionette/unit.ini
+++ b/testing/marionette/unit.ini
@@ -5,8 +5,9 @@
 # xpcshell unit tests for Marionette
 
 [DEFAULT]
 skip-if = appname == "thunderbird"
 
 [test_element.js]
 [test_error.js]
 [test_message.js]
+[test_navigate.js]