--- a/toolkit/components/extensions/ext-webNavigation.js
+++ b/toolkit/components/extensions/ext-webNavigation.js
@@ -74,16 +74,17 @@ extensions.registerSchemaAPI("webNavigat
return {
webNavigation: {
onBeforeNavigate: new WebNavigationEventManager(context, "onBeforeNavigate").api(),
onCommitted: new WebNavigationEventManager(context, "onCommitted").api(),
onDOMContentLoaded: new WebNavigationEventManager(context, "onDOMContentLoaded").api(),
onCompleted: new WebNavigationEventManager(context, "onCompleted").api(),
onErrorOccurred: new WebNavigationEventManager(context, "onErrorOccurred").api(),
onReferenceFragmentUpdated: new WebNavigationEventManager(context, "onReferenceFragmentUpdated").api(),
+ onHistoryStateUpdated: new WebNavigationEventManager(context, "onHistoryStateUpdated").api(),
onCreatedNavigationTarget: ignoreEvent(context, "webNavigation.onCreatedNavigationTarget"),
getAllFrames(details) {
let tab = TabManager.getTab(details.tabId);
if (!tab) {
return Promise.reject({message: `No tab found with tabId: ${details.tabId}`});
}
let {innerWindowID, messageManager} = tab.linkedBrowser;
--- a/toolkit/components/extensions/schemas/web_navigation.json
+++ b/toolkit/components/extensions/schemas/web_navigation.json
@@ -340,17 +340,16 @@
"tabId": {"type": "integer", "description": "The ID of the tab that replaced the old tab."},
"timeStamp": {"type": "number", "description": "The time when the replacement happened, in milliseconds since the epoch."}
}
}
]
},
{
"name": "onHistoryStateUpdated",
- "unsupported": true,
"type": "function",
"description": "Fired when the frame's history was updated to a new URL. All future events for that frame will use the updated URL.",
"filters": [
{
"name": "url",
"type": "array",
"items": { "$ref": "events.UrlFilter" },
"description": "Conditions that the URL being navigated to must satisfy. The 'schemes' and 'ports' fields of UrlFilter are ignored for this event."
--- a/toolkit/components/extensions/test/mochitest/test_ext_webnavigation.html
+++ b/toolkit/components/extensions/test/mochitest/test_ext_webnavigation.html
@@ -19,16 +19,17 @@ function backgroundScript() {
const EVENTS = [
"onBeforeNavigate",
"onCommitted",
"onDOMContentLoaded",
"onCompleted",
"onErrorOccurred",
"onReferenceFragmentUpdated",
+ "onHistoryStateUpdated",
];
let expectedTabId = -1;
function gotEvent(event, details) {
if (!details.url.startsWith(BASE)) {
return;
}
@@ -63,16 +64,17 @@ function backgroundScript() {
browser.test.sendMessage("ready", browser.webRequest.ResourceType);
}
const BASE = "http://mochi.test:8888/tests/toolkit/components/extensions/test/mochitest";
const URL = BASE + "/file_WebNavigation_page1.html";
const FRAME = BASE + "/file_WebNavigation_page2.html";
const FRAME2 = BASE + "/file_WebNavigation_page3.html";
+const FRAME_PUSHSTATE = BASE + "/file_WebNavigation_page3_pushState.html";
const REQUIRED = [
"onBeforeNavigate",
"onCommitted",
"onDOMContentLoaded",
"onCompleted",
];
@@ -150,20 +152,81 @@ add_task(function* webnav_ordering() {
checkBefore({url: URL, event: "onCommitted"}, {url: FRAME, event: "onBeforeNavigate"});
checkBefore({url: FRAME, event: "onCompleted"}, {url: URL, event: "onCompleted"});
yield loadAndWait(win, "onCompleted", FRAME2, () => { win.frames[0].location = FRAME2; });
checkRequired(FRAME2);
- yield loadAndWait(win, "onReferenceFragmentUpdated", FRAME2 + "#ref",
- () => { win.frames[0].document.getElementById("elt").click(); });
+ let navigationSequence = [
+ {
+ action: () => { win.frames[0].document.getElementById("elt").click(); },
+ waitURL: `${FRAME2}#ref`,
+ expectedEvent: "onReferenceFragmentUpdated",
+ description: "clicked an anchor link",
+ },
+ {
+ action: () => { win.frames[0].history.pushState({}, "History PushState", `${FRAME2}#ref2`); },
+ waitURL: `${FRAME2}#ref2`,
+ expectedEvent: "onReferenceFragmentUpdated",
+ description: "history.pushState, same pathname, different hash",
+ },
+ {
+ action: () => { win.frames[0].history.pushState({}, "History PushState", `${FRAME2}#ref2`); },
+ waitURL: `${FRAME2}#ref2`,
+ expectedEvent: "onHistoryStateUpdated",
+ description: "history.pushState, same pathname, same hash",
+ },
+ {
+ action: () => {
+ win.frames[0].history.pushState({}, "History PushState", `${FRAME2}?query_param1=value#ref2`);
+ },
+ waitURL: `${FRAME2}?query_param1=value#ref2`,
+ expectedEvent: "onHistoryStateUpdated",
+ description: "history.pushState, same pathname, same hash, different query params",
+ },
+ {
+ action: () => {
+ win.frames[0].history.pushState({}, "History PushState", `${FRAME2}?query_param2=value#ref3`);
+ },
+ waitURL: `${FRAME2}?query_param2=value#ref3`,
+ expectedEvent: "onHistoryStateUpdated",
+ description: "history.pushState, same pathname, different hash, different query params",
+ },
+ {
+ action: () => { win.frames[0].history.pushState(null, "History PushState", FRAME_PUSHSTATE); },
+ waitURL: FRAME_PUSHSTATE,
+ expectedEvent: "onHistoryStateUpdated",
+ description: "history.pushState, different pathname",
+ },
+ ];
- info("Received onReferenceFragmentUpdated from FRAME2");
+ for (let navigation of navigationSequence) {
+ let {expectedEvent, waitURL, action, description} = navigation;
+ info(`Waiting ${expectedEvent} from ${waitURL} - ${description}`);
+ yield loadAndWait(win, expectedEvent, waitURL, action);
+ info(`Received ${expectedEvent} from ${waitURL} - ${description}`);
+ }
+
+ for (let i = navigationSequence.length - 1; i > 0; i--) {
+ let {waitURL: fromURL, expectedEvent} = navigationSequence[i];
+ let {waitURL} = navigationSequence[i - 1];
+ info(`Waiting ${expectedEvent} from ${waitURL} - history.back() from ${fromURL} to ${waitURL}`);
+ yield loadAndWait(win, expectedEvent, waitURL, () => { win.frames[0].history.back(); });
+ info(`Received ${expectedEvent} from ${waitURL} - history.back() from ${fromURL} to ${waitURL}`);
+ }
+
+ for (let i = 0; i < navigationSequence.length - 1; i++) {
+ let {waitURL: fromURL} = navigationSequence[i];
+ let {waitURL, expectedEvent} = navigationSequence[i + 1];
+ info(`Waiting ${expectedEvent} from ${waitURL} - history.forward() from ${fromURL} to ${waitURL}`);
+ yield loadAndWait(win, expectedEvent, waitURL, () => { win.frames[0].history.forward(); });
+ info(`Received ${expectedEvent} from ${waitURL} - history.forward() from ${fromURL} to ${waitURL}`);
+ }
win.close();
yield extension.unload();
info("webnavigation extension unloaded");
});
</script>
--- a/toolkit/modules/addons/WebNavigation.jsm
+++ b/toolkit/modules/addons/WebNavigation.jsm
@@ -94,18 +94,21 @@ var Manager = {
this.fire("onErrorOccurred", browser, data, {error, url});
}
}
}
},
onLocationChange(browser, data) {
let url = data.location;
- if (data.flags & Ci.nsIWebProgressListener.LOCATION_CHANGE_SAME_DOCUMENT) {
+
+ if (data.isReferenceFragmentUpdated) {
this.fire("onReferenceFragmentUpdated", browser, data, {url});
+ } else if (data.isHistoryStateUpdated) {
+ this.fire("onHistoryStateUpdated", browser, data, {url});
} else {
this.fire("onCommitted", browser, data, {url});
}
},
onLoad(browser, data) {
this.fire("onDOMContentLoaded", browser, data, {url: data.url});
},
@@ -137,19 +140,18 @@ var Manager = {
const EVENTS = [
"onBeforeNavigate",
"onCommitted",
"onDOMContentLoaded",
"onCompleted",
"onErrorOccurred",
"onReferenceFragmentUpdated",
-
+ "onHistoryStateUpdated",
// "onCreatedNavigationTarget",
- // "onHistoryStateUpdated",
];
var WebNavigation = {};
for (let event of EVENTS) {
WebNavigation[event] = {
addListener: Manager.addListener.bind(Manager, event),
removeListener: Manager.removeListener.bind(Manager, event),
--- a/toolkit/modules/addons/WebNavigationContent.js
+++ b/toolkit/modules/addons/WebNavigationContent.js
@@ -20,16 +20,29 @@ function loadListener(event) {
addEventListener("DOMContentLoaded", loadListener);
addMessageListener("Extension:DisableWebNavigation", () => {
removeEventListener("DOMContentLoaded", loadListener);
});
var WebProgressListener = {
init: function() {
+ // This WeakMap (DOMWindow -> nsIURI) keeps track of the pathname and hash
+ // of the previous location for all the existent docShells.
+ this.previousURIMap = new WeakMap();
+
+ // Populate the above previousURIMap by iterating over the docShells tree.
+ for (let currentDocShell of WebNavigationFrames.iterateDocShellTree(docShell)) {
+ let win = currentDocShell.QueryInterface(Ci.nsIInterfaceRequestor)
+ .getInterface(Ci.nsIDOMWindow);
+ let {currentURI} = currentDocShell.QueryInterface(Ci.nsIWebNavigation);
+
+ this.previousURIMap.set(win, currentURI);
+ }
+
let webProgress = docShell.QueryInterface(Ci.nsIInterfaceRequestor)
.getInterface(Ci.nsIWebProgress);
webProgress.addProgressListener(this, Ci.nsIWebProgress.NOTIFY_STATE_WINDOW |
Ci.nsIWebProgress.NOTIFY_LOCATION);
},
uninit() {
if (!docShell) {
@@ -43,42 +56,70 @@ var WebProgressListener = {
onStateChange: function onStateChange(webProgress, request, stateFlags, status) {
let data = {
requestURL: request.QueryInterface(Ci.nsIChannel).URI.spec,
windowId: webProgress.DOMWindowID,
parentWindowId: WebNavigationFrames.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: WebNavigationFrames.getParentWindowId(webProgress.DOMWindow),
- flags: 0,
- };
- sendAsyncMessage("Extension:LocationChange", data);
+ this.onLocationChange(webProgress, request, request.QueryInterface(Ci.nsIChannel).URI, 0);
}
}
},
onLocationChange: function onLocationChange(webProgress, request, locationURI, flags) {
+ let {DOMWindow, loadType} = webProgress;
+
+ // Get the previous URI loaded in the DOMWindow.
+ let previousURI = this.previousURIMap.get(DOMWindow);
+
+ // Update the URI in the map with the new locationURI.
+ this.previousURIMap.set(DOMWindow, locationURI);
+
+ let isSameDocument = (flags & Ci.nsIWebProgressListener.LOCATION_CHANGE_SAME_DOCUMENT);
+ let isHistoryStateUpdated = false;
+ let isReferenceFragmentUpdated = false;
+
+ if (isSameDocument) {
+ let pathChanged = !(previousURI && locationURI.equalsExceptRef(previousURI));
+ let hashChanged = !(previousURI && previousURI.ref == locationURI.ref);
+
+ // When the location changes but the document is the same:
+ // - path not changed and hash changed -> |onReferenceFragmentUpdated|
+ // (even if it changed using |history.pushState|)
+ // - path not changed and hash not changed -> |onHistoryStateUpdated|
+ // (only if it changes using |history.pushState|)
+ // - path changed -> |onHistoryStateUpdated|
+
+ if (!pathChanged && hashChanged) {
+ isReferenceFragmentUpdated = true;
+ } else if (loadType & Ci.nsIDocShell.LOAD_CMD_PUSHSTATE) {
+ isHistoryStateUpdated = true;
+ } else if (loadType & Ci.nsIDocShell.LOAD_CMD_HISTORY) {
+ isHistoryStateUpdated = true;
+ }
+ }
+
let data = {
+ isHistoryStateUpdated, isReferenceFragmentUpdated,
location: locationURI ? locationURI.spec : "",
windowId: webProgress.DOMWindowID,
parentWindowId: WebNavigationFrames.getParentWindowId(webProgress.DOMWindow),
- flags,
};
+
sendAsyncMessage("Extension:LocationChange", data);
},
QueryInterface: XPCOMUtils.generateQI([Ci.nsIWebProgressListener, Ci.nsISupportsWeakReference]),
};
var disabled = false;
WebProgressListener.init();
--- a/toolkit/modules/addons/WebNavigationFrames.jsm
+++ b/toolkit/modules/addons/WebNavigationFrames.jsm
@@ -92,16 +92,18 @@ function findFrame(windowId, rootDocShel
return convertDocShellToFrameDetail(docShell);
}
}
return null;
}
var WebNavigationFrames = {
+ iterateDocShellTree,
+
getFrame(docShell, frameId) {
if (frameId == 0) {
return convertDocShellToFrameDetail(docShell);
}
return findFrame(frameId, docShell);
},