Bug 1238311: Part 3 - [webext] Add audible and muted support to browser.tabs API. r=gabor

This commit is contained in:
Kris Maglione 2016-01-26 17:06:41 -08:00
parent 127ae55171
commit 64d33abd44
5 changed files with 287 additions and 38 deletions

View File

@ -150,29 +150,48 @@ extensions.registerSchemaAPI("tabs", null, (extension, context) => {
return [nonempty, result];
}
let listener = event => {
let tab = event.originalTarget;
let window = tab.ownerDocument.defaultView;
let tabId = TabManager.getId(tab);
let fireForBrowser = (browser, changed) => {
let [needed, changeInfo] = sanitize(extension, changed);
if (needed) {
let gBrowser = browser.ownerDocument.defaultView.gBrowser;
let tabElem = gBrowser.getTabForBrowser(browser);
let changeInfo = {};
let needed = false;
let tab = TabManager.convert(extension, tabElem);
fire(tab.id, changeInfo, tab);
}
};
let listener = event => {
let needed = [];
if (event.type == "TabAttrModified") {
if (event.detail.changed.indexOf("image") != -1) {
changeInfo.favIconUrl = window.gBrowser.getIcon(tab);
needed = true;
let changed = event.detail.changed;
if (changed.includes("image")) {
needed.push("favIconUrl");
}
if (changed.includes("muted")) {
needed.push("mutedInfo");
}
if (changed.includes("soundplaying")) {
needed.push("audible");
}
} else if (event.type == "TabPinned") {
changeInfo.pinned = true;
needed = true;
needed.push("pinned");
} else if (event.type == "TabUnpinned") {
changeInfo.pinned = false;
needed = true;
needed.push("pinned");
}
[needed, changeInfo] = sanitize(extension, changeInfo);
if (needed) {
fire(tabId, changeInfo, TabManager.convert(extension, tab));
if (needed.length && !extension.hasPermission("tabs")) {
needed = needed.filter(attr => attr != "url" && attr != "favIconUrl");
}
if (needed.length) {
let tab = TabManager.convert(extension, event.originalTarget);
let changeInfo = {};
for (let prop of needed) {
changeInfo[prop] = tab[prop];
}
fire(tab.id, changeInfo, tab);
}
};
let progressListener = {
@ -193,29 +212,18 @@ extensions.registerSchemaAPI("tabs", null, (extension, context) => {
status = "complete";
}
let gBrowser = browser.ownerDocument.defaultView.gBrowser;
let tab = gBrowser.getTabForBrowser(browser);
let tabId = TabManager.getId(tab);
let [needed, changeInfo] = sanitize(extension, {status});
if (needed) {
fire(tabId, changeInfo, TabManager.convert(extension, tab));
}
fireForBrowser(browser, {status});
},
onLocationChange(browser, webProgress, request, locationURI, flags) {
if (!webProgress.isTopLevel) {
return;
}
let gBrowser = browser.ownerDocument.defaultView.gBrowser;
let tab = gBrowser.getTabForBrowser(browser);
let tabId = TabManager.getId(tab);
let [needed, changeInfo] = sanitize(extension, {
fireForBrowser(browser, {
status: webProgress.isLoadingDocument ? "loading" : "complete",
url: locationURI.spec,
});
if (needed) {
fire(tabId, changeInfo, TabManager.convert(extension, tab));
}
},
};
@ -333,6 +341,11 @@ extensions.registerSchemaAPI("tabs", null, (extension, context) => {
// Not sure what to do here? Which tab should we select?
}
}
if (updateProperties.muted !== null) {
if (tab.muted != updateProperties.muted) {
tab.toggleMuteAudio(extension.uuid);
}
}
if (updateProperties.pinned !== null) {
if (updateProperties.pinned) {
tabbrowser.pinTab(tab);
@ -340,7 +353,7 @@ extensions.registerSchemaAPI("tabs", null, (extension, context) => {
tabbrowser.unpinTab(tab);
}
}
// FIXME: highlighted/selected, muted, openerTabId
// FIXME: highlighted/selected, openerTabId
if (callback) {
runSafe(context, callback, TabManager.convert(extension, tab));
@ -415,6 +428,18 @@ extensions.registerSchemaAPI("tabs", null, (extension, context) => {
}
}
if (queryInfo.audible !== null) {
if (queryInfo.audible != tab.audible) {
return false;
}
}
if (queryInfo.muted !== null) {
if (queryInfo.muted != tab.mutedInfo.muted) {
return false;
}
}
if (queryInfo.currentWindow !== null) {
let eq = window == currentWindow(context);
if (queryInfo.currentWindow != eq) {

View File

@ -423,6 +423,14 @@ ExtensionTabManager.prototype = {
convert(tab) {
let window = tab.ownerDocument.defaultView;
let mutedInfo = { muted: tab.muted };
if (tab.muteReason === null) {
mutedInfo.reason = "user";
} else if (tab.muteReason) {
mutedInfo.reason = "extension";
mutedInfo.extensionId = tab.muteReason;
}
let result = {
id: TabManager.getId(tab),
index: tab._tPos,
@ -435,6 +443,8 @@ ExtensionTabManager.prototype = {
incognito: PrivateBrowsingUtils.isBrowserPrivate(tab.linkedBrowser),
width: tab.linkedBrowser.clientWidth,
height: tab.linkedBrowser.clientHeight,
audible: tab.soundPlaying,
mutedInfo,
};
if (this.hasTabPermission(tab)) {

View File

@ -49,8 +49,8 @@
"highlighted": {"type": "boolean", "description": "Whether the tab is highlighted."},
"active": {"type": "boolean", "description": "Whether the tab is active in its window. (Does not necessarily mean the window is focused.)"},
"pinned": {"type": "boolean", "description": "Whether the tab is pinned."},
"audible": {"unsupported": true, "type": "boolean", "optional": true, "description": "Whether the tab has produced sound over the past couple of seconds (but it might not be heard if also muted). Equivalent to whether the speaker audio indicator is showing."},
"mutedInfo": {"unsupported": true, "$ref": "MutedInfo", "optional": true, "description": "Current tab muted state and the reason for the last state change."},
"audible": {"type": "boolean", "optional": true, "description": "Whether the tab has produced sound over the past couple of seconds (but it might not be heard if also muted). Equivalent to whether the speaker audio indicator is showing."},
"mutedInfo": {"$ref": "MutedInfo", "optional": true, "description": "Current tab muted state and the reason for the last state change."},
"url": {"type": "string", "optional": true, "description": "The URL the tab is displaying. This property is only present if the extension's manifest includes the <code>\"tabs\"</code> permission."},
"title": {"type": "string", "optional": true, "description": "The title of the tab. This property is only present if the extension's manifest includes the <code>\"tabs\"</code> permission."},
"favIconUrl": {"type": "string", "optional": true, "description": "The URL of the tab's favicon. This property is only present if the extension's manifest includes the <code>\"tabs\"</code> permission. It may also be an empty string if the tab is loading."},
@ -432,13 +432,11 @@
"description": "Whether the tabs are pinned."
},
"audible": {
"unsupported": true,
"type": "boolean",
"optional": true,
"description": "Whether the tabs are audible."
},
"muted": {
"unsupported": true,
"type": "boolean",
"optional": true,
"description": "Whether the tabs are muted."
@ -593,7 +591,6 @@
"description": "Whether the tab should be pinned."
},
"muted": {
"unsupported": true,
"type": "boolean",
"optional": true,
"description": "Whether the tab should be muted."
@ -948,13 +945,11 @@
"description": "The tab's new pinned state."
},
"audible": {
"unsupported": true,
"type": "boolean",
"optional": true,
"description": "The tab's new audible state."
},
"mutedInfo": {
"unsupported": true,
"$ref": "MutedInfo",
"optional": true,
"description": "The tab's new muted state and the reason for the change."

View File

@ -20,6 +20,7 @@ support-files =
[browser_ext_popup_api_injection.js]
[browser_ext_contextMenus.js]
[browser_ext_getViews.js]
[browser_ext_tabs_audio.js]
[browser_ext_tabs_executeScript.js]
[browser_ext_tabs_executeScript_good.js]
[browser_ext_tabs_executeScript_bad.js]

View File

@ -0,0 +1,218 @@
/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
/* vim: set sts=2 sw=2 et tw=80: */
"use strict";
add_task(function* () {
let tab1 = yield BrowserTestUtils.openNewForegroundTab(gBrowser, "about:blank?1");
let tab2 = yield BrowserTestUtils.openNewForegroundTab(gBrowser, "about:blank?2");
gBrowser.selectedTab = tab1;
function background() {
// Wrap API methods in promise-based variants.
let promiseTabs = {};
Object.keys(browser.tabs).forEach(method => {
promiseTabs[method] = (...args) => {
return new Promise(resolve => {
browser.tabs[method](...args, resolve);
});
};
});
function promiseUpdated(tabId, attr) {
return new Promise(resolve => {
let onUpdated = (tabId_, changeInfo, tab) => {
if (tabId == tabId_ && attr in changeInfo) {
browser.tabs.onUpdated.removeListener(onUpdated);
resolve({changeInfo, tab});
}
};
browser.tabs.onUpdated.addListener(onUpdated);
});
}
let deferred = {};
browser.test.onMessage.addListener((message, tabId, result) => {
if (message == "change-tab-done" && deferred[tabId]) {
deferred[tabId].resolve(result);
}
});
function changeTab(tabId, attr, on) {
return new Promise((resolve, reject) => {
deferred[tabId] = {resolve, reject};
browser.test.sendMessage("change-tab", tabId, attr, on);
});
}
let windowId;
let tabIds;
promiseTabs.query({ lastFocusedWindow: true }).then(tabs => {
browser.test.assertEq(tabs.length, 3, "We have three tabs");
for (let tab of tabs) {
// Note: We want to check that these are actual boolean values, not
// just that they evaluate as false.
browser.test.assertEq(false, tab.mutedInfo.muted, "Tab is not muted");
browser.test.assertEq(undefined, tab.mutedInfo.reason, "Tab has no muted info reason");
browser.test.assertEq(false, tab.audible, "Tab is not audible");
}
windowId = tabs[0].windowId;
tabIds = [tabs[1].id, tabs[2].id];
browser.test.log("Test initial queries for muted and audible return no tabs");
return Promise.all([
promiseTabs.query({ windowId, audible: false }),
promiseTabs.query({ windowId, audible: true }),
promiseTabs.query({ windowId, muted: true }),
promiseTabs.query({ windowId, muted: false }),
]);
}).then(([silent, audible, muted, nonMuted]) => {
browser.test.assertEq(3, silent.length, "Three silent tabs");
browser.test.assertEq(0, audible.length, "No audible tabs");
browser.test.assertEq(0, muted.length, "No muted tabs");
browser.test.assertEq(3, nonMuted.length, "Three non-muted tabs");
browser.test.log("Toggle muted and audible externally on one tab each, and check results");
return Promise.all([
promiseUpdated(tabIds[0], "mutedInfo"),
promiseUpdated(tabIds[1], "audible"),
changeTab(tabIds[0], "muted", true),
changeTab(tabIds[1], "audible", true),
]);
}).then(([muted, audible]) => {
for (let obj of [muted.changeInfo, muted.tab]) {
browser.test.assertEq(true, obj.mutedInfo.muted, "Tab is muted");
browser.test.assertEq("user", obj.mutedInfo.reason, "Tab was muted by the user");
}
browser.test.assertEq(true, audible.changeInfo.audible, "Tab audible state changed");
browser.test.assertEq(true, audible.tab.audible, "Tab is audible");
browser.test.log("Re-check queries. Expect one audible and one muted tab");
return Promise.all([
promiseTabs.query({ windowId, audible: false }),
promiseTabs.query({ windowId, audible: true }),
promiseTabs.query({ windowId, muted: true }),
promiseTabs.query({ windowId, muted: false }),
]);
}).then(([silent, audible, muted, nonMuted]) => {
browser.test.assertEq(2, silent.length, "Two silent tabs");
browser.test.assertEq(1, audible.length, "One audible tab");
browser.test.assertEq(1, muted.length, "One muted tab");
browser.test.assertEq(2, nonMuted.length, "Two non-muted tabs");
browser.test.assertEq(true, muted[0].mutedInfo.muted, "Tab is muted");
browser.test.assertEq("user", muted[0].mutedInfo.reason, "Tab was muted by the user");
browser.test.assertEq(true, audible[0].audible, "Tab is audible");
browser.test.log("Toggle muted internally on two tabs, and check results");
return Promise.all([
promiseUpdated(tabIds[0], "mutedInfo"),
promiseUpdated(tabIds[1], "mutedInfo"),
promiseTabs.update(tabIds[0], { muted: false }),
promiseTabs.update(tabIds[1], { muted: true }),
]);
}).then(([unmuted, muted]) => {
for (let obj of [unmuted.changeInfo, unmuted.tab]) {
browser.test.assertEq(false, obj.mutedInfo.muted, "Tab is not muted");
}
for (let obj of [muted.changeInfo, muted.tab]) {
browser.test.assertEq(true, obj.mutedInfo.muted, "Tab is muted");
}
for (let obj of [unmuted.changeInfo, unmuted.tab, muted.changeInfo, muted.tab]) {
browser.test.assertEq("extension", obj.mutedInfo.reason, "Mute state changed by extension");
// FIXME: browser.runtime.id is currently broken.
browser.test.assertEq(browser.i18n.getMessage("@@extension_id"),
obj.mutedInfo.extensionId,
"Mute state changed by extension");
}
browser.test.log("Test that mutedInfo is preserved by sessionstore");
return changeTab(tabIds[1], "duplicate").then(promiseTabs.get);
}).then(tab => {
browser.test.assertEq(true, tab.mutedInfo.muted, "Tab is muted");
browser.test.assertEq("extension", tab.mutedInfo.reason, "Mute state changed by extension");
// FIXME: browser.runtime.id is currently broken.
browser.test.assertEq(browser.i18n.getMessage("@@extension_id"),
tab.mutedInfo.extensionId,
"Mute state changed by extension");
browser.test.log("Unmute externally, and check results");
return Promise.all([
promiseUpdated(tabIds[1], "mutedInfo"),
changeTab(tabIds[1], "muted", false),
promiseTabs.remove(tab.id),
]);
}).then(([unmuted]) => {
for (let obj of [unmuted.changeInfo, unmuted.tab]) {
browser.test.assertEq(false, obj.mutedInfo.muted, "Tab is not muted");
browser.test.assertEq("user", obj.mutedInfo.reason, "Mute state changed by user");
}
browser.test.notifyPass("tab-audio");
}).catch(e => {
browser.test.fail(`${e} :: ${e.stack}`);
browser.test.notifyFail("tab-audio");
});
}
let extension = ExtensionTestUtils.loadExtension({
manifest: {
"permissions": ["tabs"],
},
background,
});
extension.onMessage("change-tab", (tabId, attr, on) => {
let {TabManager} = Cu.import("resource://gre/modules/Extension.jsm", {});
let tab = TabManager.getTab(tabId);
if (attr == "muted") {
// Ideally we'd simulate a click on the tab audio icon for this, but the
// handler relies on CSS :hover states, which are complicated and fragile
// to simulate.
if (tab.muted != on) {
tab.toggleMuteAudio();
}
} else if (attr == "audible") {
let browser = tab.linkedBrowser;
if (on) {
browser.audioPlaybackStarted();
} else {
browser.audioPlaybackStopped();
}
} else if (attr == "duplicate") {
// This is a bit of a hack. It won't be necessary once we have
// `tabs.duplicate`.
let newTab = gBrowser.duplicateTab(tab);
BrowserTestUtils.waitForEvent(newTab, "SSTabRestored", () => true).then(() => {
extension.sendMessage("change-tab-done", tabId, TabManager.getId(newTab));
});
return;
}
extension.sendMessage("change-tab-done", tabId);
});
yield extension.startup();
yield extension.awaitFinish("tab-audio");
yield extension.unload();
yield BrowserTestUtils.removeTab(tab1);
yield BrowserTestUtils.removeTab(tab2);
});