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
--- 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);