Bug 1171256 - Add an API similar to Chrome's webNavigation (r=Mossop)

This commit is contained in:
Bill McCloskey 2015-06-03 14:53:12 -07:00
parent 4decb9f9e6
commit f7a876cfcc
8 changed files with 433 additions and 0 deletions

View File

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

View File

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

View File

@ -13,6 +13,8 @@ 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',

View File

@ -3,6 +3,9 @@ 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
@ -23,6 +26,7 @@ support-files =
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]

View File

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

View File

@ -0,0 +1,9 @@
<!DOCTYPE HTML>
<html>
<body>
<iframe src="file_WebNavigation_page2.html" width="200" height="200"></iframe>
</body>
</html>

View File

@ -0,0 +1,7 @@
<!DOCTYPE HTML>
<html>
<body>
</body>
</html>

View File

@ -0,0 +1,9 @@
<!DOCTYPE HTML>
<html>
<body>
<a id="elt" href="file_WebNavigation_page3.html#ref">click me</a>
</body>
</html>