Bug 1381992 - Ability to tell if a tab is in reader mode and to move and tab in and out of reader mode, r?mixedpuppy draft
authorBob Silverberg <bsilverberg@mozilla.com>
Fri, 08 Sep 2017 17:00:27 -0400
changeset 662438 ca769164773738d36cd1bba64216e103056836e5
parent 662437 6a4ec3c5b723205950183bd2a3905fec04df00d2
child 730868 29ca05029308bc82034d5c2fa2b071125bfe3aca
push id79083
push userbmo:bob.silverberg@gmail.com
push dateMon, 11 Sep 2017 17:42:37 +0000
reviewersmixedpuppy
bugs1381992
milestone57.0a1
Bug 1381992 - Ability to tell if a tab is in reader mode and to move and tab in and out of reader mode, r?mixedpuppy This adds three methods to the tabs API: isInReaderMode() which reports whether a tab is currently in reader mode. enterReaderMode() which puts a tab into reader mode, if possible. leaveReaderMode() which takes a tab out of reader mode, if it is currently in reader mode. MozReview-Commit-ID: ERTJZJNKWga
browser/components/extensions/ext-tabs.js
browser/components/extensions/schemas/tabs.json
browser/components/extensions/test/browser/browser_ext_tabs_readerMode.js
toolkit/components/extensions/ExtensionContent.jsm
toolkit/components/extensions/extension-process-script.js
--- a/browser/components/extensions/ext-tabs.js
+++ b/browser/components/extensions/ext-tabs.js
@@ -12,16 +12,18 @@ XPCOMUtils.defineLazyGetter(this, "strBu
 
 XPCOMUtils.defineLazyModuleGetter(this, "PrivateBrowsingUtils",
                                   "resource://gre/modules/PrivateBrowsingUtils.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "PromiseUtils",
                                   "resource://gre/modules/PromiseUtils.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "Services",
                                   "resource://gre/modules/Services.jsm");
 
+const READER_MODE_PREFIX = "about:reader";
+
 let tabListener = {
   tabReadyInitialized: false,
   tabReadyPromises: new WeakMap(),
   initializingTabs: new WeakSet(),
 
   initTabReady() {
     if (!this.tabReadyInitialized) {
       windowTracker.addListener("progress", this);
@@ -94,16 +96,21 @@ this.tabs = class extends ExtensionAPI {
         tab = tabManager.getWrapper(tabTracker.activeTab);
       }
 
       await tabListener.awaitTabReady(tab.nativeTab);
 
       return tab;
     }
 
+    function isTabInReaderMode(tab) {
+      return tab.url.startsWith(READER_MODE_PREFIX);
+    }
+
+
     let self = {
       tabs: {
         onActivated: new EventManager(context, "tabs.onActivated", fire => {
           let listener = (eventName, event) => {
             fire.async(event);
           };
 
           tabTracker.on("tab-activated", listener);
@@ -916,13 +923,39 @@ this.tabs = class extends ExtensionAPI {
           });
         },
 
         async hasReaderMode(tabId) {
           let tab = await promiseTabWhenReady(tabId);
 
           return tab.sendMessage(context, "Extension:IsTabReaderable");
         },
+
+        async isInReaderMode(tabId) {
+          let tab = await promiseTabWhenReady(tabId);
+
+          return isTabInReaderMode(tab);
+        },
+
+        async enterReaderMode(tabId) {
+          let tab = await promiseTabWhenReady(tabId);
+
+          if (isTabInReaderMode(tab)) {
+            return;
+          }
+
+          return tab.sendMessage(context, "Extension:EnterReaderMode");
+        },
+
+        async leaveReaderMode(tabId) {
+          let tab = await promiseTabWhenReady(tabId);
+
+          if (!isTabInReaderMode(tab)) {
+            return;
+          }
+
+          return tab.sendMessage(context, "Extension:LeaveReaderMode");
+        },
       },
     };
     return self;
   }
 };
--- a/browser/components/extensions/schemas/tabs.json
+++ b/browser/components/extensions/schemas/tabs.json
@@ -911,16 +911,61 @@
             "name": "tabId",
             "minimum": 0,
             "optional": true,
             "description": "Defaults to the active tab of the $(topic:current-window)[current window]."
           }
         ]
       },
       {
+        "name": "isInReaderMode",
+        "type": "function",
+        "description": "Reports whether the document in the tab is being rendered in reader mode.",
+        "async": true,
+        "parameters": [
+          {
+            "type": "integer",
+            "name": "tabId",
+            "minimum": 0,
+            "optional": true,
+            "description": "Defaults to the active tab of the $(topic:current-window)[current window]."
+          }
+        ]
+      },
+      {
+        "name": "enterReaderMode",
+        "type": "function",
+        "description": "Puts the document in the tab into reader mode, if possible.",
+        "async": true,
+        "parameters": [
+          {
+            "type": "integer",
+            "name": "tabId",
+            "minimum": 0,
+            "optional": true,
+            "description": "Defaults to the active tab of the $(topic:current-window)[current window]."
+          }
+        ]
+      },
+      {
+        "name": "leaveReaderMode",
+        "type": "function",
+        "description": "Takes the document in the tab out of reader mode, if possible.",
+        "async": true,
+        "parameters": [
+          {
+            "type": "integer",
+            "name": "tabId",
+            "minimum": 0,
+            "optional": true,
+            "description": "Defaults to the active tab of the $(topic:current-window)[current window]."
+          }
+        ]
+      },
+      {
         "name": "captureVisibleTab",
         "type": "function",
         "description": "Captures the visible area of the currently active tab in the specified window. You must have $(topic:declare_permissions)[&lt;all_urls&gt;] permission to use this method.",
         "permissions": ["<all_urls>"],
         "async": "callback",
         "parameters": [
           {
             "type": "integer",
--- a/browser/components/extensions/test/browser/browser_ext_tabs_readerMode.js
+++ b/browser/components/extensions/test/browser/browser_ext_tabs_readerMode.js
@@ -4,34 +4,71 @@
 
 add_task(async function test_has_reader_mode() {
   let extension = ExtensionTestUtils.loadExtension({
     manifest: {
       "permissions": ["tabs"],
     },
 
     async background() {
-      browser.test.onMessage.addListener(msg => {
-        browser.tabs.update({url: msg});
+      browser.test.onMessage.addListener(async (msg, data) => {
+        let isInReaderMode;
+        switch (msg) {
+          case "updateUrl":
+            browser.tabs.update({url: data});
+            break;
+          case "enterReaderMode":
+            // data is true if we expect to enter reader mode.
+            isInReaderMode = await browser.tabs.isInReaderMode();
+            browser.test.assertTrue(!isInReaderMode, "The tab is not in reader mode.");
+            browser.tabs.enterReaderMode();
+            if (!data) {
+              isInReaderMode = await browser.tabs.isInReaderMode();
+              browser.test.assertTrue(!isInReaderMode, "The tab is still not in reader mode.");
+              browser.test.sendMessage("enterFailed");
+            }
+            break;
+          case "leaveReaderMode":
+            isInReaderMode = await browser.tabs.isInReaderMode();
+            browser.test.assertTrue(isInReaderMode, "The tab is in reader mode.");
+            browser.tabs.leaveReaderMode();
+            break;
+        }
       });
 
       browser.tabs.onUpdated.addListener(async (tabId, changeInfo, tab) => {
         if (changeInfo.status === "complete") {
           let isReaderable = await browser.tabs.hasReaderMode(tabId);
-          browser.test.sendMessage("updated", isReaderable);
+          browser.test.sendMessage("updated", {isReaderable, tab});
         }
       });
     },
   });
 
   const TEST_PATH = getRootDirectory(gTestPath).replace("chrome://mochitests/content", "http://example.com");
+  const READER_MODE_PREFIX = "about:reader";
 
   await extension.startup();
-  extension.sendMessage(`${TEST_PATH}readerModeArticle.html`);
-  let isReaderable = await extension.awaitMessage("updated");
-  ok(isReaderable, "Tab is readerable.");
+  extension.sendMessage("updateUrl", `${TEST_PATH}readerModeArticle.html`);
+  let updateData = await extension.awaitMessage("updated");
+  ok(updateData.isReaderable, "Tab is readerable.");
+  ok(!updateData.tab.url.startsWith(READER_MODE_PREFIX), "Tab url does not indicate reader mode.");
+
+  extension.sendMessage("enterReaderMode", true);
+  updateData = await extension.awaitMessage("updated");
+  ok(!updateData.isReaderable, "Tab is not readerable.");
+  ok(updateData.tab.url.startsWith(READER_MODE_PREFIX), "Tab url indicates reader mode.");
 
-  extension.sendMessage(`${TEST_PATH}readerModeNonArticle.html`);
-  isReaderable = await extension.awaitMessage("updated");
-  ok(!isReaderable, "Tab is not readerable.");
+  extension.sendMessage("leaveReaderMode");
+  updateData = await extension.awaitMessage("updated");
+  ok(updateData.isReaderable, "Tab is readerable.");
+  ok(!updateData.tab.url.startsWith(READER_MODE_PREFIX), "Tab url does not indicate reader mode.");
+
+  extension.sendMessage("updateUrl", `${TEST_PATH}readerModeNonArticle.html`);
+  updateData = await extension.awaitMessage("updated");
+  ok(!updateData.isReaderable, "Tab is not readerable.");
+  ok(!updateData.tab.url.startsWith(READER_MODE_PREFIX), "Tab url does not indicate reader mode.");
+
+  extension.sendMessage("enterReaderMode", false);
+  updateData = await extension.awaitMessage("enterFailed");
 
   await extension.unload();
 });
--- a/toolkit/components/extensions/ExtensionContent.jsm
+++ b/toolkit/components/extensions/ExtensionContent.jsm
@@ -739,18 +739,33 @@ this.ExtensionContent = {
       const fileName = js.length ? js[js.length - 1] : "<anonymous code>";
       const message = `Script '${fileName}' result is non-structured-clonable data`;
       return Promise.reject({message, fileName});
     }
 
     return result;
   },
 
+  isTabReaderable(global) {
+    return ReaderMode.isProbablyReaderable(global.content.document);
+  },
+
+
+  handleEnterReaderMode(global) {
+    if (this.isTabReaderable(global)) {
+      return ReaderMode.enterReaderMode(global.docShell, global.content);
+    }
+  },
+
+  handleLeaveReaderMode(global) {
+    return ReaderMode.leaveReaderMode(global.docShell, global.content);
+  },
+
   handleIsTabReaderable(global) {
-    return ReaderMode.isProbablyReaderable(global.content.document);
+    return this.isTabReaderable(global);
   },
 
   handleWebNavigationGetFrame(global, {frameId}) {
     return WebNavigationFrames.getFrame(global.docShell, frameId);
   },
 
   handleWebNavigationGetAllFrames(global) {
     return WebNavigationFrames.getAllFrames(global.docShell);
--- a/toolkit/components/extensions/extension-process-script.js
+++ b/toolkit/components/extensions/extension-process-script.js
@@ -89,18 +89,20 @@ class ExtensionGlobal {
   constructor(global) {
     this.global = global;
     this.global.addMessageListener("Extension:SetFrameData", this);
 
     this.frameData = null;
 
     MessageChannel.addListener(global, "Extension:Capture", this);
     MessageChannel.addListener(global, "Extension:DetectLanguage", this);
+    MessageChannel.addListener(global, "Extension:EnterReaderMode", this);
     MessageChannel.addListener(global, "Extension:Execute", this);
     MessageChannel.addListener(global, "Extension:IsTabReaderable", this);
+    MessageChannel.addListener(global, "Extension:LeaveReaderMode", this);
     MessageChannel.addListener(global, "WebNavigation:GetFrame", this);
     MessageChannel.addListener(global, "WebNavigation:GetAllFrames", this);
   }
 
   get messageFilterStrict() {
     return {
       innerWindowID: getInnerWindowID(this.global.content),
     };
@@ -127,16 +129,18 @@ class ExtensionGlobal {
         return;
     }
 
     switch (messageName) {
       case "Extension:Capture":
         return ExtensionContent.handleExtensionCapture(this.global, data.width, data.height, data.options);
       case "Extension:DetectLanguage":
         return ExtensionContent.handleDetectLanguage(this.global, target);
+      case "Extension:EnterReaderMode":
+        return ExtensionContent.handleEnterReaderMode(this.global);
       case "Extension:Execute":
         let policy = WebExtensionPolicy.getByID(recipient.extensionId);
 
         let matcher = new WebExtensionContentScript(policy, parseScriptOptions(data.options));
 
         Object.assign(matcher, {
           wantReturnValue: data.options.wantReturnValue,
           removeCSS: data.options.remove_css,
@@ -145,16 +149,18 @@ class ExtensionGlobal {
           jsCode: data.options.jsCode,
         });
 
         let script = contentScripts.get(matcher);
 
         return ExtensionContent.handleExtensionExecute(this.global, target, data.options, script);
       case "Extension:IsTabReaderable":
         return ExtensionContent.handleIsTabReaderable(this.global);
+      case "Extension:LeaveReaderMode":
+        return ExtensionContent.handleLeaveReaderMode(this.global);
       case "WebNavigation:GetFrame":
         return ExtensionContent.handleWebNavigationGetFrame(this.global, data.options);
       case "WebNavigation:GetAllFrames":
         return ExtensionContent.handleWebNavigationGetAllFrames(this.global);
     }
   }
 }