Bug 1330348 - Make forward- and backward commands synchronous. draft
authorHenrik Skupin <mail@hskupin.info>
Mon, 06 Mar 2017 14:14:21 +0100
changeset 496180 4791f8ef73f1178df6b1d4412dc2e9b376ba1148
parent 496179 b297a91f29123d23eedf8cf40bf6fd1437ad7805
child 548568 96c65ddd3bf1c2dcea35b52f577a0846783563c5
push id48548
push userbmo:hskupin@gmail.com
push dateThu, 09 Mar 2017 22:28:43 +0000
bugs1330348
milestone55.0a1
Bug 1330348 - Make forward- and backward commands synchronous. Both `goBack` and `goForward` commands should not return immediately, but when the requested page has been fully loaded. To handle that a general `waitForPageUnloaded` method has been added, which will call `pollForReadyState` when necessary. Similar to `get` the dispatcher cannot be used due to possible remoteness changes. As such the driver has to poll the framescript until the page load has been finished. MozReview-Commit-ID: 4F7Piymxwhs
testing/marionette/driver.js
testing/marionette/harness/marionette_harness/tests/unit/test_about_pages.py
testing/marionette/harness/marionette_harness/tests/unit/test_navigation.py
testing/marionette/harness/marionette_harness/www/frameset.html
testing/marionette/harness/marionette_harness/www/xhtmlTest.html
testing/marionette/listener.js
--- a/testing/marionette/driver.js
+++ b/testing/marionette/driver.js
@@ -1024,28 +1024,96 @@ GeckoDriver.prototype.getPageSource = fu
       break;
 
     case Context.CONTENT:
       resp.body.value = yield this.listener.getPageSource();
       break;
   }
 };
 
