Bug 1239349 - Implement webNavigation.onHistoryStateUpdated. r=kmag draft
authorLuca Greco <lgreco@mozilla.com>
Fri, 12 Feb 2016 02:13:19 +0100
changeset 334158 0bca074e5837641757d5a0507d0ba87947e46cce
parent 334157 7a39bf96ab31a00d8fa370d4f318213b914bd77b
child 514832 78e1b71348b7e7632c55316652f2b05d12433727
push id11458
push userluca.greco@alcacoop.it
push dateWed, 24 Feb 2016 15:45:20 +0000
reviewerskmag
bugs1239349
milestone47.0a1
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);
   },