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

--HG--
extra : rebase_source : f9152a6623a25c17e10dc3bc6552b8e635c21317
This commit is contained in:
Andreas Tolfsen 2016-07-19 18:47:33 +01:00
Родитель 2238229a3b
Коммит 175975b04b
6 изменённых файлов: 285 добавлений и 51 удалений

Просмотреть файл

@ -2,11 +2,18 @@
# 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)
@ -60,7 +67,7 @@ class TestNavigate(MarionetteTestCase):
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)
@ -74,6 +81,8 @@ class TestNavigate(MarionetteTestCase):
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"))
@ -95,7 +104,7 @@ class TestNavigate(MarionetteTestCase):
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()
@ -132,6 +141,13 @@ class TestNavigate(MarionetteTestCase):
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")

Просмотреть файл

@ -25,6 +25,7 @@ marionette.jar:
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)

Просмотреть файл

@ -23,6 +23,7 @@ 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");
@ -881,43 +882,48 @@ 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.
*/
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();
}
@ -929,25 +935,38 @@ function pollForReadyState(msg, start, callback) {
*/
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;
@ -961,7 +980,7 @@ function get(msg) {
// 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
@ -979,16 +998,15 @@ function get(msg) {
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
@ -1012,29 +1030,36 @@ function get(msg) {
}
};
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) {
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);
}
if (msg.json.pageTimeout != null) {
navTimer.initWithCallback(timerFunc, 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();

Просмотреть файл

@ -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,
};
};

Просмотреть файл

@ -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();
});

Просмотреть файл

@ -10,3 +10,4 @@ skip-if = appname == "thunderbird"
[test_element.js]
[test_error.js]
[test_message.js]
[test_navigate.js]