Bug 1386977 - Handle popstate events for page loads. draft
authorHenrik Skupin <mail@hskupin.info>
Tue, 08 Aug 2017 19:48:35 +0200
changeset 643444 2e833b010d31aa0053237fb2a54b7c1a0e1ab593
parent 642518 a921bfb8a2cf3db4d9edebe9b35799a3f9d035da
child 725306 47aff867140e9f02bfff0acdd273d3c84b58b396
push id73103
push userbmo:hskupin@gmail.com
push dateWed, 09 Aug 2017 19:16:18 +0000
bugs1386977
milestone57.0a1
Bug 1386977 - Handle popstate events for page loads. In case of websites manipulating the browser's history via history.pushState there will be no usual page load events fired. Instead listeners for popstate events have to be used. When such an event occurs we can directly return because the browser will not load the underlying page. This only happens when navigating to another page first, or restarting Firefox. MozReview-Commit-ID: 3PceeYK9Co7
testing/marionette/harness/marionette_harness/tests/unit/test_navigation.py
testing/marionette/harness/marionette_harness/www/navigation_pushstate.html
testing/marionette/harness/marionette_harness/www/navigation_pushstate_target.html
testing/marionette/listener.js
--- a/testing/marionette/harness/marionette_harness/tests/unit/test_navigation.py
+++ b/testing/marionette/harness/marionette_harness/tests/unit/test_navigation.py
@@ -30,16 +30,17 @@ class BaseNavigationTestCase(WindowManag
         super(BaseNavigationTestCase, self).setUp()
 
         file_path = os.path.join(here, 'data', 'test.html').replace("\\", "/")
 
         self.test_page_file_url = "file:///{}".format(file_path)
         self.test_page_frameset = self.marionette.absolute_url("frameset.html")
         self.test_page_insecure = self.fixtures.where_is("test.html", on="https")
         self.test_page_not_remote = "about:robots"
+        self.test_page_push_state = self.marionette.absolute_url("navigation_pushstate.html")
         self.test_page_remote = self.marionette.absolute_url("test.html")
         self.test_page_slow_resource = self.marionette.absolute_url("slow_resource.html")
 
         if self.marionette.session_capabilities["platformName"] == "darwin":
             self.mod_key = Keys.META
         else:
             self.mod_key = Keys.CONTROL
 
@@ -208,16 +209,49 @@ class TestNavigate(BaseNavigationTestCas
     def test_navigate_hash_argument_differnt(self):
         test_page = "{}#Foo".format(inline("<p id=foo>"))
 
         self.marionette.navigate(test_page)
         self.marionette.find_element(By.ID, "foo")
         self.marionette.navigate(test_page.lower())
         self.marionette.find_element(By.ID, "foo")
 
+    def test_navigate_history_pushstate(self):
+        target_page = self.marionette.absolute_url("navigation_pushstate_target.html")
+
+        self.marionette.navigate(self.test_page_push_state)
+        self.marionette.find_element(By.ID, "forward").click()
+
+        # By using pushState() the URL is updated but the target page is not loaded
+        # and as such the element is not displayed
+        self.assertEqual(self.marionette.get_url(), target_page)
+        with self.assertRaises(errors.NoSuchElementException):
+            self.marionette.find_element(By.ID, "target")
+
+        self.marionette.go_back()
+        self.assertEqual(self.marionette.get_url(), self.test_page_push_state)
+
+        # The target page still gets not loaded
+        self.marionette.go_forward()
+        self.assertEqual(self.marionette.get_url(), target_page)
+        with self.assertRaises(errors.NoSuchElementException):
+            self.marionette.find_element(By.ID, "target")
+
+        # Navigating to a different page, and returning to the injected
+        # page, it will be loaded.
+        self.marionette.navigate(self.test_page_remote)
+        self.assertEqual(self.marionette.get_url(), self.test_page_remote)
+
+        self.marionette.go_back()
+        self.assertEqual(self.marionette.get_url(), target_page)
+        self.marionette.find_element(By.ID, "target")
+
+        self.marionette.go_back()
+        self.assertEqual(self.marionette.get_url(), self.test_page_push_state)
+
     @skip_if_mobile("Test file is only located on host machine")
     def test_navigate_file_url(self):
         self.marionette.navigate(self.test_page_file_url)
         self.marionette.find_element(By.ID, "file-url")
         self.marionette.navigate(self.test_page_remote)
 
     @run_if_e10s("Requires e10s mode enabled")
     @skip_if_mobile("Test file is only located on host machine")
@@ -574,16 +608,36 @@ class TestRefresh(BaseNavigationTestCase
         image = self.marionette.absolute_url('black.png')
 
         self.marionette.navigate(image)
         self.assertEqual(image, self.marionette.get_url())
 
         self.marionette.refresh()
         self.assertEqual(image, self.marionette.get_url())
 
+    def test_history_pushstate(self):
+        target_page = self.marionette.absolute_url("navigation_pushstate_target.html")
+
+        self.marionette.navigate(self.test_page_push_state)
+        self.marionette.find_element(By.ID, "forward").click()
+
+        # By using pushState() the URL is updated but the target page is not loaded
+        # and as such the element is not displayed
+        self.assertEqual(self.marionette.get_url(), target_page)
+        with self.assertRaises(errors.NoSuchElementException):
+            self.marionette.find_element(By.ID, "target")
+
+        # Refreshing the target page will trigger a full page load.
+        self.marionette.refresh()
+        self.assertEqual(self.marionette.get_url(), target_page)
+        self.marionette.find_element(By.ID, "target")
+
+        self.marionette.go_back()
+        self.assertEqual(self.marionette.get_url(), self.test_page_push_state)
+
     def test_timeout_error(self):
         slow_page = self.marionette.absolute_url("slow?delay=3")
 
         self.marionette.navigate(slow_page)
         self.assertEqual(slow_page, self.marionette.get_url())
 
         self.marionette.timeout.page_load = 0.5
         with self.assertRaises(errors.TimeoutException):
new file mode 100644
--- /dev/null
+++ b/testing/marionette/harness/marionette_harness/www/navigation_pushstate.html
@@ -0,0 +1,20 @@
+<!-- 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/. -->
+
+<!DOCTYPE html>
+<html>
+<head>
+  <title>Navigation by manipulating the browser history</title>
+  <script type="text/javascript">
+    function forward() {
+      var stateObj = { foo: "bar" };
+      history.pushState(stateObj, "", "navigation_pushstate_target.html");
+    }
+  </script>
+</head>
+
+<body>
+  <p>Navigate <a onclick="javascript:forward();" id="forward">forward</a></p>
+</body>
+</html>
new file mode 100644
--- /dev/null
+++ b/testing/marionette/harness/marionette_harness/www/navigation_pushstate_target.html
@@ -0,0 +1,13 @@
+<!-- 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/. -->
+
+<!DOCTYPE html>
+<html>
+<head>
+</head>
+
+<body>
+  <p id="target">Pushstate target</p>
+</body>
+</html>
--- a/testing/marionette/listener.js
+++ b/testing/marionette/listener.js
@@ -162,16 +162,17 @@ var loadListener = {
     if (timeout <= 0) {
       this.notify(this.timerPageLoad);
       return;
     }
 
     if (waitForUnloaded) {
       addEventListener("hashchange", this, false);
       addEventListener("pagehide", this, false);
+      addEventListener("popstate", this, false);
 
       // The events can only be received when the event listeners are
       // added to the currently selected frame.
       curContainer.frame.addEventListener("beforeunload", this);
       curContainer.frame.addEventListener("unload", this);
 
       Services.obs.addObserver(this, "outer-window-destroyed");
 
@@ -208,16 +209,17 @@ var loadListener = {
     }
 
     if (this.timerPageUnload) {
       this.timerPageUnload.cancel();
     }
 
     removeEventListener("hashchange", this);
     removeEventListener("pagehide", this);
+    removeEventListener("popstate", this);
     removeEventListener("DOMContentLoaded", this);
     removeEventListener("pageshow", this);
 
     // If the original content window, where the navigation was triggered,
     // doesn't exist anymore, exceptions can be silently ignored.
     try {
       curContainer.frame.removeEventListener("beforeunload", this);
       curContainer.frame.removeEventListener("unload", this);
@@ -257,23 +259,25 @@ var loadListener = {
         this.seenUnload = true;
         break;
 
       case "pagehide":
         this.seenUnload = true;
 
         removeEventListener("hashchange", this);
         removeEventListener("pagehide", this);
+        removeEventListener("popstate", this);
 
         // Now wait until the target page has been loaded
         addEventListener("DOMContentLoaded", this, false);
         addEventListener("pageshow", this, false);
         break;
 
       case "hashchange":
+      case "popstate":
         this.stop();
         sendOk(this.command_id);
         break;
 
       case "DOMContentLoaded":
       case "pageshow":
         this.handleReadyState(event.target.readyState,
             event.target.documentURI);