зеркало из https://github.com/mozilla/gecko-dev.git
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:
Родитель
2238229a3b
Коммит
175975b04b
|
@ -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]
|
||||
|
|
Загрузка…
Ссылка в новой задаче