mirror of
https://gitlab.winehq.org/wine/wine-gecko.git
synced 2024-09-13 09:24:08 -07:00
Bug 1238311: Part 3 - [webext] Add audible and muted support to browser.tabs API. r=gabor
This commit is contained in:
parent
127ae55171
commit
64d33abd44
@ -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) {
|
||||
|
@ -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)) {
|
||||
|
@ -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."
|
||||
|
@ -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]
|
||||
|
@ -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);
|
||||
});
|
Loading…
Reference in New Issue
Block a user