Bug 1381992 - Part 2: Detect if a tab is in reader mode and enter/leave 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: 9f83cpIpv4K
--- 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)[<all_urls>] 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);
}
}
}