-/** Go back in history. */
-GeckoDriver.prototype.goBack = function*(cmd, resp) {
+/**
+ * Cause the browser to traverse one step backward in the joint history
+ * of the current browsing context.
+ */
+GeckoDriver.prototype.goBack = function* (cmd, resp) {
   assert.content(this.context);
 
-  yield this.listener.goBack();
+  if (!this.curBrowser.tab) {
+    // Navigation does not work for non-browser windows
+    return;
+  }
+
+  let contentBrowser = browser.getBrowserForTab(this.curBrowser.tab)
+  if (!contentBrowser.webNavigation.canGoBack) {
+    return;
+  }
+
+  let currentURL = yield this.listener.getCurrentUrl();
+  let goBack = this.listener.goBack({pageTimeout: this.timeouts.pageLoad});
+
+  // If a remoteness update interrupts our page load, this will never return
+  // We need to re-issue this request to correctly poll for readyState and
+  // send errors.
+  this.curBrowser.pendingCommands.push(() => {
+    let parameters = {
+      // TODO(ato): Bug 1242595
+      command_id: this.listener.activeMessageId,
+      lastSeenURL: currentURL,
+      pageTimeout: this.timeouts.pageLoad,
+      startTime: new Date().getTime(),
+    };
+    this.mm.broadcastAsyncMessage(
+        // TODO: combine with
+        // "Marionette:pollForReadyState" + this.curBrowser.curFrameId,
+        "Marionette:pollForReadyState" + this.curBrowser.curFrameId,
+        parameters);
+  });
+
+  yield goBack;
 };
 
-/** Go forward in history. */
-GeckoDriver.prototype.goForward = function*(cmd, resp) {
+/**
+ * Cause the browser to traverse one step forward in the joint history
+ * of the current browsing context.
+ */
+GeckoDriver.prototype.goForward = function* (cmd, resp) {
   assert.content(this.context);
 
-  yield this.listener.goForward();
+  if (!this.curBrowser.tab) {
+    // Navigation does not work for non-browser windows
+    return;
+  }
+
+  let contentBrowser = browser.getBrowserForTab(this.curBrowser.tab)
+  if (!contentBrowser.webNavigation.canGoForward) {
+    return;
+  }
+
+  let currentURL = yield this.listener.getCurrentUrl();
+  let goForward = this.listener.goForward({pageTimeout: this.timeouts.pageLoad});
+
+  // If a remoteness update interrupts our page load, this will never return
+  // We need to re-issue this request to correctly poll for readyState and
+  // send errors.
+  this.curBrowser.pendingCommands.push(() => {
+    let parameters = {
+      // TODO(ato): Bug 1242595
+      command_id: this.listener.activeMessageId,
+      lastSeenURL: currentURL,
+      pageTimeout: this.timeouts.pageLoad,
+      startTime: new Date().getTime(),
+    };
+    this.mm.broadcastAsyncMessage(
+        // TODO: combine with
+        // "Marionette:pollForReadyState" + this.curBrowser.curFrameId,
+        "Marionette:pollForReadyState" + this.curBrowser.curFrameId,
+        parameters);
+  });
+
+  yield goForward;
 };
 
 /** Refresh the page. */
 GeckoDriver.prototype.refresh = function*(cmd, resp) {
   assert.content(this.context);
 
   yield this.listener.refresh();
 };
--- a/testing/marionette/harness/marionette_harness/tests/unit/test_about_pages.py
+++ b/testing/marionette/harness/marionette_harness/tests/unit/test_about_pages.py
@@ -39,23 +39,20 @@ class TestAboutPages(WindowManagerMixin,
         new_tab = self.open_tab(trigger=self.open_tab_with_link)
         self.marionette.switch_to_window(new_tab)
 
         self.marionette.navigate("about:blank")
         self.marionette.navigate(self.remote_uri)
         self.marionette.navigate("about:support")
 
         self.marionette.go_back()
-        Wait(self.marionette).until(lambda mn: mn.get_url() == self.remote_uri,
-                                    message="'{}' hasn't been loaded".format(self.remote_uri))
+        self.assertEqual(self.marionette.get_url(), self.remote_uri)
 
-        # Bug 1332998 - Timeout loading the page
-        # self.marionette.go_forward()
-        # Wait(self.marionette).until(lambda mn: mn.get_url() == self.remote_uri,
-        #                             message="'about:support' hasn't been loaded")
+        self.marionette.go_forward()
+        self.assertEqual(self.marionette.get_url(), "about:support")
 
         self.marionette.close()
         self.marionette.switch_to_window(self.start_tab)
 
     @skip_if_mobile("Bug 1333209 - Process killed because of connection loss")
     def test_navigate_non_remote_about_pages(self):
         # Bug 1311041 - Prevent changing of window handle by forcing the test
         # to be run in a new tab.
--- a/testing/marionette/harness/marionette_harness/tests/unit/test_navigation.py
+++ b/testing/marionette/harness/marionette_harness/tests/unit/test_navigation.py
@@ -1,41 +1,253 @@
 # 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 contextlib
 import time
 import urllib
 
-from marionette_driver import errors, By, Wait
+from marionette_driver import By, errors, expected, Wait
 from marionette_harness import (
     MarionetteTestCase,
+    run_if_e10s,
     run_if_manage_instance,
     skip,
     skip_if_mobile,
     WindowManagerMixin,
 )
 
 
 def inline(doc):
     return "data:text/html;charset=utf-8,%s" % urllib.quote(doc)
 
 
+class TestBackForwardNavigation(WindowManagerMixin, MarionetteTestCase):
+
+    def setUp(self):
+        super(TestBackForwardNavigation, self).setUp()
+
+        self.test_page = self.marionette.absolute_url('test.html')
+
+        def open_with_link():
+            link = self.marionette.find_element(By.ID, "new-blank-tab")
+            link.click()
+
+        # Always use a blank new tab for an empty history
+        self.marionette.navigate(self.marionette.absolute_url("windowHandles.html"))
+        self.new_tab = self.open_tab(open_with_link)
+        self.marionette.switch_to_window(self.new_tab)
+        self.assertEqual(self.history_length, 1)
+
+    def tearDown(self):
+        self.marionette.switch_to_parent_frame()
+        self.close_all_tabs()
+
+        super(TestBackForwardNavigation, self).tearDown()
+
+    @property
+    def history_length(self):
+        return self.marionette.execute_script("return window.history.length;")
+
+    def run_test(self, test_pages):
+        # Helper method to run simple back and forward testcases.
+        for index, page in enumerate(test_pages):
+            if "error" in page:
+                with self.assertRaises(page["error"]):
+                    self.marionette.navigate(page["url"])
+            else:
+                self.marionette.navigate(page["url"])
+            self.assertEqual(page["url"], self.marionette.get_url())
+            self.assertEqual(self.history_length, index + 1)
+
+        for page in test_pages[-2::-1]:
+            if "error" in page:
+                with self.assertRaises(page["error"]):
+                    self.marionette.go_back()
+            else:
+                self.marionette.go_back()
+            self.assertEqual(page["url"], self.marionette.get_url())
+
+        for page in test_pages[1::]:
+            if "error" in page:
+                with self.assertRaises(page["error"]):
+                    self.marionette.go_forward()
+            else:
+                self.marionette.go_forward()
+            self.assertEqual(page["url"], self.marionette.get_url())
+
+    def test_no_history_items(self):
+        # Both methods should not raise a failure if no navigation is possible
+        self.marionette.go_back()
+        self.marionette.go_forward()
+
+    def test_data_urls(self):
+        test_pages = [
+            {"url": inline("<p>foobar</p>")},
+            {"url": self.test_page},
+            {"url": inline("<p>foobar</p>")},
+        ]
+        self.run_test(test_pages)
+
+    def test_same_document_hash_change(self):
+        test_pages = [
+            {"url": "{}#23".format(self.test_page)},
+            {"url": self.test_page},
+            {"url": "{}#42".format(self.test_page)},
+        ]
+        self.run_test(test_pages)
+
+    @skip("Causes crashes for JS GC (bug 1344863) and a11y (bug 1344868)")
+    def test_frameset(self):
+        test_pages = [
+            {"url": self.marionette.absolute_url("frameset.html")},
+            {"url": self.test_page},
+            {"url": self.marionette.absolute_url("frameset.html")},
+        ]
+        self.run_test(test_pages)
+
+    def test_frameset_after_navigating_in_frame(self):
+        test_element_locator = (By.ID, "email")
+
+        self.marionette.navigate(self.test_page)
+        self.assertEqual(self.marionette.get_url(), self.test_page)
+        self.assertEqual(self.history_length, 1)
+        page = self.marionette.absolute_url("frameset.html")
+        self.marionette.navigate(page)
+        self.assertEqual(self.marionette.get_url(), page)
+        self.assertEqual(self.history_length, 2)
+        frame = self.marionette.find_element(By.ID, "fifth")
+        self.marionette.switch_to_frame(frame)
+        link = self.marionette.find_element(By.ID, "linkId")
+        link.click()
+
+        # We cannot use get_url() to wait until the target page has been loaded,
+        # because it will return the URL of the top browsing context and doesn't
+        # wait for the page load to be complete.
+        Wait(self.marionette, timeout=self.marionette.timeout.page_load).until(
+            expected.element_present(*test_element_locator),
+            message="Target element 'email' has not been found")
+        self.assertEqual(self.history_length, 3)
+
+        # Go back to the frame the click navigated away from
+        self.marionette.go_back()
+        self.assertEqual(self.marionette.get_url(), page)
+        with self.assertRaises(errors.NoSuchElementException):
+            self.marionette.find_element(*test_element_locator)
+
+        # Go back to the non-frameset page
+        self.marionette.switch_to_parent_frame()
+        self.marionette.go_back()
+        self.assertEqual(self.marionette.get_url(), self.test_page)
+
+        # Go forward to the frameset page
+        self.marionette.go_forward()
+        self.assertEqual(self.marionette.get_url(), page)
+
+        # Go forward to the frame the click navigated to
+        # TODO: See above for automatic browser context switches. Hard to do here
+        frame = self.marionette.find_element(By.ID, "fifth")
+        self.marionette.switch_to_frame(frame)
+        self.marionette.go_forward()
+        self.marionette.find_element(*test_element_locator)
+        self.assertEqual(self.marionette.get_url(), page)
+
+    def test_image_document_to_html(self):
+        test_pages = [
+            {"url": self.marionette.absolute_url('black.png')},
+            {"url": self.test_page},
+            {"url": self.marionette.absolute_url('white.png')},
+        ]
+        self.run_test(test_pages)
+
+    def test_image_document_to_image_document(self):
+        test_pages = [
+            {"url": self.marionette.absolute_url('black.png')},
+            {"url": self.marionette.absolute_url('white.png')},
+        ]
+        self.run_test(test_pages)
+
+    @run_if_e10s("Requires e10s mode enabled")
+    def test_remoteness_change(self):
+        # TODO: Verify that a remoteness change happened
+        # like: self.assertNotEqual(self.marionette.current_window_handle, self.new_tab)
+
+        # about:robots is always a non-remote page for now
+        test_pages = [
+            {"url": "about:robots"},
+            {"url": self.test_page},
+            {"url": "about:robots"},
+        ]
+        self.run_test(test_pages)
+
+    def test_navigate_to_requested_about_page_after_error_page(self):
+        test_pages = [
+            {"url": "about:neterror"},
+            {"url": self.marionette.absolute_url("test.html")},
+            {"url": "about:blocked"},
+        ]
+        self.run_test(test_pages)
+
+    def test_timeout_error(self):
+        urls = [
+            self.marionette.absolute_url('slow'),
+            self.test_page,
+            self.marionette.absolute_url('slow'),
+        ]
+
+        # First, load all pages completely to get them added to the cache
+        for index, url in enumerate(urls):
+            self.marionette.navigate(url)
+            self.assertEqual(url, self.marionette.get_url())
+            self.assertEqual(self.history_length, index + 1)
+
+        self.marionette.go_back()
+        self.assertEqual(urls[1], self.marionette.get_url())
+
+        # Force triggering a timeout error
+        self.marionette.timeout.page_load = 0.1
+        with self.assertRaises(errors.TimeoutException):
+            self.marionette.go_back()
+        self.assertEqual(urls[0], self.marionette.get_url())
+        self.marionette.timeout.page_load = 300000
+
+        self.marionette.go_forward()
+        self.assertEqual(urls[1], self.marionette.get_url())
+
+        # Force triggering a timeout error
+        self.marionette.timeout.page_load = 0.1
+        with self.assertRaises(errors.TimeoutException):
+            self.marionette.go_forward()
+        self.assertEqual(urls[2], self.marionette.get_url())
+        self.marionette.timeout.page_load = 300000
+
+    def test_certificate_error(self):
+        test_pages = [
+            {"url": self.fixtures.where_is("/test.html", on="https"),
+             "error": errors.InsecureCertificateException},
+            {"url": self.test_page},
+            {"url": self.fixtures.where_is("/test.html", on="https"),
+             "error": errors.InsecureCertificateException},
+        ]
+        self.run_test(test_pages)
+
+
 class TestNavigate(WindowManagerMixin, MarionetteTestCase):
 
     def setUp(self):
         super(TestNavigate, self).setUp()
 
         self.marionette.navigate("about:")
         self.test_doc = self.marionette.absolute_url("test.html")
         self.iframe_doc = self.marionette.absolute_url("test_iframe.html")
 
     def tearDown(self):
-        self.close_all_windows()
+        self.marionette.timeout.reset()
+        self.close_all_tabs()
 
         super(TestNavigate, self).tearDown()
 
     @property
     def location_href(self):
         # Windows 8 has recently seen a proliferation of intermittent
         # test failures to do with failing to compare "about:blank" ==
         # u"about:blank". For the sake of consistenty, we encode the
@@ -47,21 +259,16 @@ class TestNavigate(WindowManagerMixin, M
 
     def test_set_location_through_execute_script(self):
         self.marionette.execute_script(
             "window.location.href = '%s'" % self.test_doc)
         Wait(self.marionette).until(
             lambda _: self.test_doc == self.location_href)
         self.assertEqual("Marionette Test", self.marionette.title)
 
-    def test_navigate(self):
-        self.marionette.navigate(self.test_doc)
-        self.assertNotEqual("about:", self.location_href)
-        self.assertEqual("Marionette Test", self.marionette.title)
-
     def test_navigate_chrome_error(self):
         with self.marionette.using_context("chrome"):
             self.assertRaises(errors.UnsupportedOperationException,
                               self.marionette.navigate, "about:blank")
             self.assertRaises(errors.UnsupportedOperationException, self.marionette.go_back)
             self.assertRaises(errors.UnsupportedOperationException, self.marionette.go_forward)
             self.assertRaises(errors.UnsupportedOperationException, self.marionette.refresh)
 
@@ -73,43 +280,16 @@ class TestNavigate(WindowManagerMixin, M
         self.assertEqual(self.iframe_doc, self.marionette.get_url())
 
     def test_get_current_url(self):
         self.marionette.navigate(self.test_doc)
         self.assertEqual(self.test_doc, self.marionette.get_url())
         self.marionette.navigate("about:blank")
         self.assertEqual("about:blank", self.marionette.get_url())
 
-    # TODO(ato): Remove wait conditions when fixing bug 1330348
-    def test_go_back(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()
-        Wait(self.marionette).until(lambda m: self.location_href == self.test_doc)
-        self.assertNotEqual("about:blank", self.location_href)
-        self.assertEqual("Marionette Test", self.marionette.title)
-
-    # TODO(ato): Remove wait conditions when fixing bug 1330348
-    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()
-        Wait(self.marionette).until(lambda m: self.location_href == self.test_doc)
-        self.assertEqual(self.test_doc, self.location_href)
-        self.assertEqual("Marionette Test", self.marionette.title)
-        self.marionette.go_forward()
-        Wait(self.marionette).until(lambda m: self.location_href == "about:blank")
-        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"))
@@ -132,46 +312,39 @@ class TestNavigate(WindowManagerMixin, M
         self.marionette.navigate(frame_html)
         self.marionette.find_element(By.NAME, "third")
 
     @skip_if_mobile("Bug 1323755 - Socket timeout")
     def test_invalid_protocol(self):
         with self.assertRaises(errors.MarionetteException):
             self.marionette.navigate("thisprotocoldoesnotexist://")
 
-    def test_should_navigate_to_requested_about_page(self):
-        self.marionette.navigate("about:neterror")
-        self.assertEqual(self.marionette.get_url(), "about:neterror")
-        self.marionette.navigate(self.marionette.absolute_url("test.html"))
-        self.marionette.navigate("about:blocked")
-        self.assertEqual(self.marionette.get_url(), "about:blocked")
-
     def test_find_element_state_complete(self):
         self.marionette.navigate(self.test_doc)
         state = self.marionette.execute_script(
             "return window.document.readyState")
         self.assertEqual("complete", state)
         self.assertTrue(self.marionette.find_element(By.ID, "mozLink"))
 
     def test_error_when_exceeding_page_load_timeout(self):
+        self.marionette.timeout.page_load = 0.1
         with self.assertRaises(errors.TimeoutException):
-            self.marionette.timeout.page_load = 0.1
             self.marionette.navigate(self.marionette.absolute_url("slow"))
-            self.marionette.find_element(By.TAG_NAME, "p")
 
-    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_navigate_to_same_image_document_twice(self):
+        self.marionette.navigate(self.fixtures.where_is("black.png"))
+        self.assertIn("black.png", self.marionette.title)
+        self.marionette.navigate(self.fixtures.where_is("black.png"))
+        self.assertIn("black.png", self.marionette.title)
 
-    def test_fragment(self):
+    def test_navigate_hash_change(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.marionette.navigate("{}#foo".format(doc))
         self.assertTrue(self.marionette.execute_script(
             "return window.visited", sandbox=None))
 
     @skip_if_mobile("Bug 1334095 - Timeout: No new tab has been opened")
     def test_about_blank_for_new_docshell(self):
         """ Bug 1312674 - Hang when loading about:blank for a new docshell."""
         def open_with_link():
             link = self.marionette.find_element(By.ID, "new-blank-tab")
@@ -182,33 +355,16 @@ class TestNavigate(WindowManagerMixin, M
         new_tab = self.open_tab(trigger=open_with_link)
         self.marionette.switch_to_window(new_tab)
         self.assertEqual(self.marionette.get_url(), "about:blank")
 
         self.marionette.navigate('about:blank')
         self.marionette.close()
         self.marionette.switch_to_window(self.start_window)
 
-    def test_error_on_tls_navigation(self):
-        self.assertRaises(errors.InsecureCertificateException,
-                          self.marionette.navigate, self.fixtures.where_is("/test.html", on="https"))
-
-    def test_html_document_to_image_document(self):
-        self.marionette.navigate(self.fixtures.where_is("test.html"))
-        self.marionette.navigate(self.fixtures.where_is("white.png"))
-        self.assertIn("white.png", self.marionette.title)
-
-    def test_image_document_to_image_document(self):
-        self.marionette.navigate(self.fixtures.where_is("test.html"))
-
-        self.marionette.navigate(self.fixtures.where_is("white.png"))
-        self.assertIn("white.png", self.marionette.title)
-        self.marionette.navigate(self.fixtures.where_is("black.png"))
-        self.assertIn("black.png", self.marionette.title)
-
     @run_if_manage_instance("Only runnable if Marionette manages the instance")
     @skip_if_mobile("Bug 1322993 - Missing temporary folder")
     def test_focus_after_navigation(self):
         self.marionette.quit()
         self.marionette.start_session()
 
         self.marionette.navigate(inline("<input autofocus>"))
         active_el = self.marionette.execute_script("return document.activeElement")
--- a/testing/marionette/harness/marionette_harness/www/frameset.html
+++ b/testing/marionette/harness/marionette_harness/www/frameset.html
@@ -3,12 +3,12 @@
     <title>Unique title</title>
   </head>
 <frameset cols="*, *, *, *, *, *, *">
     <frame name="first" src="page/1"/>
     <frame name="second" src="page/2?title=Fish"/>
     <frame name="third" src="formPage.html"/>
     <frame name="fourth" src="framesetPage2.html"/>
     <frame id="fifth" src="xhtmlTest.html"/>
-    <frame id="sixth" src="iframes.html"/>
+    <frame id="sixth" src="test_iframe.html"/>
     <frame id="sixth.iframe1" src="page/3"/>
 </frameset>
 </html>
\ No newline at end of file
--- a/testing/marionette/harness/marionette_harness/www/xhtmlTest.html
+++ b/testing/marionette/harness/marionette_harness/www/xhtmlTest.html
@@ -5,17 +5,17 @@
         - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
 <head>
     <title>XHTML Test Page</title>
 </head>
 <body>
 <div class="navigation">
     <p><a href="resultPage.html" target="result" name="windowOne">Open new window</a></p>
     <p><a href="iframes.html" target="_blank" name="windowTwo">Create a new anonymous window</a></p>
-    <p><a href="iframes.html" name="sameWindow">Open page with iframes in same window</a></p>
+    <p><a href="test_iframe.html" name="sameWindow">Open page with iframes in same window</a></p>
     <p><a href="javascriptPage.html" target="result" name="windowThree">Open a window with a close button</a></p>
 </div>
 
 <a name="notext"><b></b></a>
 
 <div class="content">
     <h1 class="header">XHTML Might Be The Future</h1>
 
--- a/testing/marionette/listener.js
+++ b/testing/marionette/listener.js
@@ -217,17 +217,16 @@ function addMessageListenerId(messageNam
 function removeMessageListenerId(messageName, handler) {
   removeMessageListener(messageName + listenerId, handler);
 }
 
 var getTitleFn = dispatch(getTitle);
 var getPageSourceFn = dispatch(getPageSource);
 var getActiveElementFn = dispatch(getActiveElement);
 var clickElementFn = dispatch(clickElement);
-var goBackFn = dispatch(goBack);
 var getElementAttributeFn = dispatch(getElementAttribute);
 var getElementPropertyFn = dispatch(getElementProperty);
 var getElementTextFn = dispatch(getElementText);
 var getElementTagNameFn = dispatch(getElementTagName);
 var getElementRectFn = dispatch(getElementRect);
 var isElementEnabledFn = dispatch(isElementEnabled);
 var getCurrentUrlFn = dispatch(getCurrentUrl);
 var findElementContentFn = dispatch(findElementContent);
@@ -266,17 +265,17 @@ function startListeners() {
   addMessageListenerId("Marionette:actionChain", actionChainFn);
   addMessageListenerId("Marionette:multiAction", multiActionFn);
   addMessageListenerId("Marionette:get", get);
   addMessageListenerId("Marionette:pollForReadyState", pollForReadyState);
   addMessageListenerId("Marionette:cancelRequest", cancelRequest);
   addMessageListenerId("Marionette:getCurrentUrl", getCurrentUrlFn);
   addMessageListenerId("Marionette:getTitle", getTitleFn);
   addMessageListenerId("Marionette:getPageSource", getPageSourceFn);
-  addMessageListenerId("Marionette:goBack", goBackFn);
+  addMessageListenerId("Marionette:goBack", goBack);
   addMessageListenerId("Marionette:goForward", goForward);
   addMessageListenerId("Marionette:refresh", refresh);
   addMessageListenerId("Marionette:findElementContent", findElementContentFn);
   addMessageListenerId("Marionette:findElementsContent", findElementsContentFn);
   addMessageListenerId("Marionette:getActiveElement", getActiveElementFn);
   addMessageListenerId("Marionette:clickElement", clickElementFn);
   addMessageListenerId("Marionette:getElementAttribute", getElementAttributeFn);
   addMessageListenerId("Marionette:getElementProperty", getElementPropertyFn);
@@ -371,17 +370,17 @@ function deleteSession(msg) {
   removeMessageListenerId("Marionette:actionChain", actionChainFn);
   removeMessageListenerId("Marionette:multiAction", multiActionFn);
   removeMessageListenerId("Marionette:get", get);
   removeMessageListenerId("Marionette:pollForReadyState", pollForReadyState);
   removeMessageListenerId("Marionette:cancelRequest", cancelRequest);
   removeMessageListenerId("Marionette:getTitle", getTitleFn);
   removeMessageListenerId("Marionette:getPageSource", getPageSourceFn);
   removeMessageListenerId("Marionette:getCurrentUrl", getCurrentUrlFn);
-  removeMessageListenerId("Marionette:goBack", goBackFn);
+  removeMessageListenerId("Marionette:goBack", goBack);
   removeMessageListenerId("Marionette:goForward", goForward);
   removeMessageListenerId("Marionette:refresh", refresh);
   removeMessageListenerId("Marionette:findElementContent", findElementContentFn);
   removeMessageListenerId("Marionette:findElementsContent", findElementsContentFn);
   removeMessageListenerId("Marionette:getActiveElement", getActiveElementFn);
   removeMessageListenerId("Marionette:clickElement", clickElementFn);
   removeMessageListenerId("Marionette:getElementAttribute", getElementAttributeFn);
   removeMessageListenerId("Marionette:getElementProperty", getElementPropertyFn);
@@ -891,23 +890,25 @@ function multiAction(args, maxLen) {
  * 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.
  *
  * @param {function=} cleanupCallback
  *     Callback to execute when registered event handlers or observer notifications
  *     have to be cleaned-up.
  * @param {number} command_id
  *     ID of the currently handled message between the driver and listener.
+ * @param {string=} lastSeenURL
+ *     Last URL as seen before the navigation request got triggered.
  * @param {number} pageTimeout
  *     Timeout in seconds the method has to wait for the page being finished loading.
  * @param {number} startTime
  *     Unix timestap when the navitation request got triggred.
  */
 function pollForReadyState(msg) {
-  let {cleanupCallback, command_id, pageTimeout, startTime} = msg.json;
+  let {cleanupCallback, command_id, lastSeenURL, pageTimeout, startTime} = msg.json;
 
   if (typeof startTime == "undefined") {
     startTime = new Date().getTime();
   }
 
   if (typeof cleanupCallback == "undefined") {
     cleanupCallback = () => {};
   }
@@ -915,18 +916,26 @@ function pollForReadyState(msg) {
   let endTime = startTime + pageTimeout;
 
   let checkLoad = () => {
     navTimer.cancel();
 
     let doc = curContainer.frame.document;
 
     if (pageTimeout === null || new Date().getTime() <= endTime) {
+      // Under some conditions (eg. for error pages) the pagehide event is fired
+      // even with a readyState complete for the formerly loaded page.
+      // To prevent race conditition for goBack and goForward we have to wait
+      // until the last seen page has been fully unloaded.
+      // TODO: Bug 1333458 has to improve this.
+      if (!doc.location || lastSeenURL && doc.location.href === lastSeenURL) {
+        navTimer.initWithCallback(checkLoad, 100, Ci.nsITimer.TYPE_ONE_SHOT);
+
       // document fully loaded
-      if (doc.readyState === "complete") {
+      } else if (doc.readyState === "complete") {
         cleanupCallback();
         sendOk(command_id);
 
       // document with an insecure cert
       } else if (doc.readyState === "interactive" &&
           doc.baseURI.startsWith("about:certerror")) {
         cleanupCallback();
         sendError(new InsecureCertificateError(), command_id);
@@ -1120,22 +1129,18 @@ function cancelRequest() {
   if (onDOMContentLoaded) {
     removeEventListener("DOMContentLoaded", onDOMContentLoaded, false);
   }
 }
 
 /**
  * Get URL of the top-level browsing context.
  */
-function getCurrentUrl(isB2G) {
-  if (isB2G) {
-    return curContainer.frame.location.href;
-  } else {
-    return content.location.href;
-  }
+function getCurrentUrl() {
+  return content.location.href;
 }
 
 /**
  * Get the title of the current browsing context.
  */
 function getTitle() {
   return curContainer.frame.top.document.title;
 }
@@ -1143,29 +1148,124 @@ function getTitle() {
 /**
  * Get source of the current browsing context's DOM.
  */
 function getPageSource() {
   return curContainer.frame.document.documentElement.outerHTML;
 }
 
 /**
- * Cause the browser to traverse one step backward in the joint history
- * of the current top-level browsing context.
+ * Wait for the current page to be unloaded after a navigation got triggered.
+ *
+ * @param {function} trigger
+ *     Callback to execute which triggers a page navigation.
+ * @param {function} doneCallback
+ *     Callback to execute when the current page has been unloaded.
+ *
+ *     It receives a dictionary with the following items as argument:
+ *         loading - Flag if a page load will follow.
+ *         lastSeenURL - Last seen URL before the navigation request.
+ *         startTime - Time when the navigation request has been triggered.
  */
-function goBack() {
-  curContainer.frame.history.back();
+function waitForPageUnloaded(trigger, doneCallback) {
+  let currentURL = curContainer.frame.location.href;
+  let start = new Date().getTime();
+
+  function handleEvent(event) {
+    // In case of a remoteness change it can happen that we are no longer able
+    // to access the document's location. In those cases ignore the event,
+    // but keep the code waiting, and assume in the driver that waiting for the
+    // page load is necessary. Bug 1333458 should improve things.
+    if (typeof event.originalTarget.location == "undefined") {
+      return;
+    }
+
+    switch (event.type) {
+      case "hashchange":
+        removeEventListener("hashchange", handleEvent);
+        removeEventListener("pagehide", handleEvent);
+        removeEventListener("unload", handleEvent);
+
+        doneCallback({loading: false, lastSeenURL: currentURL});
+        break;
+
+      case "pagehide":
+      case "unload":
+        if (event.originalTarget === curContainer.frame.document) {
+          removeEventListener("hashchange", handleEvent);
+          removeEventListener("pagehide", handleEvent);
+          removeEventListener("unload", handleEvent);
+
+          doneCallback({loading: true, lastSeenURL: currentURL, startTime: start});
+        }
+        break;
+    }
+  }
+
+  addEventListener("hashchange", handleEvent, false);
+  addEventListener("pagehide", handleEvent, false);
+  addEventListener("unload", handleEvent, false);
+
+  trigger();
 }
 
 /**
- * Go forward in history
+ * Cause the browser to traverse one step backward in the joint history
+ * of the current browsing context.
+ *
+ * @param {number} command_id
+ *     ID of the currently handled message between the driver and listener.
+ * @param {number} pageTimeout
+ *     Timeout in milliseconds the method has to wait for the page being finished loading.
+ */
+function goBack(msg) {
+  let {command_id, pageTimeout} = msg.json;
+
+  waitForPageUnloaded(() => {
+      curContainer.frame.history.back();
+    }, pageLoadStatus => {
+    if (pageLoadStatus.loading) {
+      pollForReadyState({json: {
+        command_id: command_id,
+        lastSeenURL: pageLoadStatus.lastSeenURL,
+        pageTimeout: pageTimeout,
+        startTime: pageLoadStatus.startTime,
+      }});
+    } else {
+      sendOk(command_id);
+    }
+  });
+}
+
+/**
+ * Cause the browser to traverse one step forward in the joint history
+ * of the current browsing context.
+ *
+ * @param {number} command_id
+ *     ID of the currently handled message between the driver and listener.
+ * @param {number} pageTimeout
+ *     Timeout in milliseconds the method has to wait for the page being finished loading.
  */
 function goForward(msg) {
-  curContainer.frame.history.forward();
-  sendOk(msg.json.command_id);
+  let {command_id, pageTimeout} = msg.json;
+
+  waitForPageUnloaded(() => {
+    curContainer.frame.history.forward();
+  }, pageLoadStatus => {
+    if (pageLoadStatus.loading) {
+      pollForReadyState({json: {
+        command_id: command_id,
+        lastSeenURL: pageLoadStatus.lastSeenURL,
+        pageTimeout: pageTimeout,
+        startTime: pageLoadStatus.startTime,
+      }});
+    } else {
+      sendOk(command_id);
+    }
+  });
 }
 
 /**
  * Refresh the page
  */
 function refresh(msg) {
   let command_id = msg.json.command_id;
   curContainer.frame.location.reload(true);