Bug 1239349 - Implement webNavigation.onHistoryStateUpdated. r=kmag
authorLuca Greco <lgreco@mozilla.com>
Fri, 12 Feb 2016 02:13:19 +0100
changeset 321921 9b5fd0f8dbf1223932f7af246f871f6968e13ee1
parent 321920 4d789e933b43ca5f78964551b4c2f323ad814d94
child 321922 96732e2e7174c1085ed2f81dfc9fdaff10e0e712
push id5913
push userjlund@mozilla.com
push dateMon, 25 Apr 2016 16:57:49 +0000
treeherdermozilla-beta@dcaf0a6fa115 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewerskmag
bugs1239349
milestone47.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
Bug 1239349 - Implement webNavigation.onHistoryStateUpdated. r=kmag MozReview-Commit-ID: FvtkZpcJYCU
toolkit/components/extensions/ext-webNavigation.js
toolkit/components/extensions/schemas/web_navigation.json
toolkit/components/extensions/test/mochitest/test_ext_webnavigation.html
toolkit/modules/addons/WebNavigation.jsm
toolkit/modules/addons/WebNavigationContent.js
toolkit/modules/addons/WebNavigationFrames.jsm
--- 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);
   },