author | Bob Silverberg <bsilverberg@mozilla.com> |
Fri, 08 Sep 2017 17:00:27 -0400 | |
changeset 662440 | 14f5e7142d499904758e41855a8d6937c686c8c3 |
parent 662439 | a59cb1261a62b97fc24e2b06f30499c37a8b7287 |
child 730869 | 3b5b5c364af63decd12490ea7d3f63e7d3fabf8d |
push id | 79084 |
push user | bmo:bob.silverberg@gmail.com |
push date | Mon, 11 Sep 2017 17:49:24 +0000 |
reviewers | mixedpuppy |
bugs | 1381992 |
milestone | 57.0a1 |
--- 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); } } }