author | Bill McCloskey <billm@mozilla.com> |
Wed, 03 Jun 2015 14:53:12 -0700 | |
changeset 249808 | 7eca38a74be8389935720bd096d6c23a01268a21 |
parent 249807 | f8f4a6c0654967a5514f78e47480356bd642ec8b |
child 249809 | fe6e29cd1894d960b223a4291ffb992372c3b467 |
push id | 61342 |
push user | wmccloskey@mozilla.com |
push date | Sat, 20 Jun 2015 00:36:11 +0000 |
treeherder | mozilla-inbound@ec2835827e23 [default view] [failures only] |
perfherder | [talos] [build metrics] [platform microbench] (compared to previous push) |
reviewers | Mossop |
bugs | 1171256 |
milestone | 41.0a1 |
first release with | nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
|
last release without | nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
|
new file mode 100644 --- /dev/null +++ b/toolkit/modules/addons/WebNavigation.jsm @@ -0,0 +1,157 @@ +/* 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 EXPORTED_SYMBOLS = ["WebNavigation"]; + +const Ci = Components.interfaces; +const Cc = Components.classes; +const Cu = Components.utils; + +Cu.import("resource://gre/modules/XPCOMUtils.jsm"); +Cu.import("resource://gre/modules/Services.jsm"); + +// TODO: +// Transition types and qualifiers +// onReferenceFragmentUpdated also triggers for pushState +// getFrames, getAllFrames +// onCreatedNavigationTarget, onHistoryStateUpdated + +let Manager = { + listeners: new Map(), + + init() { + Services.mm.addMessageListener("Extension:DOMContentLoaded", this); + Services.mm.addMessageListener("Extension:StateChange", this); + Services.mm.addMessageListener("Extension:LocationChange", this); + Services.mm.loadFrameScript("resource://gre/modules/WebNavigationContent.js", true); + }, + + uninit() { + Services.mm.removeMessageListener("Extension:StateChange", this); + Services.mm.removeMessageListener("Extension:LocationChange", this); + Services.mm.removeMessageListener("Extension:DOMContentLoaded", this); + Services.mm.removeDelayedFrameScript("resource://gre/modules/WebNavigationContent.js"); + Services.mm.broadcastAsyncMessage("Extension:DisableWebNavigation"); + }, + + addListener(type, listener) { + if (this.listeners.size == 0) { + this.init(); + } + + if (!this.listeners.has(type)) { + this.listeners.set(type, new Set()); + } + let listeners = this.listeners.get(type); + listeners.add(listener); + }, + + removeListener(type, listener) { + let listeners = this.listeners.get(type); + if (!listeners) { + return; + } + listeners.delete(listener); + if (listeners.size == 0) { + this.listeners.delete(type); + } + + if (this.listeners.size == 0) { + this.uninit(); + } + }, + + receiveMessage({name, data, target}) { + switch (name) { + case "Extension:StateChange": + this.onStateChange(target, data); + break; + + case "Extension:LocationChange": + this.onLocationChange(target, data); + break; + + case "Extension:DOMContentLoaded": + this.onLoad(target, data); + break; + } + }, + + onStateChange(browser, data) { + let stateFlags = data.stateFlags; + if (stateFlags & Ci.nsIWebProgressListener.STATE_IS_WINDOW) { + let url = data.requestURL; + if (stateFlags & Ci.nsIWebProgressListener.STATE_START) { + this.fire("onBeforeNavigate", browser, data, {url}); + } else if (stateFlags & Ci.nsIWebProgressListener.STATE_STOP) { + if (Components.isSuccessCode(data.status)) { + this.fire("onCompleted", browser, data, {url}); + } else { + let error = `Error code ${data.status}`; + this.fire("onErrorOccurred", browser, data, {error, url}); + } + } + } + }, + + onLocationChange(browser, data) { + let url = data.location; + if (data.flags & Ci.nsIWebProgressListener.LOCATION_CHANGE_SAME_DOCUMENT) { + this.fire("onReferenceFragmentUpdated", browser, data, {url}); + } else { + this.fire("onCommitted", browser, data, {url}); + } + }, + + onLoad(browser, data) { + this.fire("onDOMContentLoaded", browser, data, {url: data.url}); + }, + + fire(type, browser, data, extra) { + let listeners = this.listeners.get(type); + if (!listeners) { + return; + } + + let details = { + browser, + windowId: data.windowId, + }; + + if (data.parentWindowId) { + details.parentWindowId = data.parentWindowId; + } + + for (let prop in extra) { + details[prop] = extra[prop]; + } + + for (let listener of listeners) { + listener(details); + } + }, +}; + +const EVENTS = [ + "onBeforeNavigate", + "onCommitted", + "onDOMContentLoaded", + "onCompleted", + "onErrorOccurred", + "onReferenceFragmentUpdated", + + //"onCreatedNavigationTarget", + //"onHistoryStateUpdated", +]; + +let WebNavigation = {}; + +for (let event of EVENTS) { + WebNavigation[event] = { + addListener: Manager.addListener.bind(Manager, event), + removeListener: Manager.removeListener.bind(Manager, event), + } +}
new file mode 100644 --- /dev/null +++ b/toolkit/modules/addons/WebNavigationContent.js @@ -0,0 +1,105 @@ +const Ci = Components.interfaces; + +function getWindowId(window) +{ + return window.QueryInterface(Ci.nsIInterfaceRequestor) + .getInterface(Ci.nsIDOMWindowUtils) + .outerWindowID; +} + +function getParentWindowId(window) +{ + return getWindowId(window.parent); +} + +function loadListener(event) +{ + let document = event.target; + let window = document.defaultView; + let url = document.documentURI; + let windowId = getWindowId(window); + let parentWindowId = getParentWindowId(window); + sendAsyncMessage("Extension:DOMContentLoaded", {windowId, parentWindowId, url}); +} + +addEventListener("DOMContentLoaded", loadListener); +addMessageListener("Extension:DisableWebNavigation", () => { + removeEventListener("DOMContentLoaded", loadListener); +}); + +let WebProgressListener = { + init: function() { + let webProgress = docShell.QueryInterface(Ci.nsIInterfaceRequestor) + .getInterface(Ci.nsIWebProgress); + webProgress.addProgressListener(this, Ci.nsIWebProgress.NOTIFY_STATE_WINDOW | + Ci.nsIWebProgress.NOTIFY_LOCATION); + }, + + uninit() { + if (!docShell) { + return; + } + let webProgress = docShell.QueryInterface(Ci.nsIInterfaceRequestor) + .getInterface(Ci.nsIWebProgress); + webProgress.removeProgressListener(this); + }, + + onStateChange: function onStateChange(webProgress, request, stateFlags, status) { + let data = { + requestURL: request.QueryInterface(Ci.nsIChannel).URI.spec, + windowId: webProgress.DOMWindowID, + parentWindowId: getParentWindowId(webProgress.DOMWindow), + status, + stateFlags, + }; + sendAsyncMessage("Extension:StateChange", data); + + if (webProgress.DOMWindow.top != webProgress.DOMWindow) { + let webNav = webProgress.QueryInterface(Ci.nsIWebNavigation); + if (!webNav.canGoBack) { + // For some reason we don't fire onLocationChange for the + // initial navigation of a sub-frame. So we need to simulate + // it here. + let data = { + location: request.QueryInterface(Ci.nsIChannel).URI.spec, + windowId: webProgress.DOMWindowID, + parentWindowId: getParentWindowId(webProgress.DOMWindow), + flags: 0, + }; + sendAsyncMessage("Extension:LocationChange", data); + } + } + }, + + onLocationChange: function onLocationChange(webProgress, request, locationURI, flags) { + let data = { + location: locationURI ? locationURI.spec : "", + windowId: webProgress.DOMWindowID, + parentWindowId: getParentWindowId(webProgress.DOMWindow), + flags, + }; + sendAsyncMessage("Extension:LocationChange", data); + }, + + QueryInterface: function QueryInterface(aIID) { + if (aIID.equals(Ci.nsIWebProgressListener) || + aIID.equals(Ci.nsISupportsWeakReference) || + aIID.equals(Ci.nsISupports)) { + return this; + } + + throw Components.results.NS_ERROR_NO_INTERFACE; + } +}; + +let disabled = false; +WebProgressListener.init(); +addEventListener("unload", () => { + if (!disabled) { + WebProgressListener.uninit(); + } +}); +addMessageListener("Extension:DisableWebNavigation", () => { + disabled = true; + WebProgressListener.uninit(); +});
--- a/toolkit/modules/moz.build +++ b/toolkit/modules/moz.build @@ -8,16 +8,18 @@ XPCSHELL_TESTS_MANIFESTS += ['tests/xpcs BROWSER_CHROME_MANIFESTS += ['tests/browser/browser.ini'] MOCHITEST_MANIFESTS += ['tests/mochitest/mochitest.ini'] MOCHITEST_CHROME_MANIFESTS += ['tests/chrome/chrome.ini'] SPHINX_TREES['toolkit_modules'] = 'docs' EXTRA_JS_MODULES += [ 'addons/MatchPattern.jsm', + 'addons/WebNavigation.jsm', + 'addons/WebNavigationContent.js', 'addons/WebRequest.jsm', 'addons/WebRequestCommon.jsm', 'addons/WebRequestContent.js', 'Battery.jsm', 'BinarySearch.jsm', 'BrowserUtils.jsm', 'CertUtils.jsm', 'CharsetMenu.jsm',
--- a/toolkit/modules/tests/browser/browser.ini +++ b/toolkit/modules/tests/browser/browser.ini @@ -1,13 +1,16 @@ [DEFAULT] support-files = dummy_page.html metadata_*.html testremotepagemanager.html + file_WebNavigation_page1.html + file_WebNavigation_page2.html + file_WebNavigation_page3.html file_WebRequest_page1.html file_WebRequest_page2.html file_image_good.png file_image_bad.png file_image_redirect.png file_style_good.css file_style_bad.css file_style_redirect.css @@ -18,15 +21,16 @@ support-files = WebRequest_dynamic.sjs [browser_Battery.js] [browser_Deprecated.js] [browser_Finder.js] skip-if = e10s # Bug ?????? - test already uses content scripts, but still fails only under e10s. [browser_Geometry.js] [browser_InlineSpellChecker.js] +[browser_WebNavigation.js] [browser_WebRequest.js] [browser_WebRequest_cookies.js] [browser_WebRequest_filtering.js] [browser_PageMetadata.js] [browser_RemotePageManager.js] [browser_RemoteWebNavigation.js] [browser_Troubleshoot.js]
new file mode 100644 --- /dev/null +++ b/toolkit/modules/tests/browser/browser_WebNavigation.js @@ -0,0 +1,140 @@ +"use strict"; + +const { interfaces: Ci, classes: Cc, utils: Cu, results: Cr } = Components; + +let {WebNavigation} = Cu.import("resource://gre/modules/WebNavigation.jsm", {}); + +const BASE = "http://example.com/browser/toolkit/modules/tests/browser"; +const URL = BASE + "/file_WebNavigation_page1.html"; +const FRAME = BASE + "/file_WebNavigation_page2.html"; +const FRAME2 = BASE + "/file_WebNavigation_page3.html"; + +const EVENTS = [ + "onBeforeNavigate", + "onCommitted", + "onDOMContentLoaded", + "onCompleted", + "onErrorOccurred", + "onReferenceFragmentUpdated", +]; + +const REQUIRED = [ + "onBeforeNavigate", + "onCommitted", + "onDOMContentLoaded", + "onCompleted", +]; + +let expectedBrowser; +let received = []; +let completedResolve; +let waitingURL, waitingEvent; +let rootWindowID; + +function gotEvent(event, details) +{ + if (!details.url.startsWith(BASE)) { + return; + } + info(`Got ${event} ${details.url} ${details.windowId} ${details.parentWindowId}`); + + is(details.browser, expectedBrowser, "correct <browser> element"); + + received.push({url: details.url, event}); + + if (typeof(rootWindowID) == "undefined") { + rootWindowID = details.windowId; + } + + if (details.url == URL) { + is(details.windowId, rootWindowID, "root window ID correct"); + } else { + is(details.parentWindowId, rootWindowID, "parent window ID correct"); + isnot(details.windowId, rootWindowID, "window ID probably okay"); + } + + isnot(details.windowId, undefined); + isnot(details.parentWindowId, undefined); + + if (details.url == waitingURL && event == waitingEvent) { + completedResolve(); + } +} + +function loadViaFrameScript(url, event, script) +{ + // Loading via a frame script ensures that the chrome process never + // "gets ahead" of frame scripts in non-e10s mode. + received = []; + waitingURL = url; + waitingEvent = event; + expectedBrowser.messageManager.loadFrameScript("data:," + script, false); + return new Promise(resolve => { completedResolve = resolve; }); +} + +add_task(function* webnav_ordering() { + let listeners = {}; + for (let event of EVENTS) { + listeners[event] = gotEvent.bind(null, event); + WebNavigation[event].addListener(listeners[event]); + } + + gBrowser.selectedTab = gBrowser.addTab(); + let browser = gBrowser.selectedBrowser; + expectedBrowser = browser; + + yield BrowserTestUtils.browserLoaded(browser); + + yield loadViaFrameScript(URL, "onCompleted", `content.location = "${URL}";`); + + function checkRequired(url) { + for (let event of REQUIRED) { + let found = false; + for (let r of received) { + if (r.url == url && r.event == event) { + found = true; + } + } + ok(found, `Received event ${event} from ${url}`); + } + } + + checkRequired(URL); + checkRequired(FRAME); + + function checkBefore(action1, action2) { + function find(action) { + for (let i = 0; i < received.length; i++) { + if (received[i].url == action.url && received[i].event == action.event) { + return i; + } + } + return -1; + } + + let index1 = find(action1); + let index2 = find(action2); + ok(index1 != -1, `Action ${JSON.stringify(action1)} happened`); + ok(index2 != -1, `Action ${JSON.stringify(action2)} happened`); + ok(index1 < index2, `Action ${JSON.stringify(action1)} happened before ${JSON.stringify(action2)}`); + } + + checkBefore({url: URL, event: "onCommitted"}, {url: FRAME, event: "onBeforeNavigate"}); + checkBefore({url: FRAME, event: "onCompleted"}, {url: URL, event: "onCompleted"}); + + yield loadViaFrameScript(FRAME2, "onCompleted", `content.frames[0].location = "${FRAME2}";`); + + checkRequired(FRAME2); + + yield loadViaFrameScript(FRAME2 + "#ref", "onReferenceFragmentUpdated", + "content.frames[0].document.getElementById('elt').click();"); + + info("Received onReferenceFragmentUpdated from FRAME2"); + + gBrowser.removeCurrentTab(); + + for (let event of EVENTS) { + WebNavigation[event].removeListener(listeners[event]); + } +}); +
new file mode 100644 --- /dev/null +++ b/toolkit/modules/tests/browser/file_WebNavigation_page1.html @@ -0,0 +1,9 @@ +<!DOCTYPE HTML> + +<html> +<body> + +<iframe src="file_WebNavigation_page2.html" width="200" height="200"></iframe> + +</body> +</html>