зеркало из https://github.com/mozilla/gecko-dev.git
Merge autoland to mozilla-central. a=merge
This commit is contained in:
Коммит
86dc471972
|
@ -165,15 +165,17 @@ DistributionCustomizer.prototype = {
|
|||
break;
|
||||
|
||||
case "livemark":
|
||||
if (itemIndex < defaultIndex)
|
||||
// Livemarks are no more supported, instead of a livemark we'll insert
|
||||
// a bookmark pointing to the site uri, if available.
|
||||
if (!item.siteLink) {
|
||||
break;
|
||||
}
|
||||
if (itemIndex < defaultIndex) {
|
||||
index = prependIndex++;
|
||||
}
|
||||
|
||||
// Don't bother updating the livemark contents on creation.
|
||||
let parentId = await PlacesUtils.promiseItemId(parentGuid);
|
||||
await PlacesUtils.livemarks.addLivemark({
|
||||
feedURI: Services.io.newURI(item.feedLink),
|
||||
siteURI: Services.io.newURI(item.siteLink),
|
||||
parentId, index, title: item.title,
|
||||
await PlacesUtils.bookmarks.insert({
|
||||
parentGuid, index, title: item.title, url: item.siteLink,
|
||||
});
|
||||
break;
|
||||
|
||||
|
|
|
@ -203,8 +203,16 @@ class ExtensionControlledPopup {
|
|||
return;
|
||||
}
|
||||
|
||||
// Find the elements we need.
|
||||
let win = targetWindow || this.topWindow;
|
||||
// If the window closes while waiting for focus, this might reject/throw,
|
||||
// and we should stop trying to show the popup.
|
||||
try {
|
||||
await this._ensureWindowReady(win);
|
||||
} catch (ex) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Find the elements we need.
|
||||
let doc = win.document;
|
||||
let panel = doc.getElementById("extension-notification-panel");
|
||||
let popupnotification = doc.getElementById(this.popupnotificationId);
|
||||
|
@ -301,4 +309,53 @@ class ExtensionControlledPopup {
|
|||
link.textContent = strBundle.GetStringFromName(this.learnMoreMessageId);
|
||||
description.appendChild(link);
|
||||
}
|
||||
|
||||
_ensureWindowReady(win) {
|
||||
return new Promise(async (resolve, reject) => {
|
||||
if (win.closed) {
|
||||
reject();
|
||||
return;
|
||||
}
|
||||
let promises = [];
|
||||
let listenersToRemove = [];
|
||||
function promiseEvent(type) {
|
||||
promises.push(new Promise(resolve => {
|
||||
let listener = () => {
|
||||
win.removeEventListener(type, listener);
|
||||
resolve();
|
||||
};
|
||||
win.addEventListener(type, listener);
|
||||
listenersToRemove.push([type, listener]);
|
||||
}));
|
||||
}
|
||||
let {focusedWindow, activeWindow} = Services.focus;
|
||||
if (activeWindow != win) {
|
||||
promiseEvent("activate");
|
||||
}
|
||||
if (focusedWindow) {
|
||||
// We may have focused a non-remote child window, find the browser window:
|
||||
let {rootTreeItem} = focusedWindow.docShell;
|
||||
rootTreeItem.QueryInterface(Ci.nsIDocShell);
|
||||
focusedWindow = rootTreeItem.contentViewer.DOMDocument.defaultView;
|
||||
}
|
||||
if (focusedWindow != win) {
|
||||
promiseEvent("focus");
|
||||
}
|
||||
let unloadListener;
|
||||
if (promises.length) {
|
||||
unloadListener = () => {
|
||||
for (let [type, listener] of listenersToRemove) {
|
||||
win.removeEventListener(type, listener);
|
||||
}
|
||||
reject();
|
||||
};
|
||||
win.addEventListener("unload", unloadListener, {once: true});
|
||||
}
|
||||
await Promise.all(promises);
|
||||
if (unloadListener) {
|
||||
win.removeEventListener("unload", unloadListener);
|
||||
}
|
||||
resolve();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2,9 +2,8 @@
|
|||
/* vim: set sts=2 sw=2 et tw=80: */
|
||||
"use strict";
|
||||
|
||||
XPCOMUtils.defineLazyServiceGetter(this, "aboutNewTabService",
|
||||
"@mozilla.org/browser/aboutnewtab-service;1",
|
||||
"nsIAboutNewTabService");
|
||||
ChromeUtils.defineModuleGetter(this, "HomePage",
|
||||
"resource:///modules/HomePage.jsm");
|
||||
ChromeUtils.defineModuleGetter(this, "PrivateBrowsingUtils",
|
||||
"resource://gre/modules/PrivateBrowsingUtils.jsm");
|
||||
|
||||
|
@ -152,9 +151,6 @@ this.windows = class extends ExtensionAPI {
|
|||
if (createData.incognito !== null && createData.incognito != incognito) {
|
||||
return Promise.reject({message: "`incognito` property must match the incognito state of tab"});
|
||||
}
|
||||
if (createData.incognito && !PrivateBrowsingUtils.enabled) {
|
||||
return Promise.reject({message: "`incognito` cannot be used if incognito mode is disabled"});
|
||||
}
|
||||
createData.incognito = incognito;
|
||||
|
||||
if (createData.cookieStoreId && createData.cookieStoreId !== getCookieStoreIdForTab(createData, tab)) {
|
||||
|
@ -173,12 +169,14 @@ this.windows = class extends ExtensionAPI {
|
|||
args.appendElement(mkstr(createData.url));
|
||||
}
|
||||
} else {
|
||||
let url = aboutNewTabService.newTabURL;
|
||||
let url = createData.incognito && !PrivateBrowsingUtils.permanentPrivateBrowsing ?
|
||||
"about:privatebrowsing" : HomePage.get().split("|", 1)[0];
|
||||
args.appendElement(mkstr(url));
|
||||
|
||||
if (url === "about:newtab") {
|
||||
// The extension principal cannot directly load about:newtab,
|
||||
// so use the system principal instead.
|
||||
if (url.startsWith("about:") &&
|
||||
!context.checkLoadURL(url, {dontReportErrors: true})) {
|
||||
// The extension principal cannot directly load about:-URLs,
|
||||
// except for about:blank. So use the system principal instead.
|
||||
principal = Services.scriptSecurityManager.getSystemPrincipal();
|
||||
}
|
||||
}
|
||||
|
@ -213,6 +211,9 @@ this.windows = class extends ExtensionAPI {
|
|||
|
||||
if (createData.incognito !== null) {
|
||||
if (createData.incognito) {
|
||||
if (!PrivateBrowsingUtils.enabled) {
|
||||
return Promise.reject({message: "`incognito` cannot be used if incognito mode is disabled"});
|
||||
}
|
||||
features.push("private");
|
||||
} else {
|
||||
features.push("non-private");
|
||||
|
|
|
@ -2,6 +2,26 @@
|
|||
/* vim: set sts=2 sw=2 et tw=80: */
|
||||
"use strict";
|
||||
|
||||
async function runWithDisabledPrivateBrowsing(callback) {
|
||||
const {
|
||||
EnterprisePolicyTesting,
|
||||
PoliciesPrefTracker,
|
||||
} = ChromeUtils.import("resource://testing-common/EnterprisePolicyTesting.jsm", {});
|
||||
|
||||
PoliciesPrefTracker.start();
|
||||
await EnterprisePolicyTesting.setupPolicyEngineWithJson({
|
||||
policies: {DisablePrivateBrowsing: true},
|
||||
});
|
||||
|
||||
try {
|
||||
await callback();
|
||||
} finally {
|
||||
await EnterprisePolicyTesting.setupPolicyEngineWithJson("");
|
||||
EnterprisePolicyTesting.resetRunOnceState();
|
||||
PoliciesPrefTracker.stop();
|
||||
}
|
||||
}
|
||||
|
||||
add_task(async function test_urlbar_focus() {
|
||||
// Disable preloaded new tab because the urlbar is automatically focused when
|
||||
// a preloaded new tab is opened, while this test is supposed to test that the
|
||||
|
@ -60,3 +80,112 @@ add_task(async function test_urlbar_focus() {
|
|||
|
||||
await extension.unload();
|
||||
});
|
||||
|
||||
add_task(async function default_url() {
|
||||
const extension = ExtensionTestUtils.loadExtension({
|
||||
manifest: {
|
||||
permissions: ["tabs"],
|
||||
},
|
||||
background() {
|
||||
function promiseNonBlankTab() {
|
||||
return new Promise(resolve => {
|
||||
browser.tabs.onUpdated.addListener(function listener(tabId, changeInfo, tab) {
|
||||
if (changeInfo.status === "complete" && tab.url !== "about:blank") {
|
||||
browser.tabs.onUpdated.removeListener(listener);
|
||||
resolve(tab);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
browser.test.onMessage.addListener(async (msg, {incognito, expectedNewWindowUrl, expectedNewTabUrl}) => {
|
||||
browser.test.assertEq("start", msg, `Start test, incognito=${incognito}`);
|
||||
|
||||
let tabPromise = promiseNonBlankTab();
|
||||
let win;
|
||||
try {
|
||||
win = await browser.windows.create({incognito});
|
||||
browser.test.assertEq(1, win.tabs.length, "Expected one tab in the new window.");
|
||||
} catch (e) {
|
||||
browser.test.assertEq(expectedNewWindowUrl, e.message, "Expected error");
|
||||
browser.test.sendMessage("done");
|
||||
return;
|
||||
}
|
||||
let tab = await tabPromise;
|
||||
browser.test.assertEq(expectedNewWindowUrl, tab.url, "Expected default URL of new window");
|
||||
|
||||
tabPromise = promiseNonBlankTab();
|
||||
await browser.tabs.create({windowId: win.id});
|
||||
tab = await tabPromise;
|
||||
browser.test.assertEq(expectedNewTabUrl, tab.url, "Expected default URL of new tab");
|
||||
|
||||
await browser.windows.remove(win.id);
|
||||
browser.test.sendMessage("done");
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
await extension.startup();
|
||||
|
||||
extension.sendMessage("start", {
|
||||
incognito: false,
|
||||
expectedNewWindowUrl: "about:home",
|
||||
expectedNewTabUrl: "about:newtab",
|
||||
});
|
||||
await extension.awaitMessage("done");
|
||||
extension.sendMessage("start", {
|
||||
incognito: true,
|
||||
expectedNewWindowUrl: "about:privatebrowsing",
|
||||
expectedNewTabUrl: "about:privatebrowsing",
|
||||
});
|
||||
await extension.awaitMessage("done");
|
||||
|
||||
info("Testing with multiple homepages.");
|
||||
await SpecialPowers.pushPrefEnv({set: [["browser.startup.homepage", "about:robots|about:blank|about:home"]]});
|
||||
extension.sendMessage("start", {
|
||||
incognito: false,
|
||||
expectedNewWindowUrl: "about:robots",
|
||||
expectedNewTabUrl: "about:newtab",
|
||||
});
|
||||
await extension.awaitMessage("done");
|
||||
extension.sendMessage("start", {
|
||||
incognito: true,
|
||||
expectedNewWindowUrl: "about:privatebrowsing",
|
||||
expectedNewTabUrl: "about:privatebrowsing",
|
||||
});
|
||||
await extension.awaitMessage("done");
|
||||
await SpecialPowers.popPrefEnv();
|
||||
|
||||
info("Testing with perma-private browsing mode.");
|
||||
await SpecialPowers.pushPrefEnv({set: [["browser.privatebrowsing.autostart", true]]});
|
||||
extension.sendMessage("start", {
|
||||
incognito: false,
|
||||
expectedNewWindowUrl: "about:home",
|
||||
expectedNewTabUrl: "about:newtab",
|
||||
});
|
||||
await extension.awaitMessage("done");
|
||||
extension.sendMessage("start", {
|
||||
incognito: true,
|
||||
expectedNewWindowUrl: "about:home",
|
||||
expectedNewTabUrl: "about:newtab",
|
||||
});
|
||||
await extension.awaitMessage("done");
|
||||
await SpecialPowers.popPrefEnv();
|
||||
|
||||
info("Testing with disabled private browsing mode.");
|
||||
await runWithDisabledPrivateBrowsing(async () => {
|
||||
extension.sendMessage("start", {
|
||||
incognito: false,
|
||||
expectedNewWindowUrl: "about:home",
|
||||
expectedNewTabUrl: "about:newtab",
|
||||
});
|
||||
await extension.awaitMessage("done");
|
||||
extension.sendMessage("start", {
|
||||
incognito: true,
|
||||
expectedNewWindowUrl: "`incognito` cannot be used if incognito mode is disabled",
|
||||
});
|
||||
await extension.awaitMessage("done");
|
||||
});
|
||||
|
||||
await extension.unload();
|
||||
});
|
||||
|
|
|
@ -74,7 +74,7 @@ add_task(async function valid_cookieStoreId() {
|
|||
"firefox-container-1",
|
||||
],
|
||||
expectedExecuteScriptResult: [
|
||||
// Default URL is about:newtab, and extensions cannot run scripts in it.
|
||||
// Default URL is about:home, and extensions cannot run scripts in it.
|
||||
"Missing host permission for the tab",
|
||||
],
|
||||
}, {
|
||||
|
@ -169,7 +169,7 @@ add_task(async function valid_cookieStoreId() {
|
|||
for (let [i, expectedResult] of Object.entries(expectedExecuteScriptResult)) {
|
||||
// Wait until the the tab can process the tabs.executeScript calls.
|
||||
// TODO: Remove this when bug 1418655 and bug 1397667 are fixed.
|
||||
let expectedUrl = Array.isArray(createParams.url) ? createParams.url[i] : createParams.url || "about:newtab";
|
||||
let expectedUrl = Array.isArray(createParams.url) ? createParams.url[i] : createParams.url || "about:home";
|
||||
await awaitTabReady(win.tabs[i].id, expectedUrl);
|
||||
|
||||
let result = await executeScriptAndGetResult(win.tabs[i].id);
|
||||
|
|
|
@ -53,7 +53,16 @@ function installDistributionEngine() {
|
|||
});
|
||||
}
|
||||
|
||||
function run_test() {
|
||||
registerCleanupFunction(async function() {
|
||||
// Remove the distribution dir, even if the test failed, otherwise all
|
||||
// next tests will use it.
|
||||
let folderPath = OS.Path.join(OS.Constants.Path.profileDir, "distribution");
|
||||
await OS.File.removeDir(folderPath, { ignoreAbsent: true });
|
||||
Assert.ok(!(await OS.File.exists(folderPath)));
|
||||
Services.prefs.clearUserPref("distribution.testing.loadFromProfile");
|
||||
});
|
||||
|
||||
add_task(async function() {
|
||||
// Set special pref to load distribution.ini from the profile folder.
|
||||
Services.prefs.setBoolPref("distribution.testing.loadFromProfile", true);
|
||||
|
||||
|
@ -73,18 +82,6 @@ function run_test() {
|
|||
Assert.ok(testDistributionFile.exists());
|
||||
|
||||
installDistributionEngine();
|
||||
|
||||
run_next_test();
|
||||
}
|
||||
|
||||
registerCleanupFunction(function() {
|
||||
// Remove the distribution dir, even if the test failed, otherwise all
|
||||
// next tests will use it.
|
||||
let distDir = gProfD.clone();
|
||||
distDir.append("distribution");
|
||||
distDir.remove(true);
|
||||
Assert.ok(!distDir.exists());
|
||||
Services.prefs.clearUserPref("distribution.testing.loadFromProfile");
|
||||
});
|
||||
|
||||
add_task(async function() {
|
||||
|
@ -147,14 +144,14 @@ add_task(async function() {
|
|||
// Language should not override locale
|
||||
Assert.notEqual(defaultBranch.getComplexValue("distribution.test.locale.set", Ci.nsIPrefLocalizedString).data, "Language Set");
|
||||
|
||||
do_test_pending();
|
||||
|
||||
Services.prefs.setCharPref("distribution.searchplugins.defaultLocale", "de-DE");
|
||||
|
||||
await new Promise(resolve => {
|
||||
Services.search.init(function() {
|
||||
Assert.equal(Services.search.isInitialized, true);
|
||||
var engine = Services.search.getEngineByName("Google");
|
||||
Assert.equal(engine.description, "override-de-DE");
|
||||
do_test_finished();
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -31,8 +31,8 @@ add_task(async function() {
|
|||
});
|
||||
|
||||
function testAccordionStateAfterClickingHeader(doc) {
|
||||
const header = doc.querySelector("#layout-container .box-model-pane ._header");
|
||||
const bContent = doc.querySelector("#layout-container .box-model-pane ._content");
|
||||
const header = doc.querySelector(".layout-container .box-model-pane ._header");
|
||||
const bContent = doc.querySelector(".layout-container .box-model-pane ._content");
|
||||
|
||||
info("Checking initial state of the box model panel.");
|
||||
is(bContent.style.display, "block", "The box model panel content is 'display: block'.");
|
||||
|
@ -51,7 +51,7 @@ function testAccordionStateAfterClickingHeader(doc) {
|
|||
function testAccordionStateAfterSwitchingSidebars(inspector, doc) {
|
||||
info("Checking the box model accordion state is persistent after switching sidebars.");
|
||||
|
||||
const bContent = doc.querySelector("#layout-container .box-model-pane ._content");
|
||||
const bContent = doc.querySelector(".layout-container .box-model-pane ._content");
|
||||
|
||||
info("Selecting the computed view.");
|
||||
inspector.sidebar.select("computedview");
|
||||
|
@ -75,7 +75,7 @@ async function testAccordionStateAfterReopeningLayoutView(toolbox) {
|
|||
info("Re-opening the layout view.");
|
||||
const { boxmodel } = await openLayoutView();
|
||||
const { document: doc } = boxmodel;
|
||||
const bContent = doc.querySelector("#layout-container .box-model-pane ._content");
|
||||
const bContent = doc.querySelector(".layout-container .box-model-pane ._content");
|
||||
|
||||
info("Checking the state of the box model panel.");
|
||||
ok(!bContent, "The box model panel content is not rendered.");
|
||||
|
|
|
@ -46,10 +46,7 @@ class FlexItemSizingOutline extends PureComponent {
|
|||
);
|
||||
}
|
||||
|
||||
renderFinalOutline(mainFinalSize, mainMaxSize, mainMinSize) {
|
||||
const isClamped = mainFinalSize === mainMaxSize ||
|
||||
mainFinalSize === mainMinSize;
|
||||
|
||||
renderFinalOutline(mainFinalSize, mainMaxSize, mainMinSize, isClamped) {
|
||||
return (
|
||||
dom.div({
|
||||
className: "flex-outline-final" + (isClamped ? " clamped" : "")
|
||||
|
@ -62,13 +59,19 @@ class FlexItemSizingOutline extends PureComponent {
|
|||
}
|
||||
|
||||
render() {
|
||||
const {
|
||||
flexItemSizing,
|
||||
properties,
|
||||
} = this.props.flexItem;
|
||||
const {
|
||||
mainBaseSize,
|
||||
mainDeltaSize,
|
||||
mainMaxSize,
|
||||
mainMinSize,
|
||||
} = this.props.flexItem.flexItemSizing;
|
||||
} = flexItemSizing;
|
||||
|
||||
const isRow = this.props.flexDirection.startsWith("row");
|
||||
const dimension = isRow ? "width" : "height";
|
||||
|
||||
// Calculate the final size. This is base + delta, then clamped by min or max.
|
||||
let mainFinalSize = mainBaseSize + mainDeltaSize;
|
||||
|
@ -76,10 +79,17 @@ class FlexItemSizingOutline extends PureComponent {
|
|||
mainFinalSize = Math.min(mainFinalSize, mainMaxSize);
|
||||
|
||||
// The max size is only interesting to show if it did clamp the item
|
||||
// TODO: replace this with the new clamping state that the API will return once bug
|
||||
// 1498273 is fixed.
|
||||
const showMax = mainMaxSize === mainFinalSize;
|
||||
|
||||
// The min size is only really interesting if it actually clamped the item.
|
||||
const showMin = mainMinSize === mainFinalSize;
|
||||
// Just checking that the main size = final size isn't enough because this may be true
|
||||
// if the max content size is the final size. So also check that min-width/height is
|
||||
// set.
|
||||
// TODO: replace this with the new clamping state that the API will return once bug
|
||||
// 1498273 is fixed.
|
||||
const showMin = mainMinSize === mainFinalSize && properties[`min-${dimension}`];
|
||||
|
||||
// Sort all of the dimensions in order to come up with a grid track template.
|
||||
// Make mainDeltaSize start from the same point as the other ones so we can compare.
|
||||
|
@ -137,7 +147,8 @@ class FlexItemSizingOutline extends PureComponent {
|
|||
showMax ? this.renderPoint("max") : null,
|
||||
this.renderBasisOutline(mainBaseSize),
|
||||
this.renderDeltaOutline(mainDeltaSize),
|
||||
this.renderFinalOutline(mainFinalSize, mainMaxSize, mainMinSize)
|
||||
this.renderFinalOutline(mainFinalSize, mainMaxSize, mainMinSize,
|
||||
showMin || showMax)
|
||||
)
|
||||
)
|
||||
);
|
||||
|
|
|
@ -19,6 +19,300 @@ class FlexItemSizingProperties extends PureComponent {
|
|||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Rounds some dimension in pixels and returns a string to be displayed to the user.
|
||||
* The string will end with 'px'. If the number is 0, the string "0" is returned.
|
||||
*
|
||||
* @param {Number} value
|
||||
* The number to be rounded
|
||||
* @return {String}
|
||||
* Representation of the rounded number
|
||||
*/
|
||||
getRoundedDimension(value) {
|
||||
if (value == 0) {
|
||||
return "0";
|
||||
}
|
||||
return (Math.round(value * 100) / 100) + "px";
|
||||
}
|
||||
|
||||
/**
|
||||
* Format the flexibility value into a meaningful value for the UI.
|
||||
* If the item grew, then prepend a + sign, if it shrank, prepend a - sign.
|
||||
* If it didn't flex, return "0".
|
||||
*
|
||||
* @param {Boolean} grew
|
||||
* Whether the item grew or not
|
||||
* @param {Number} value
|
||||
* The amount of pixels the item flexed
|
||||
* @return {String}
|
||||
* Representation of the flexibility value
|
||||
*/
|
||||
getFlexibilityValueString(grew, mainDeltaSize) {
|
||||
const value = this.getRoundedDimension(mainDeltaSize);
|
||||
|
||||
if (grew) {
|
||||
return "+" + value;
|
||||
}
|
||||
|
||||
return value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Render an authored CSS property.
|
||||
*
|
||||
* @param {String} name
|
||||
* The name for this CSS property
|
||||
* @param {String} value
|
||||
* The property value
|
||||
* @param {Booleam} isDefaultValue
|
||||
* Whether the value come from the browser default style
|
||||
* @return {Object}
|
||||
* The React component representing this CSS property
|
||||
*/
|
||||
renderCssProperty(name, value, isDefaultValue) {
|
||||
return (
|
||||
dom.span({ className: "css-property-link" },
|
||||
dom.span({ className: "theme-fg-color5" }, name),
|
||||
": ",
|
||||
dom.span({ className: "theme-fg-color1" }, value),
|
||||
";"
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Render a list of sentences to be displayed in the UI as reasons why a certain sizing
|
||||
* value happened.
|
||||
*
|
||||
* @param {Array} sentences
|
||||
* The list of sentences as Strings
|
||||
* @return {Object}
|
||||
* The React component representing these sentences
|
||||
*/
|
||||
renderReasons(sentences) {
|
||||
return (
|
||||
dom.ul({ className: "reasons" },
|
||||
sentences.map(sentence => dom.li({}, sentence))
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
renderBaseSizeSection({ mainBaseSize, mainMinSize }, properties, dimension) {
|
||||
const flexBasisValue = properties["flex-basis"];
|
||||
const dimensionValue = properties[dimension];
|
||||
const minDimensionValue = properties[`min-${dimension}`];
|
||||
const hasMinClamping = mainMinSize && mainMinSize === mainBaseSize;
|
||||
|
||||
let property = null;
|
||||
let reason = null;
|
||||
|
||||
if (hasMinClamping && minDimensionValue) {
|
||||
// If min clamping happened, then the base size is going to be that value.
|
||||
// TODO: this isn't going to be necessarily true after bug 1498273 is fixed.
|
||||
property = this.renderCssProperty(`min-${dimension}`, minDimensionValue);
|
||||
} else if (flexBasisValue && !hasMinClamping) {
|
||||
// If flex-basis is defined, then that's what is used for the base size.
|
||||
property = this.renderCssProperty("flex-basis", flexBasisValue);
|
||||
} else if (dimensionValue) {
|
||||
// If not and width/height is defined, then that's what defines the base size.
|
||||
property = this.renderCssProperty(dimension, dimensionValue);
|
||||
} else {
|
||||
// Finally, if nothing is set, then the base size is the max-content size.
|
||||
reason = this.renderReasons(
|
||||
[getStr("flexbox.itemSizing.itemBaseSizeFromContent")]);
|
||||
}
|
||||
|
||||
return (
|
||||
dom.li({ className: property ? "section" : "section no-property" },
|
||||
dom.span({ className: "name" },
|
||||
getStr("flexbox.itemSizing.baseSizeSectionHeader")
|
||||
),
|
||||
dom.span({ className: "value theme-fg-color1" },
|
||||
this.getRoundedDimension(mainBaseSize)
|
||||
),
|
||||
property,
|
||||
reason
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
renderFlexibilitySection(flexItemSizing, properties) {
|
||||
const {
|
||||
mainDeltaSize,
|
||||
mainBaseSize,
|
||||
mainFinalSize,
|
||||
lineGrowthState
|
||||
} = flexItemSizing;
|
||||
|
||||
const flexGrow = properties["flex-grow"];
|
||||
const flexGrow0 = parseFloat(flexGrow) === 0;
|
||||
const flexShrink = properties["flex-shrink"];
|
||||
const flexShrink0 = parseFloat(flexShrink) === 0;
|
||||
const grew = mainDeltaSize > 0;
|
||||
const shrank = mainDeltaSize < 0;
|
||||
// TODO: replace this with the new clamping state that the API will return once bug
|
||||
// 1498273 is fixed.
|
||||
const wasClamped = mainDeltaSize + mainBaseSize !== mainFinalSize;
|
||||
|
||||
const reasons = [];
|
||||
|
||||
// First output a sentence for telling users about whether there was enough room or
|
||||
// not on the line.
|
||||
if (lineGrowthState === "growing") {
|
||||
reasons.push(getStr("flexbox.itemSizing.extraRoomOnLine"));
|
||||
} else if (lineGrowthState === "shrinking") {
|
||||
reasons.push(getStr("flexbox.itemSizing.notEnoughRoomOnLine"));
|
||||
}
|
||||
|
||||
// Then tell users whether the item was set to grow, shrink or none of them.
|
||||
if (flexGrow && !flexGrow0 && lineGrowthState !== "shrinking") {
|
||||
reasons.push(getStr("flexbox.itemSizing.setToGrow"));
|
||||
}
|
||||
if (flexShrink && !flexShrink0 && lineGrowthState !== "growing") {
|
||||
reasons.push(getStr("flexbox.itemSizing.setToShrink"));
|
||||
}
|
||||
if (!grew && !shrank && lineGrowthState === "growing") {
|
||||
reasons.push(getStr("flexbox.itemSizing.notSetToGrow"));
|
||||
}
|
||||
if (!grew && !shrank && lineGrowthState === "shrinking") {
|
||||
reasons.push(getStr("flexbox.itemSizing.notSetToShrink"));
|
||||
}
|
||||
|
||||
let property = null;
|
||||
|
||||
if (grew) {
|
||||
// If the item grew.
|
||||
if (flexGrow) {
|
||||
// It's normally because it was set to grow (flex-grow is non 0).
|
||||
property = this.renderCssProperty("flex-grow", flexGrow);
|
||||
}
|
||||
|
||||
if (wasClamped) {
|
||||
// It may have wanted to grow more than it did, because it was later max-clamped.
|
||||
reasons.push(getStr("flexbox.itemSizing.growthAttemptWhenClamped"));
|
||||
}
|
||||
} else if (shrank) {
|
||||
// If the item shrank.
|
||||
if (flexShrink && !flexShrink0) {
|
||||
// It's either because flex-shrink is non 0.
|
||||
property = this.renderCssProperty("flex-shrink", flexShrink);
|
||||
} else {
|
||||
// Or also because it's default value is 1 anyway.
|
||||
property = this.renderCssProperty("flex-shrink", "1", true);
|
||||
}
|
||||
|
||||
if (wasClamped) {
|
||||
// It might have wanted to shrink more (to accomodate all items) but couldn't
|
||||
// because it was later min-clamped.
|
||||
reasons.push(getStr("flexbox.itemSizing.shrinkAttemptWhenClamped"));
|
||||
}
|
||||
} else if (lineGrowthState === "growing" && flexGrow && !flexGrow0) {
|
||||
// The item did not grow or shrink. There was room on the line and flex-grow was
|
||||
// set, other items have likely used up all of the space.
|
||||
property = this.renderCssProperty("flex-grow", flexGrow);
|
||||
reasons.push(getStr("flexbox.itemSizing.growthAttemptButSiblings"));
|
||||
} else if (lineGrowthState === "shrinking") {
|
||||
// The item did not grow or shrink and there wasn't enough room on the line.
|
||||
if (!flexShrink0) {
|
||||
// flex-shrink was set (either defined in CSS, or via its default value of 1).
|
||||
// but the item didn't shrink.
|
||||
if (flexShrink) {
|
||||
property = this.renderCssProperty("flex-shrink", flexShrink);
|
||||
} else {
|
||||
property = this.renderCssProperty("flex-shrink", 1, true);
|
||||
}
|
||||
|
||||
reasons.push(getStr("flexbox.itemSizing.shrinkAttemptButCouldnt"));
|
||||
|
||||
if (wasClamped) {
|
||||
// Maybe it was clamped.
|
||||
reasons.push(getStr("flexbox.itemSizing.shrinkAttemptWhenClamped"));
|
||||
}
|
||||
} else {
|
||||
// flex-shrink was set to 0, so it didn't shrink.
|
||||
property = this.renderCssProperty("flex-shrink", flexShrink);
|
||||
}
|
||||
}
|
||||
|
||||
// Don't display the section at all if there's nothing useful to show users.
|
||||
if (!property && !reasons.length) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
dom.li({ className: property ? "section" : "section no-property" },
|
||||
dom.span({ className: "name" },
|
||||
getStr("flexbox.itemSizing.flexibilitySectionHeader")
|
||||
),
|
||||
dom.span({ className: "value theme-fg-color1" },
|
||||
this.getFlexibilityValueString(grew, mainDeltaSize)
|
||||
),
|
||||
property,
|
||||
this.renderReasons(reasons)
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
renderMinimumSizeSection({ mainMinSize, mainFinalSize }, properties, dimension) {
|
||||
// We only display the minimum size when the item actually violates that size during
|
||||
// layout & is clamped.
|
||||
// For now, we detect this by checking that the min-size is the same as the final size
|
||||
// and that a min-size is actually defined in CSS.
|
||||
// TODO: replace this with the new clamping state that the API will return once bug
|
||||
// 1498273 is fixed.
|
||||
const minDimensionValue = properties[`min-${dimension}`];
|
||||
if (mainMinSize !== mainFinalSize || !minDimensionValue) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
dom.li({ className: "section" },
|
||||
dom.span({ className: "name" },
|
||||
getStr("flexbox.itemSizing.minSizeSectionHeader")
|
||||
),
|
||||
dom.span({ className: "value theme-fg-color1" },
|
||||
this.getRoundedDimension(mainMinSize)
|
||||
),
|
||||
this.renderCssProperty(`min-${dimension}`, minDimensionValue)
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
renderMaximumSizeSection({ mainMaxSize, mainFinalSize }, properties, dimension) {
|
||||
// TODO: replace this with the new clamping state that the API will return once bug
|
||||
// 1498273 is fixed.
|
||||
if (mainMaxSize !== mainFinalSize) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const maxDimensionValue = properties[`max-${dimension}`];
|
||||
|
||||
return (
|
||||
dom.li({ className: "section" },
|
||||
dom.span({ className: "name" },
|
||||
getStr("flexbox.itemSizing.maxSizeSectionHeader")
|
||||
),
|
||||
dom.span({ className: "value theme-fg-color1" },
|
||||
this.getRoundedDimension(mainMaxSize)
|
||||
),
|
||||
this.renderCssProperty(`max-${dimension}`, maxDimensionValue)
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
renderFinalSizeSection({ mainFinalSize }) {
|
||||
return (
|
||||
dom.li({ className: "section no-property" },
|
||||
dom.span({ className: "name" },
|
||||
getStr("flexbox.itemSizing.finalSizeSectionHeader")
|
||||
),
|
||||
dom.span({ className: "value theme-fg-color1" },
|
||||
this.getRoundedDimension(mainFinalSize)
|
||||
)
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
render() {
|
||||
const {
|
||||
flexDirection,
|
||||
|
@ -28,46 +322,27 @@ class FlexItemSizingProperties extends PureComponent {
|
|||
flexItemSizing,
|
||||
properties,
|
||||
} = flexItem;
|
||||
const {
|
||||
mainBaseSize,
|
||||
mainDeltaSize,
|
||||
mainMaxSize,
|
||||
mainMinSize,
|
||||
} = flexItemSizing;
|
||||
const dimension = flexDirection.startsWith("row") ? "width" : "height";
|
||||
const contentStr = dimension === "width" ?
|
||||
getStr("flexbox.contentWidth") : getStr("flexbox.contentHeight");
|
||||
const finalStr = dimension === "width" ?
|
||||
getStr("flexbox.finalWidth") : getStr("flexbox.finalHeight");
|
||||
|
||||
// Calculate the final size. This is base + delta, then clamped by min or max.
|
||||
let mainFinalSize = mainBaseSize + mainDeltaSize;
|
||||
mainFinalSize = Math.max(mainFinalSize, mainMinSize);
|
||||
mainFinalSize = Math.min(mainFinalSize, mainMaxSize);
|
||||
flexItemSizing.mainFinalSize = mainFinalSize;
|
||||
|
||||
return (
|
||||
dom.ol(
|
||||
{
|
||||
id: "flex-item-sizing-properties",
|
||||
className: "flex-item-list",
|
||||
},
|
||||
dom.li({},
|
||||
dom.span({}, "flex-basis: "),
|
||||
properties["flex-basis"]
|
||||
),
|
||||
dom.li({},
|
||||
dom.span({}, "flex-grow: "),
|
||||
properties["flex-grow"]
|
||||
),
|
||||
dom.li({},
|
||||
dom.span({}, "flex-shrink: "),
|
||||
properties["flex-shrink"]
|
||||
),
|
||||
dom.li({},
|
||||
dom.span({}, `${contentStr} `),
|
||||
`${parseFloat(flexItemSizing.mainBaseSize.toPrecision(6))}px`
|
||||
),
|
||||
dom.li({},
|
||||
dom.span({}, `Min-${dimension}: `),
|
||||
properties["min-" + dimension]
|
||||
),
|
||||
dom.li({},
|
||||
dom.span({}, `Max-${dimension}: `),
|
||||
properties["max-" + dimension]
|
||||
),
|
||||
dom.li({},
|
||||
dom.span({}, `${finalStr} `),
|
||||
`${parseFloat(properties[dimension].toPrecision(6))}px`
|
||||
)
|
||||
dom.ul({ className: "flex-item-sizing" },
|
||||
this.renderBaseSizeSection(flexItemSizing, properties, dimension),
|
||||
this.renderFlexibilitySection(flexItemSizing, properties),
|
||||
this.renderMinimumSizeSection(flexItemSizing, properties, dimension),
|
||||
this.renderMaximumSizeSection(flexItemSizing, properties, dimension),
|
||||
this.renderFinalSizeSection(flexItemSizing)
|
||||
)
|
||||
);
|
||||
}
|
||||
|
|
|
@ -9,5 +9,7 @@ support-files =
|
|||
!/devtools/client/shared/test/shared-head.js
|
||||
|
||||
[browser_flexbox_item_outline_exists.js]
|
||||
[browser_flexbox_item_outline_rotates_for_column.js]
|
||||
[browser_flexbox_item_outline_has_correct_layout.js]
|
||||
[browser_flexbox_item_outline_rotates_for_column.js]
|
||||
[browser_flexbox_sizing_info_exists.js]
|
||||
[browser_flexbox_sizing_info_has_correct_sections.js]
|
||||
|
|
|
@ -0,0 +1,32 @@
|
|||
/* vim: set ts=2 et sw=2 tw=80: */
|
||||
/* Any copyright is dedicated to the Public Domain.
|
||||
http://creativecommons.org/publicdomain/zero/1.0/ */
|
||||
|
||||
"use strict";
|
||||
|
||||
// Test that the flex item sizing information exists when a flex item is selected.
|
||||
|
||||
const TEST_URI = URL_ROOT + "doc_flexbox_simple.html";
|
||||
|
||||
add_task(async function() {
|
||||
await addTab(TEST_URI);
|
||||
const { inspector, flexboxInspector } = await openLayoutView();
|
||||
const { document: doc } = flexboxInspector;
|
||||
|
||||
// Select a flex item in the test document and wait for the sizing info to be rendered.
|
||||
// Note that we select an item that has base, delta and final sizes, so we can check
|
||||
// those sections exists.
|
||||
const onFlexItemSizingRendered = waitForDOM(doc, "ul.flex-item-sizing");
|
||||
await selectNode(".container.growing .item", inspector);
|
||||
const [flexSizingContainer] = await onFlexItemSizingRendered;
|
||||
|
||||
ok(flexSizingContainer, "The flex sizing exists in the DOM");
|
||||
|
||||
info("Check that the base, flexibility and final sizes are displayed");
|
||||
const allSections = [...flexSizingContainer.querySelectorAll(".section .name")];
|
||||
const allSectionTitles = allSections.map(el => el.textContent);
|
||||
const expectedTitles = ["Base Size", "Flexibility", "Final Size"];
|
||||
|
||||
ok(expectedTitles.every(title => allSectionTitles.includes(title)),
|
||||
"The 3 main sizing sections where found");
|
||||
});
|
|
@ -0,0 +1,53 @@
|
|||
/* vim: set ts=2 et sw=2 tw=80: */
|
||||
/* Any copyright is dedicated to the Public Domain.
|
||||
http://creativecommons.org/publicdomain/zero/1.0/ */
|
||||
|
||||
"use strict";
|
||||
|
||||
// Test that the flex item sizing UI contains the right sections, depending on which
|
||||
// element is selected. Some items may be clamped, others not, so not all sections are
|
||||
// visible at all times.
|
||||
|
||||
const TEST_URI = URL_ROOT + "doc_flexbox_simple.html";
|
||||
|
||||
const TEST_DATA = [{
|
||||
selector: ".shrinking .item",
|
||||
expectedSections: ["Base Size", "Flexibility", "Final Size"]
|
||||
}, {
|
||||
selector: ".shrinking.is-clamped .item",
|
||||
expectedSections: ["Base Size", "Flexibility", "Minimum Size", "Final Size"]
|
||||
}, {
|
||||
selector: ".growing .item",
|
||||
expectedSections: ["Base Size", "Flexibility", "Final Size"]
|
||||
}, {
|
||||
selector: ".growing.is-clamped .item",
|
||||
expectedSections: ["Base Size", "Flexibility", "Maximum Size", "Final Size"]
|
||||
}];
|
||||
|
||||
add_task(async function() {
|
||||
await addTab(TEST_URI);
|
||||
const { inspector, flexboxInspector } = await openLayoutView();
|
||||
const { document: doc } = flexboxInspector;
|
||||
|
||||
for (const { selector, expectedSections } of TEST_DATA) {
|
||||
info(`Checking the list of sections for the flex item ${selector}`);
|
||||
const sections = await selectNodeAndGetFlexSizingSections(selector, inspector, doc);
|
||||
|
||||
is(sections.length, expectedSections.length, "Correct number of sections found");
|
||||
expectedSections.forEach((expectedSection, i) => {
|
||||
is(sections[i], expectedSection, `The ${expectedSection} section was found`);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
async function selectNodeAndGetFlexSizingSections(selector, inspector, doc) {
|
||||
const onFlexItemSizingRendered = waitForDOM(doc, "ul.flex-item-sizing");
|
||||
await selectNode(selector, inspector);
|
||||
const [flexSizingContainer] = await onFlexItemSizingRendered;
|
||||
|
||||
info(`Getting the list of displayed sections for ${selector}`);
|
||||
const allSections = [...flexSizingContainer.querySelectorAll(".section .name")];
|
||||
const allSectionTitles = allSections.map(el => el.textContent);
|
||||
|
||||
return allSectionTitles;
|
||||
}
|
|
@ -139,7 +139,7 @@ class LayoutApp extends PureComponent {
|
|||
}
|
||||
|
||||
return (
|
||||
dom.div({ id: "layout-container" },
|
||||
dom.div({ className: "layout-container" },
|
||||
Accordion({ items })
|
||||
)
|
||||
);
|
||||
|
|
|
@ -32,6 +32,87 @@ flexbox.contentHeight=Content height:
|
|||
flexbox.finalWidth=Final width:
|
||||
flexbox.finalHeight=Final height:
|
||||
|
||||
# LOCALIZATION NOTE (flexbox.itemSizing.baseSizeSectionHeader): Header label displayed
|
||||
# at the start of the flex item sizing Base Size section.
|
||||
flexbox.itemSizing.baseSizeSectionHeader=Base Size
|
||||
|
||||
# LOCALIZATION NOTE (flexbox.itemSizing.flexibilitySectionHeader): Header label displayed
|
||||
# at the start of the flex item sizing Flexibility section.
|
||||
flexbox.itemSizing.flexibilitySectionHeader=Flexibility
|
||||
|
||||
# LOCALIZATION NOTE (flexbox.itemSizing.minSizeSectionHeader): Header label displayed
|
||||
# at the start of the flex item sizing Minimum Size section.
|
||||
flexbox.itemSizing.minSizeSectionHeader=Minimum Size
|
||||
|
||||
# LOCALIZATION NOTE (flexbox.itemSizing.maxSizeSectionHeader): Header label displayed at
|
||||
# the start of the flex item sizing Maximum Size section.
|
||||
flexbox.itemSizing.maxSizeSectionHeader=Maximum Size
|
||||
|
||||
# LOCALIZATION NOTE (flexbox.itemSizing.finalSizeSectionHeader): Header label displayed at
|
||||
# the start of the flex item sizing Final Size section.
|
||||
flexbox.itemSizing.finalSizeSectionHeader=Final Size
|
||||
|
||||
# LOCALIZATION NOTE (flexbox.itemSizing.itemBaseSizeFromContent): Label shown in the flex
|
||||
# item sizing panel. It tells users that a given item’s base size was calculated from its
|
||||
# content size when unconstrained.
|
||||
flexbox.itemSizing.itemBaseSizeFromContent=The item’s content size when unconstrained.
|
||||
|
||||
# LOCALIZATION NOTE (flexbox.itemSizing.itemMinSizeFromItemMinContent): Label shown in the
|
||||
# flex item sizing panel. It tells users that a given item’s minimum size is coming from
|
||||
# its min-content size.
|
||||
flexbox.itemSizing.itemMinSizeFromItemMinContent=This is the element’s minimum content size.
|
||||
|
||||
# LOCALIZATION NOTE (flexbox.itemSizing.extraRoomOnLine): Label shown in the flexbox item
|
||||
# sizing panel. It tells users that there was extra room to distribute inside a given flex
|
||||
# line.
|
||||
flexbox.itemSizing.extraRoomOnLine=There was extra room available on the flex line.
|
||||
|
||||
# LOCALIZATION NOTE (flexbox.itemSizing.notEnoughRoomOnLine): Label shown in the flexbox
|
||||
# item sizing panel. It tells users that there wasn’t enough room inside a given flex line
|
||||
# for all of its items.
|
||||
flexbox.itemSizing.notEnoughRoomOnLine=There wasn’t enough room available on the flex line.
|
||||
|
||||
# LOCALIZATION NOTE (flexbox.itemSizing.growthAttemptWhenClamped): Label shown in the
|
||||
# flexbox item sizing panel. It tells users that a given item attempted to grow by a
|
||||
# certain amount but ended up being clamped by a max size.
|
||||
# (note that clamp is a common word in flexbox terminology. It refers to constraining an
|
||||
# item's size to some defined min/max-width/height set on the element, even though there
|
||||
# might have been room for it to grow, or reason for it to shrink more).
|
||||
flexbox.itemSizing.growthAttemptWhenClamped=The item wanted to grow, but it was clamped.
|
||||
|
||||
# LOCALIZATION NOTE (flexbox.itemSizing.shrinkAttemptWhenClamped): Label shown in the
|
||||
# flexbox item sizing panel. It tells users that a given item attempted to shrink by a
|
||||
# certain amount but ended up being clamped by a min size.
|
||||
flexbox.itemSizing.shrinkAttemptWhenClamped=The item wanted to shrink, but it was clamped.
|
||||
|
||||
# LOCALIZATION NOTE (flexbox.itemSizing.shrinkAttemptButCouldnt): Label shown in the
|
||||
# flexbox item sizing panel. It tells users that a given item attempted to shrink by a
|
||||
# certain amount but could not
|
||||
flexbox.itemSizing.shrinkAttemptButCouldnt=Item was set to shrink but could not.
|
||||
|
||||
# LOCALIZATION NOTE (flexbox.itemSizing.growthAttemptButSiblings): Label shown in the
|
||||
# flexbox item sizing panel. It tells users that a given item could not grow to occupy
|
||||
# extra space because its siblings have likely already used it.
|
||||
flexbox.itemSizing.growthAttemptButSiblings=Item could not grow, siblings have likely used the extra space.
|
||||
|
||||
# LOCALIZATION NOTE (flexbox.itemSizing.setToGrow): Label shown in the flex item sizing
|
||||
# panel. It tells users that a given item was set to grow.
|
||||
flexbox.itemSizing.setToGrow=Item was set to grow.
|
||||
|
||||
# LOCALIZATION NOTE (flexbox.itemSizing.setToShrink): Label shown in the flexbox item
|
||||
# sizing panel. It tells users that a given item was set to shrink.
|
||||
flexbox.itemSizing.setToShrink=Item was set to shrink.
|
||||
|
||||
# LOCALIZATION NOTE (flexbox.itemSizing.notSetToGrow): Label shown in the
|
||||
# flexbox item sizing panel. It tells users that a given item was not set to grow, even
|
||||
# though there might have been space on the flex line for it to grow.
|
||||
flexbox.itemSizing.notSetToGrow=Item was not set to grow.
|
||||
|
||||
# LOCALIZATION NOTE (flexbox.itemSizing.notSetToShrink): Label shown in the
|
||||
# flexbox item sizing panel. It tells users that a given item did not shrink even though
|
||||
# there might not have been enough space on the flex line for all items to fit.
|
||||
flexbox.itemSizing.notSetToShrink=Item was not set to shrink.
|
||||
|
||||
# LOCALIZATION NOTE (layout.cannotShowGridOutline, layout.cannotSHowGridOutline.title):
|
||||
# In the case where the grid outline cannot be effectively displayed.
|
||||
layout.cannotShowGridOutline=Cannot show outline for this grid
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
#layout-container {
|
||||
.layout-container {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
overflow-y: auto;
|
||||
|
@ -10,7 +10,7 @@
|
|||
min-width: 200px;
|
||||
}
|
||||
|
||||
#layout-container .accordion ._content {
|
||||
.layout-container .accordion ._content {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
|
@ -24,17 +24,17 @@
|
|||
* Common styles for the layout container
|
||||
*/
|
||||
|
||||
#layout-container li {
|
||||
.layout-container li {
|
||||
padding: 3px 0;
|
||||
-moz-user-select: none;
|
||||
}
|
||||
|
||||
#layout-container input {
|
||||
.layout-container input {
|
||||
margin-inline-end: 7px;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
#layout-container label {
|
||||
.layout-container label {
|
||||
margin-inline-start: -3px;
|
||||
}
|
||||
|
||||
|
@ -363,12 +363,47 @@
|
|||
* Flex Item Sizing Properties
|
||||
*/
|
||||
|
||||
#flex-item-sizing-properties {
|
||||
padding-top: 0;
|
||||
.flex-item-sizing {
|
||||
margin: 20px;
|
||||
padding: 0;
|
||||
list-style: none;
|
||||
}
|
||||
|
||||
#flex-item-sizing-properties span {
|
||||
.flex-item-sizing .section {
|
||||
--padding: 10px;
|
||||
margin-block-start: var(--padding);
|
||||
padding: var(--padding) 0 0 0;
|
||||
border-block-start: 1px solid var(--theme-splitter-color);
|
||||
display: grid;
|
||||
grid-template-columns: 1fr max-content;
|
||||
grid-column-gap: var(--padding);
|
||||
}
|
||||
|
||||
.flex-item-sizing .section:first-child {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.flex-item-sizing .name {
|
||||
font-weight: 600;
|
||||
grid-column: 1;
|
||||
}
|
||||
|
||||
.flex-item-sizing .value {
|
||||
text-align: end;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.flex-item-sizing .css-property-link {
|
||||
grid-column: 2;
|
||||
text-align: end;
|
||||
}
|
||||
|
||||
.flex-item-sizing .reasons,
|
||||
.flex-item-sizing .reasons li {
|
||||
grid-column: 1 / 3;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
list-style: none;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -12,11 +12,11 @@ const {
|
|||
gridSpec,
|
||||
layoutSpec,
|
||||
} = require("devtools/shared/specs/layout");
|
||||
const { ELEMENT_NODE } = require("devtools/shared/dom-node-constants");
|
||||
const { SHOW_ELEMENT } = require("devtools/shared/dom-node-filter-constants");
|
||||
const { getStringifiableFragments } =
|
||||
require("devtools/server/actors/utils/css-grid-utils");
|
||||
|
||||
loader.lazyRequireGetter(this, "getCSSStyleRules", "devtools/shared/inspector/css-logic", true);
|
||||
loader.lazyRequireGetter(this, "CssLogic", "devtools/server/actors/inspector/css-logic", true);
|
||||
loader.lazyRequireGetter(this, "nodeConstants", "devtools/shared/dom-node-constants");
|
||||
|
||||
|
@ -103,10 +103,6 @@ const FlexboxActor = ActorClassWithSpec(flexboxSpec, {
|
|||
|
||||
for (const line of flex.getLines()) {
|
||||
for (const item of line.getItems()) {
|
||||
if (item.node.nodeType !== ELEMENT_NODE) {
|
||||
continue;
|
||||
}
|
||||
|
||||
flexItemActors.push(new FlexItemActor(this, item.node, {
|
||||
crossMaxSize: item.crossMaxSize,
|
||||
crossMinSize: item.crossMinSize,
|
||||
|
@ -114,6 +110,7 @@ const FlexboxActor = ActorClassWithSpec(flexboxSpec, {
|
|||
mainDeltaSize: item.mainDeltaSize,
|
||||
mainMaxSize: item.mainMaxSize,
|
||||
mainMinSize: item.mainMinSize,
|
||||
lineGrowthState: line.growthState,
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
@ -157,32 +154,50 @@ const FlexItemActor = ActorClassWithSpec(flexItemSpec, {
|
|||
return this.actorID;
|
||||
}
|
||||
|
||||
const { flexDirection } = CssLogic.getComputedStyle(this.containerEl);
|
||||
const dimension = flexDirection.startsWith("row") ? "width" : "height";
|
||||
|
||||
// Find the authored sizing properties for this item.
|
||||
const properties = {
|
||||
"flex-basis": "",
|
||||
"flex-grow": "",
|
||||
"flex-shrink": "",
|
||||
[`min-${dimension}`]: "",
|
||||
[`max-${dimension}`]: "",
|
||||
[dimension]: ""
|
||||
};
|
||||
|
||||
if (this.element.nodeType === this.element.ELEMENT_NODE) {
|
||||
for (const name in properties) {
|
||||
let value = "";
|
||||
// Look first on the element style.
|
||||
if (this.element.style[name] && this.element.style[name] !== "auto") {
|
||||
value = this.element.style[name];
|
||||
} else {
|
||||
// And then on the rules that apply to the element.
|
||||
// getCSSStyleRules returns rules from least to most specific, so override
|
||||
// values as we find them.
|
||||
const cssRules = getCSSStyleRules(this.element);
|
||||
for (const rule of cssRules) {
|
||||
const rulePropertyValue = rule.style.getPropertyValue(name);
|
||||
if (rulePropertyValue && rulePropertyValue !== "auto") {
|
||||
value = rulePropertyValue;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
properties[name] = value;
|
||||
}
|
||||
}
|
||||
|
||||
const form = {
|
||||
actor: this.actorID,
|
||||
// The flex item sizing data.
|
||||
flexItemSizing: this.flexItemSizing,
|
||||
// The authored style properties of the flex item.
|
||||
properties,
|
||||
};
|
||||
|
||||
if (this.element.nodeType === ELEMENT_NODE) {
|
||||
const { flexDirection } = CssLogic.getComputedStyle(this.containerEl);
|
||||
const styles = CssLogic.getComputedStyle(this.element);
|
||||
const clientRect = this.element.getBoundingClientRect();
|
||||
const dimension = flexDirection.startsWith("row") ? "width" : "height";
|
||||
|
||||
// The computed style properties of the flex item.
|
||||
form.properties = {
|
||||
"flex-basis": styles.flexBasis,
|
||||
"flex-grow": styles.flexGrow,
|
||||
"flex-shrink": styles.flexShrink,
|
||||
// min-width/height computed style.
|
||||
[`min-${dimension}`]: styles[`min-${dimension}`],
|
||||
// max-width/height computed style.
|
||||
[`max-${dimension}`]: styles[`max-${dimension}`],
|
||||
// Computed width/height of the flex item element.
|
||||
[dimension]: parseFloat(clientRect[dimension.toLowerCase()].toPrecision(6)),
|
||||
};
|
||||
}
|
||||
|
||||
// If the WalkerActor already knows the flex item element, then also return its
|
||||
// ActorID so we avoid the client from doing another round trip to get it in many
|
||||
// cases.
|
||||
|
|
|
@ -65,7 +65,7 @@ class EventEmitter {
|
|||
return;
|
||||
}
|
||||
|
||||
if (length === 3) {
|
||||
if (length >= 3) {
|
||||
// Trying to remove from the `target` the `listener` specified for the
|
||||
// event's `type` given.
|
||||
const listenersForType = events.get(type);
|
||||
|
|
|
@ -70,7 +70,7 @@ const FlexItemFront = FrontClassWithSpec(flexItemSpec, {
|
|||
},
|
||||
|
||||
/**
|
||||
* Get the computed style properties for the flex item.
|
||||
* Get the style properties for the flex item.
|
||||
*/
|
||||
get properties() {
|
||||
return this._form.properties;
|
||||
|
|
|
@ -323,6 +323,13 @@ const TESTS = {
|
|||
await Promise.all([pFoo, pDone]);
|
||||
},
|
||||
|
||||
testCallingOffWithMoreThan3Args() {
|
||||
const target = { name: "target"};
|
||||
on(target, "data", fail);
|
||||
off(target, "data", fail, undefined);
|
||||
emit(target, "data", "Listener should be removed");
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
/**
|
||||
|
|
|
@ -132,8 +132,7 @@ nsStyledElement::SetInlineStyleDeclaration(DeclarationBlock& aDeclaration,
|
|||
nsIDocument* document = GetComposedDoc();
|
||||
mozAutoDocUpdate updateBatch(document, true);
|
||||
return SetAttrAndNotify(kNameSpaceID_None, nsGkAtoms::style, nullptr,
|
||||
aData.mOldValue.isSome() ?
|
||||
aData.mOldValue.ptr() : nullptr,
|
||||
aData.mOldValue.ptrOr(nullptr),
|
||||
attrValue, nullptr, aData.mModType,
|
||||
hasListeners, true, kDontCallAfterSetAttr,
|
||||
document, updateBatch);
|
||||
|
|
|
@ -546,10 +546,6 @@ DOMInterfaces = {
|
|||
'headerFile': 'MediaStreamList.h',
|
||||
},
|
||||
|
||||
'MediaStreamTrack': {
|
||||
'concrete': False
|
||||
},
|
||||
|
||||
'MediaRecorder': {
|
||||
'headerFile': 'MediaRecorder.h',
|
||||
},
|
||||
|
|
|
@ -13,6 +13,7 @@
|
|||
|
||||
#include "mozilla/dom/HTMLMediaElement.h"
|
||||
#include "AudioChannelService.h"
|
||||
#include "AudioDeviceInfo.h"
|
||||
#include "AudioStreamTrack.h"
|
||||
#include "AutoplayPolicy.h"
|
||||
#include "ChannelMediaDecoder.h"
|
||||
|
@ -31,6 +32,7 @@
|
|||
#include "MP4Decoder.h"
|
||||
#include "MediaContainerType.h"
|
||||
#include "MediaError.h"
|
||||
#include "MediaManager.h"
|
||||
#include "MediaMetadataManager.h"
|
||||
#include "MediaResource.h"
|
||||
#include "MediaSourceDecoder.h"
|
||||
|
@ -3885,6 +3887,7 @@ HTMLMediaElement::HTMLMediaElement(
|
|||
, mPaused(true, "HTMLMediaElement::mPaused")
|
||||
, mErrorSink(new ErrorSink(this))
|
||||
, mAudioChannelWrapper(new AudioChannelAgentCallback(this))
|
||||
, mSink(MakePair(nsString(), RefPtr<AudioDeviceInfo>()))
|
||||
{
|
||||
MOZ_ASSERT(mMainThreadEventTarget);
|
||||
MOZ_ASSERT(mAbstractMainThread);
|
||||
|
@ -5085,6 +5088,19 @@ HTMLMediaElement::FinishDecoderSetup(MediaDecoder* aDecoder)
|
|||
// can affect how we feed data to MediaStreams
|
||||
NotifyDecoderPrincipalChanged();
|
||||
|
||||
// Set sink device if we have one. Otherwise the default is used.
|
||||
if (mSink.second()) {
|
||||
mDecoder->SetSink(mSink.second())
|
||||
#ifdef DEBUG
|
||||
->Then(mAbstractMainThread, __func__,
|
||||
[](const GenericPromise::ResolveOrRejectValue& aValue) {
|
||||
MOZ_ASSERT(aValue.IsResolve() && !aValue.ResolveValue());
|
||||
});
|
||||
#else
|
||||
;
|
||||
#endif
|
||||
}
|
||||
|
||||
for (OutputMediaStream& ms : mOutputStreams) {
|
||||
if (ms.mCapturingMediaStream) {
|
||||
MOZ_ASSERT(!ms.mCapturingDecoder);
|
||||
|
@ -5316,6 +5332,9 @@ HTMLMediaElement::UpdateSrcMediaStreamPlaying(uint32_t aFlags)
|
|||
|
||||
stream->AddAudioOutput(this);
|
||||
SetVolumeInternal();
|
||||
if (mSink.second()) {
|
||||
NS_WARNING("setSinkId() when playing a MediaStream is not supported yet and will be ignored");
|
||||
}
|
||||
|
||||
VideoFrameContainer* container = GetVideoFrameContainer();
|
||||
if (mSelectedVideoStreamTrack && container) {
|
||||
|
@ -8265,6 +8284,86 @@ HTMLMediaElement::ReportCanPlayTelemetry()
|
|||
NS_DISPATCH_NORMAL);
|
||||
}
|
||||
|
||||
already_AddRefed<Promise>
|
||||
HTMLMediaElement::SetSinkId(const nsAString& aSinkId, ErrorResult& aRv)
|
||||
{
|
||||
nsCOMPtr<nsPIDOMWindowInner> win = OwnerDoc()->GetInnerWindow();
|
||||
if (!win) {
|
||||
aRv.Throw(NS_ERROR_UNEXPECTED);
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
RefPtr<Promise> promise = Promise::Create(win->AsGlobal(), aRv);
|
||||
if (aRv.Failed()) {
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
if (mSink.first().Equals(aSinkId)) {
|
||||
promise->MaybeResolveWithUndefined();
|
||||
return promise.forget();
|
||||
}
|
||||
|
||||
nsString sinkId(aSinkId);
|
||||
MediaManager::Get()->GetSinkDevice(win, sinkId)
|
||||
->Then(mAbstractMainThread, __func__,
|
||||
[self = RefPtr<HTMLMediaElement>(this)](RefPtr<AudioDeviceInfo>&& aInfo) {
|
||||
// Sink found switch output device.
|
||||
MOZ_ASSERT(aInfo);
|
||||
if (self->mDecoder) {
|
||||
RefPtr<SinkInfoPromise> p = self->mDecoder->SetSink(aInfo)
|
||||
->Then(self->mAbstractMainThread, __func__,
|
||||
[aInfo] (const GenericPromise::ResolveOrRejectValue& aValue) {
|
||||
if (aValue.IsResolve()) {
|
||||
return SinkInfoPromise::CreateAndResolve(aInfo, __func__);
|
||||
}
|
||||
return SinkInfoPromise::CreateAndReject(aValue.RejectValue(), __func__);
|
||||
});
|
||||
return p;
|
||||
}
|
||||
if (self->GetSrcMediaStream()) {
|
||||
// Set Sink Id through MSG is not supported yet.
|
||||
return SinkInfoPromise::CreateAndReject(NS_ERROR_ABORT, __func__);
|
||||
}
|
||||
// No media attached to the element save it for later.
|
||||
return SinkInfoPromise::CreateAndResolve(aInfo, __func__);
|
||||
},
|
||||
[](nsresult res){
|
||||
// Promise is rejected, sink not found.
|
||||
return SinkInfoPromise::CreateAndReject(res, __func__);
|
||||
})
|
||||
->Then(mAbstractMainThread, __func__,
|
||||
[promise, self = RefPtr<HTMLMediaElement>(this),
|
||||
sinkId = std::move(sinkId)] (const SinkInfoPromise::ResolveOrRejectValue& aValue) {
|
||||
if (aValue.IsResolve()) {
|
||||
self->mSink = MakePair(sinkId, aValue.ResolveValue());
|
||||
promise->MaybeResolveWithUndefined();
|
||||
} else {
|
||||
switch (aValue.RejectValue()) {
|
||||
case NS_ERROR_ABORT:
|
||||
promise->MaybeReject(NS_ERROR_DOM_ABORT_ERR);
|
||||
break;
|
||||
case NS_ERROR_NOT_AVAILABLE:
|
||||
{
|
||||
ErrorResult notFoundError;
|
||||
notFoundError.ThrowDOMException(
|
||||
NS_ERROR_DOM_NOT_FOUND_ERR,
|
||||
NS_LITERAL_CSTRING("The object can not be found here."));
|
||||
promise->MaybeReject(notFoundError);
|
||||
break;
|
||||
}
|
||||
case NS_ERROR_DOM_MEDIA_NOT_ALLOWED_ERR:
|
||||
promise->MaybeReject(NS_ERROR_DOM_NOT_ALLOWED_ERR);
|
||||
break;
|
||||
default:
|
||||
MOZ_ASSERT_UNREACHABLE("Invalid error.");
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
aRv = NS_OK;
|
||||
return promise.forget();
|
||||
}
|
||||
|
||||
} // namespace dom
|
||||
} // namespace mozilla
|
||||
|
||||
|
|
|
@ -66,6 +66,7 @@ class VideoStreamTrack;
|
|||
} // namespace dom
|
||||
} // namespace mozilla
|
||||
|
||||
class AudioDeviceInfo;
|
||||
class nsIChannel;
|
||||
class nsIHttpChannel;
|
||||
class nsILoadGroup;
|
||||
|
@ -827,6 +828,17 @@ public:
|
|||
return mMainThreadEventTarget;
|
||||
}
|
||||
|
||||
// Set the sink id (of the output device) that the audio will play. If aSinkId
|
||||
// is empty the default device will be set.
|
||||
already_AddRefed<Promise> SetSinkId(const nsAString& aSinkId, ErrorResult& aRv);
|
||||
// Get the sink id of the device that audio is being played. Initial value is
|
||||
// empty and the default device is being used.
|
||||
void GetSinkId(nsString& aSinkId)
|
||||
{
|
||||
MOZ_ASSERT(NS_IsMainThread());
|
||||
aSinkId = mSink.first();
|
||||
}
|
||||
|
||||
protected:
|
||||
virtual ~HTMLMediaElement();
|
||||
|
||||
|
@ -1893,6 +1905,14 @@ private:
|
|||
|
||||
// Attach UA Shadow Root if it is not attached.
|
||||
void AttachAndSetUAShadowRoot();
|
||||
|
||||
// Contains the unique id of the sink device and the device info.
|
||||
// The initial value is ("", nullptr) and the default output device is used.
|
||||
// It can contain an invalid id and info if the device has been
|
||||
// unplugged. It can be set to ("", nullptr). It follows the spec attribute:
|
||||
// https://w3c.github.io/mediacapture-output/#htmlmediaelement-extensions
|
||||
// Read/Write from the main thread only.
|
||||
Pair<nsString, RefPtr<AudioDeviceInfo>> mSink;
|
||||
};
|
||||
|
||||
// Check if the context is chrome or has the debugger or tabs permission
|
||||
|
|
|
@ -337,7 +337,8 @@ int AudioStream::InvokeCubeb(Function aFunction, Args&&... aArgs)
|
|||
nsresult
|
||||
AudioStream::Init(uint32_t aNumChannels,
|
||||
AudioConfig::ChannelLayout::ChannelMap aChannelMap,
|
||||
uint32_t aRate)
|
||||
uint32_t aRate,
|
||||
AudioDeviceInfo* aSinkInfo)
|
||||
{
|
||||
auto startTime = TimeStamp::Now();
|
||||
|
||||
|
@ -347,6 +348,8 @@ AudioStream::Init(uint32_t aNumChannels,
|
|||
|
||||
mDumpFile = OpenDumpFile(aNumChannels, aRate);
|
||||
|
||||
mSinkInfo = aSinkInfo;
|
||||
|
||||
cubeb_stream_params params;
|
||||
params.rate = aRate;
|
||||
params.channels = mOutChannels;
|
||||
|
@ -380,8 +383,12 @@ AudioStream::OpenCubeb(cubeb* aContext, cubeb_stream_params& aParams,
|
|||
/* Convert from milliseconds to frames. */
|
||||
uint32_t latency_frames =
|
||||
CubebUtils::GetCubebPlaybackLatencyInMilliseconds() * aParams.rate / 1000;
|
||||
cubeb_devid deviceID = nullptr;
|
||||
if (mSinkInfo && mSinkInfo->DeviceID()) {
|
||||
deviceID = mSinkInfo->DeviceID();
|
||||
}
|
||||
if (cubeb_stream_init(aContext, &stream, "AudioStream",
|
||||
nullptr, nullptr, nullptr, &aParams,
|
||||
nullptr, nullptr, deviceID, &aParams,
|
||||
latency_frames,
|
||||
DataCallback_S, StateCallback_S, this) == CUBEB_OK) {
|
||||
mCubebStream.reset(stream);
|
||||
|
@ -411,7 +418,7 @@ AudioStream::SetVolume(double aVolume)
|
|||
}
|
||||
}
|
||||
|
||||
void
|
||||
nsresult
|
||||
AudioStream::Start()
|
||||
{
|
||||
MonitorAutoLock mon(mMonitor);
|
||||
|
@ -422,6 +429,10 @@ AudioStream::Start()
|
|||
mState = ERRORED;
|
||||
}
|
||||
LOG("started, state %s", mState == STARTED ? "STARTED" : mState == DRAINED ? "DRAINED" : "ERRORED");
|
||||
if (mState == STARTED || mState == DRAINED) {
|
||||
return NS_OK;
|
||||
}
|
||||
return NS_ERROR_FAILURE;
|
||||
}
|
||||
|
||||
void
|
||||
|
|
|
@ -200,7 +200,8 @@ public:
|
|||
// (22050Hz, 44100Hz, etc).
|
||||
nsresult Init(uint32_t aNumChannels,
|
||||
AudioConfig::ChannelLayout::ChannelMap aChannelMap,
|
||||
uint32_t aRate);
|
||||
uint32_t aRate,
|
||||
AudioDeviceInfo* aSinkInfo);
|
||||
|
||||
// Closes the stream. All future use of the stream is an error.
|
||||
void Shutdown();
|
||||
|
@ -212,7 +213,7 @@ public:
|
|||
void SetVolume(double aVolume);
|
||||
|
||||
// Start the stream.
|
||||
void Start();
|
||||
nsresult Start();
|
||||
|
||||
// Pause audio playback.
|
||||
void Pause();
|
||||
|
@ -317,6 +318,11 @@ private:
|
|||
DataSource& mDataSource;
|
||||
|
||||
bool mPrefillQuirk;
|
||||
|
||||
// The device info of the current sink. If null
|
||||
// the default device is used. It is set
|
||||
// during the Init() in decoder thread.
|
||||
RefPtr<AudioDeviceInfo> mSinkInfo;
|
||||
};
|
||||
|
||||
} // namespace mozilla
|
||||
|
|
|
@ -7,17 +7,10 @@
|
|||
|
||||
#include "nsContentUtils.h"
|
||||
|
||||
#include "mozilla/dom/AudioStreamTrackBinding.h"
|
||||
|
||||
namespace mozilla {
|
||||
namespace dom {
|
||||
|
||||
JSObject*
|
||||
AudioStreamTrack::WrapObject(JSContext* aCx, JS::Handle<JSObject*> aGivenProto)
|
||||
{
|
||||
return AudioStreamTrack_Binding::Wrap(aCx, this, aGivenProto);
|
||||
}
|
||||
|
||||
void
|
||||
AudioStreamTrack::GetLabel(nsAString& aLabel, CallerType aCallerType)
|
||||
{
|
||||
|
|
|
@ -20,10 +20,7 @@ public:
|
|||
const MediaTrackConstraints& aConstraints = MediaTrackConstraints())
|
||||
: MediaStreamTrack(aStream, aTrackID, aInputTrackID, aSource, aConstraints) {}
|
||||
|
||||
JSObject* WrapObject(JSContext* aCx, JS::Handle<JSObject*> aGivenProto) override;
|
||||
|
||||
AudioStreamTrack* AsAudioStreamTrack() override { return this; }
|
||||
|
||||
const AudioStreamTrack* AsAudioStreamTrack() const override { return this; }
|
||||
|
||||
// WebIDL
|
||||
|
|
|
@ -660,24 +660,42 @@ void
|
|||
DOMMediaStream::GetAudioTracks(nsTArray<RefPtr<AudioStreamTrack>>& aTracks) const
|
||||
{
|
||||
for (const RefPtr<TrackPort>& info : mTracks) {
|
||||
AudioStreamTrack* t = info->GetTrack()->AsAudioStreamTrack();
|
||||
if (t) {
|
||||
if (AudioStreamTrack* t = info->GetTrack()->AsAudioStreamTrack()) {
|
||||
aTracks.AppendElement(t);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
DOMMediaStream::GetAudioTracks(nsTArray<RefPtr<MediaStreamTrack>>& aTracks) const
|
||||
{
|
||||
for (const RefPtr<TrackPort>& info : mTracks) {
|
||||
if (info->GetTrack()->AsAudioStreamTrack()) {
|
||||
aTracks.AppendElement(info->GetTrack());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
DOMMediaStream::GetVideoTracks(nsTArray<RefPtr<VideoStreamTrack>>& aTracks) const
|
||||
{
|
||||
for (const RefPtr<TrackPort>& info : mTracks) {
|
||||
VideoStreamTrack* t = info->GetTrack()->AsVideoStreamTrack();
|
||||
if (t) {
|
||||
if (VideoStreamTrack* t = info->GetTrack()->AsVideoStreamTrack()) {
|
||||
aTracks.AppendElement(t);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
DOMMediaStream::GetVideoTracks(nsTArray<RefPtr<MediaStreamTrack>>& aTracks) const
|
||||
{
|
||||
for (const RefPtr<TrackPort>& info : mTracks) {
|
||||
if (info->GetTrack()->AsVideoStreamTrack()) {
|
||||
aTracks.AppendElement(info->GetTrack());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
DOMMediaStream::GetTracks(nsTArray<RefPtr<MediaStreamTrack> >& aTracks) const
|
||||
{
|
||||
|
|
|
@ -367,7 +367,9 @@ public:
|
|||
void GetId(nsAString& aID) const;
|
||||
|
||||
void GetAudioTracks(nsTArray<RefPtr<AudioStreamTrack> >& aTracks) const;
|
||||
void GetAudioTracks(nsTArray<RefPtr<MediaStreamTrack> >& aTracks) const;
|
||||
void GetVideoTracks(nsTArray<RefPtr<VideoStreamTrack> >& aTracks) const;
|
||||
void GetVideoTracks(nsTArray<RefPtr<MediaStreamTrack> >& aTracks) const;
|
||||
void GetTracks(nsTArray<RefPtr<MediaStreamTrack> >& aTracks) const;
|
||||
MediaStreamTrack* GetTrackById(const nsAString& aId) const;
|
||||
void AddTrack(MediaStreamTrack& aTrack);
|
||||
|
|
|
@ -169,6 +169,14 @@ MediaDecoder::SetVolume(double aVolume)
|
|||
mVolume = aVolume;
|
||||
}
|
||||
|
||||
RefPtr<GenericPromise>
|
||||
MediaDecoder::SetSink(AudioDeviceInfo* aSink)
|
||||
{
|
||||
MOZ_ASSERT(NS_IsMainThread());
|
||||
AbstractThread::AutoEnter context(AbstractMainThread());
|
||||
return GetStateMachine()->InvokeSetSink(aSink);
|
||||
}
|
||||
|
||||
void
|
||||
MediaDecoder::AddOutputStream(ProcessedMediaStream* aStream,
|
||||
TrackID aNextAvailableTrackID,
|
||||
|
|
|
@ -157,6 +157,9 @@ public:
|
|||
void SetPreservesPitch(bool aPreservesPitch);
|
||||
void SetLooping(bool aLooping);
|
||||
|
||||
// Set the given device as the output device.
|
||||
RefPtr<GenericPromise> SetSink(AudioDeviceInfo* aSink);
|
||||
|
||||
bool GetMinimizePreroll() const { return mMinimizePreroll; }
|
||||
|
||||
// All MediaStream-related data is protected by mReentrantMonitor.
|
||||
|
|
|
@ -3303,13 +3303,17 @@ MediaDecoderStateMachine::WaitForData(MediaData::Type aType)
|
|||
}
|
||||
}
|
||||
|
||||
void
|
||||
nsresult
|
||||
MediaDecoderStateMachine::StartMediaSink()
|
||||
{
|
||||
MOZ_ASSERT(OnTaskQueue());
|
||||
if (!mMediaSink->IsStarted()) {
|
||||
|
||||
if (mMediaSink->IsStarted()) {
|
||||
return NS_OK;
|
||||
}
|
||||
|
||||
mAudioCompleted = false;
|
||||
mMediaSink->Start(GetMediaTime(), Info());
|
||||
nsresult rv = mMediaSink->Start(GetMediaTime(), Info());
|
||||
|
||||
auto videoPromise = mMediaSink->OnEnded(TrackInfo::kVideoTrack);
|
||||
auto audioPromise = mMediaSink->OnEnded(TrackInfo::kAudioTrack);
|
||||
|
@ -3336,7 +3340,7 @@ MediaDecoderStateMachine::StartMediaSink()
|
|||
if (sample && sample->mOffset > mPlaybackOffset) {
|
||||
mPlaybackOffset = sample->mOffset;
|
||||
}
|
||||
}
|
||||
return rv;
|
||||
}
|
||||
|
||||
bool
|
||||
|
@ -3664,6 +3668,60 @@ MediaDecoderStateMachine::LoopingChanged()
|
|||
}
|
||||
}
|
||||
|
||||
RefPtr<GenericPromise>
|
||||
MediaDecoderStateMachine::InvokeSetSink(RefPtr<AudioDeviceInfo> aSink)
|
||||
{
|
||||
MOZ_ASSERT(NS_IsMainThread());
|
||||
MOZ_ASSERT(aSink);
|
||||
|
||||
++mSetSinkRequestsCount;
|
||||
return InvokeAsync(
|
||||
OwnerThread(), this, __func__,
|
||||
&MediaDecoderStateMachine::SetSink, aSink);
|
||||
}
|
||||
|
||||
RefPtr<GenericPromise>
|
||||
MediaDecoderStateMachine::SetSink(RefPtr<AudioDeviceInfo> aSink)
|
||||
{
|
||||
MOZ_ASSERT(OnTaskQueue());
|
||||
if (mAudioCaptured) {
|
||||
// Not supported yet.
|
||||
return GenericPromise::CreateAndReject(NS_ERROR_ABORT, __func__);
|
||||
}
|
||||
|
||||
// Backup current playback parameters.
|
||||
bool wasPlaying = mMediaSink->IsPlaying();
|
||||
|
||||
if (--mSetSinkRequestsCount > 0) {
|
||||
MOZ_ASSERT(mSetSinkRequestsCount > 0);
|
||||
return GenericPromise::CreateAndResolve(wasPlaying, __func__);
|
||||
}
|
||||
|
||||
MediaSink::PlaybackParams params = mMediaSink->GetPlaybackParams();
|
||||
params.mSink = std::move(aSink);
|
||||
|
||||
if (!mMediaSink->IsStarted()) {
|
||||
mMediaSink->SetPlaybackParams(params);
|
||||
return GenericPromise::CreateAndResolve(false, __func__);
|
||||
}
|
||||
|
||||
// Stop and shutdown the existing sink.
|
||||
StopMediaSink();
|
||||
mMediaSink->Shutdown();
|
||||
// Create a new sink according to whether audio is captured.
|
||||
mMediaSink = CreateMediaSink(false);
|
||||
// Restore playback parameters.
|
||||
mMediaSink->SetPlaybackParams(params);
|
||||
// Start the new sink
|
||||
if (wasPlaying) {
|
||||
nsresult rv = StartMediaSink();
|
||||
if (NS_FAILED(rv)) {
|
||||
return GenericPromise::CreateAndReject(NS_ERROR_ABORT, __func__);
|
||||
}
|
||||
}
|
||||
return GenericPromise::CreateAndResolve(wasPlaying, __func__);
|
||||
}
|
||||
|
||||
TimeUnit
|
||||
MediaDecoderStateMachine::AudioEndTime() const
|
||||
{
|
||||
|
|
|
@ -296,6 +296,8 @@ public:
|
|||
// Sets the video decode mode. Used by the suspend-video-decoder feature.
|
||||
void SetVideoDecodeMode(VideoDecodeMode aMode);
|
||||
|
||||
RefPtr<GenericPromise> InvokeSetSink(RefPtr<AudioDeviceInfo> aSink);
|
||||
|
||||
private:
|
||||
class StateObject;
|
||||
class DecodeMetadataState;
|
||||
|
@ -369,6 +371,16 @@ private:
|
|||
|
||||
void SetVideoDecodeModeInternal(VideoDecodeMode aMode);
|
||||
|
||||
// Set new sink device and restart MediaSink if playback is started.
|
||||
// Returned promise will be resolved with true if the playback is
|
||||
// started and false if playback is stopped after setting the new sink.
|
||||
// Returned promise will be rejected with value NS_ERROR_ABORT
|
||||
// if the action fails or it is not supported.
|
||||
// If there are multiple pending requests only the last one will be
|
||||
// executed, for all previous requests the promise will be resolved
|
||||
// with true or false similar to above.
|
||||
RefPtr<GenericPromise> SetSink(RefPtr<AudioDeviceInfo> aSink);
|
||||
|
||||
protected:
|
||||
virtual ~MediaDecoderStateMachine();
|
||||
|
||||
|
@ -447,7 +459,8 @@ protected:
|
|||
// Create and start the media sink.
|
||||
// The decoder monitor must be held with exactly one lock count.
|
||||
// Called on the state machine thread.
|
||||
void StartMediaSink();
|
||||
// If start fails an NS_ERROR_FAILURE is returned.
|
||||
nsresult StartMediaSink();
|
||||
|
||||
// Notification method invoked when mPlayState changes.
|
||||
void PlayStateChanged();
|
||||
|
@ -739,6 +752,9 @@ private:
|
|||
// Used to distinguish whether the audio is producing sound.
|
||||
Canonical<bool> mIsAudioDataAudible;
|
||||
|
||||
// Used to count the number of pending requests to set a new sink.
|
||||
Atomic<int> mSetSinkRequestsCount;
|
||||
|
||||
public:
|
||||
AbstractCanonical<media::TimeIntervals>* CanonicalBuffered() const;
|
||||
|
||||
|
|
|
@ -916,11 +916,12 @@ private:
|
|||
*/
|
||||
NS_IMPL_ISUPPORTS(MediaDevice, nsIMediaDevice)
|
||||
|
||||
MediaDevice::MediaDevice(MediaEngineSource* aSource,
|
||||
MediaDevice::MediaDevice(const RefPtr<MediaEngineSource>& aSource,
|
||||
const nsString& aName,
|
||||
const nsString& aID,
|
||||
const nsString& aRawID)
|
||||
: mSource(aSource)
|
||||
, mSinkInfo(nullptr)
|
||||
, mKind((mSource && MediaEngineSource::IsVideo(mSource->GetMediaSource())) ?
|
||||
dom::MediaDeviceKind::Videoinput : dom::MediaDeviceKind::Audioinput)
|
||||
, mScary(mSource->GetScary())
|
||||
|
@ -932,15 +933,16 @@ MediaDevice::MediaDevice(MediaEngineSource* aSource,
|
|||
MOZ_ASSERT(mSource);
|
||||
}
|
||||
|
||||
MediaDevice::MediaDevice(const nsString& aName,
|
||||
const dom::MediaDeviceKind aKind,
|
||||
MediaDevice::MediaDevice(const RefPtr<AudioDeviceInfo>& aAudioDeviceInfo,
|
||||
const nsString& aID,
|
||||
const nsString& aRawID)
|
||||
: mSource(nullptr)
|
||||
, mKind(aKind)
|
||||
, mSinkInfo(aAudioDeviceInfo)
|
||||
, mKind(mSinkInfo->Type() == AudioDeviceInfo::TYPE_INPUT ? dom::MediaDeviceKind::Audioinput
|
||||
: dom::MediaDeviceKind::Audiooutput)
|
||||
, mScary(false)
|
||||
, mType(NS_ConvertUTF8toUTF16(dom::MediaDeviceKindValues::strings[uint32_t(mKind)].value))
|
||||
, mName(aName)
|
||||
, mName(mSinkInfo->Name())
|
||||
, mID(aID)
|
||||
, mRawID(aRawID)
|
||||
{
|
||||
|
@ -949,12 +951,14 @@ MediaDevice::MediaDevice(const nsString& aName,
|
|||
// when we do not instantiate a MediaEngineSource
|
||||
// during EnumerateDevices.
|
||||
MOZ_ASSERT(mKind == dom::MediaDeviceKind::Audiooutput);
|
||||
MOZ_ASSERT(mSinkInfo);
|
||||
}
|
||||
|
||||
MediaDevice::MediaDevice(const MediaDevice* aOther,
|
||||
MediaDevice::MediaDevice(const RefPtr<MediaDevice>& aOther,
|
||||
const nsString& aID,
|
||||
const nsString& aRawID)
|
||||
: mSource(aOther->mSource)
|
||||
, mSinkInfo(aOther->mSinkInfo)
|
||||
, mKind(aOther->mKind)
|
||||
, mScary(aOther->mScary)
|
||||
, mType(aOther->mType)
|
||||
|
@ -3418,6 +3422,61 @@ MediaManager::EnumerateDevices(nsPIDOMWindowInner* aWindow,
|
|||
return NS_OK;
|
||||
}
|
||||
|
||||
RefPtr<SinkInfoPromise>
|
||||
MediaManager::GetSinkDevice(nsPIDOMWindowInner* aWindow,
|
||||
const nsString& aDeviceId)
|
||||
{
|
||||
MOZ_ASSERT(NS_IsMainThread());
|
||||
MOZ_ASSERT(aWindow);
|
||||
|
||||
// We have to add the window id here because enumerate methods
|
||||
// check for that and abort silently if it does not exist.
|
||||
uint64_t windowId = aWindow->WindowID();
|
||||
nsIPrincipal* principal = aWindow->GetExtantDoc()->NodePrincipal();
|
||||
RefPtr<GetUserMediaWindowListener> windowListener = GetWindowListener(windowId);
|
||||
if (windowListener) {
|
||||
PrincipalHandle existingPrincipalHandle =
|
||||
windowListener->GetPrincipalHandle();
|
||||
MOZ_ASSERT(PrincipalHandleMatches(existingPrincipalHandle, principal));
|
||||
} else {
|
||||
windowListener = new GetUserMediaWindowListener(mMediaThread, windowId,
|
||||
MakePrincipalHandle(principal));
|
||||
AddWindowID(windowId, windowListener);
|
||||
}
|
||||
// Create an inactive SourceListener to act as a placeholder, so the
|
||||
// window listener doesn't clean itself up until we're done.
|
||||
RefPtr<SourceListener> sourceListener = new SourceListener();
|
||||
windowListener->Register(sourceListener);
|
||||
|
||||
bool isSecure = aWindow->IsSecureContext();
|
||||
|
||||
return EnumerateDevicesImpl(aWindow->WindowID(),
|
||||
MediaSourceEnum::Other,
|
||||
MediaSourceEnum::Other,
|
||||
MediaSinkEnum::Speaker,
|
||||
DeviceEnumerationType::Normal,
|
||||
DeviceEnumerationType::Normal)
|
||||
->Then(GetCurrentThreadSerialEventTarget(), __func__,
|
||||
[aDeviceId, isSecure](RefPtr<MediaDeviceSetRefCnt>&& aDevices) {
|
||||
for (RefPtr<MediaDevice>& device : **aDevices) {
|
||||
if (aDeviceId.IsEmpty() && device->mSinkInfo->Preferred()) {
|
||||
return SinkInfoPromise::CreateAndResolve(device->mSinkInfo, __func__);
|
||||
}
|
||||
if (device->mID.Equals(aDeviceId)) {
|
||||
// TODO: Check if the application is authorized to play audio
|
||||
// through this device (Bug 1493982).
|
||||
if (isSecure || device->mSinkInfo->Preferred()) {
|
||||
return SinkInfoPromise::CreateAndResolve(device->mSinkInfo, __func__);
|
||||
}
|
||||
return SinkInfoPromise::CreateAndReject(NS_ERROR_DOM_MEDIA_NOT_ALLOWED_ERR, __func__);
|
||||
}
|
||||
}
|
||||
return SinkInfoPromise::CreateAndReject(NS_ERROR_NOT_AVAILABLE, __func__);
|
||||
}, [](RefPtr<MediaStreamError>&& reason) {
|
||||
return SinkInfoPromise::CreateAndReject(NS_ERROR_NOT_AVAILABLE, __func__);
|
||||
});
|
||||
}
|
||||
|
||||
/*
|
||||
* GetUserMediaDevices - called by the UI-part of getUserMedia from chrome JS.
|
||||
*/
|
||||
|
|
|
@ -71,17 +71,16 @@ public:
|
|||
NS_DECL_THREADSAFE_ISUPPORTS
|
||||
NS_DECL_NSIMEDIADEVICE
|
||||
|
||||
explicit MediaDevice(MediaEngineSource* aSource,
|
||||
explicit MediaDevice(const RefPtr<MediaEngineSource>& aSource,
|
||||
const nsString& aName,
|
||||
const nsString& aID,
|
||||
const nsString& aRawID = NS_LITERAL_STRING(""));
|
||||
const nsString& aRawID);
|
||||
|
||||
explicit MediaDevice(const nsString& aName,
|
||||
const dom::MediaDeviceKind aKind,
|
||||
explicit MediaDevice(const RefPtr<AudioDeviceInfo>& aAudioDeviceInfo,
|
||||
const nsString& aID,
|
||||
const nsString& aRawID = NS_LITERAL_STRING(""));
|
||||
|
||||
explicit MediaDevice(const MediaDevice* aOther,
|
||||
explicit MediaDevice(const RefPtr<MediaDevice>& aOther,
|
||||
const nsString& aID,
|
||||
const nsString& aRawID);
|
||||
|
||||
|
@ -129,6 +128,7 @@ private:
|
|||
|
||||
public:
|
||||
const RefPtr<MediaEngineSource> mSource;
|
||||
const RefPtr<AudioDeviceInfo> mSinkInfo;
|
||||
const dom::MediaDeviceKind mKind;
|
||||
const bool mScary;
|
||||
const nsString mType;
|
||||
|
@ -138,6 +138,7 @@ public:
|
|||
};
|
||||
|
||||
typedef nsRefPtrHashtable<nsUint64HashKey, GetUserMediaWindowListener> WindowTable;
|
||||
typedef MozPromise<RefPtr<AudioDeviceInfo>, nsresult, true> SinkInfoPromise;
|
||||
|
||||
class MediaManager final : public nsIMediaManagerService,
|
||||
public nsIObserver
|
||||
|
@ -227,6 +228,26 @@ public:
|
|||
dom::CallerType aCallerType);
|
||||
|
||||
nsresult EnumerateDevices(nsPIDOMWindowInner* aWindow, dom::Promise& aPromise);
|
||||
|
||||
// Get the sink that corresponds to the given device id.
|
||||
// It is resposible to check if an application is
|
||||
// authorized to play audio through the requested device.
|
||||
// The returned promise will be resolved with the device
|
||||
// information if the device id matches one and operation is
|
||||
// allowed. The default device is always allowed. Non default
|
||||
// devices are allowed only in secure context. It is pending to
|
||||
// implement an user authorization model. The promise will be
|
||||
// rejected in the following cases:
|
||||
// NS_ERROR_NOT_AVAILABLE: Device id does not exist.
|
||||
// NS_ERROR_DOM_MEDIA_NOT_ALLOWED_ERR:
|
||||
// The requested device exists but it is not allowed to be used.
|
||||
// Currently, this happens only on non-default default devices
|
||||
// and non https connections. TODO, authorization model to allow
|
||||
// an application to play audio through the device (Bug 1493982).
|
||||
// NS_ERROR_ABORT: General error.
|
||||
RefPtr<SinkInfoPromise> GetSinkDevice(nsPIDOMWindowInner* aWindow,
|
||||
const nsString& aDeviceId);
|
||||
|
||||
void OnNavigation(uint64_t aWindowID);
|
||||
bool IsActivelyCapturingOrHasAPermission(uint64_t aWindowId);
|
||||
|
||||
|
|
|
@ -211,6 +211,12 @@ MediaStreamTrack::GetParentObject() const
|
|||
return mOwningStream->GetParentObject();
|
||||
}
|
||||
|
||||
JSObject*
|
||||
MediaStreamTrack::WrapObject(JSContext* aCx, JS::Handle<JSObject*> aGivenProto)
|
||||
{
|
||||
return MediaStreamTrack_Binding::Wrap(aCx, this, aGivenProto);
|
||||
}
|
||||
|
||||
void
|
||||
MediaStreamTrack::GetId(nsAString& aID) const
|
||||
{
|
||||
|
|
|
@ -386,7 +386,7 @@ public:
|
|||
DOMEventTargetHelper)
|
||||
|
||||
nsPIDOMWindowInner* GetParentObject() const;
|
||||
virtual JSObject* WrapObject(JSContext* aCx, JS::Handle<JSObject*> aGivenProto) override = 0;
|
||||
JSObject* WrapObject(JSContext* aCx, JS::Handle<JSObject*> aGivenProto) override;
|
||||
|
||||
virtual AudioStreamTrack* AsAudioStreamTrack() { return nullptr; }
|
||||
virtual VideoStreamTrack* AsVideoStreamTrack() { return nullptr; }
|
||||
|
|
|
@ -9,17 +9,9 @@
|
|||
#include "MediaStreamGraph.h"
|
||||
#include "nsContentUtils.h"
|
||||
|
||||
#include "mozilla/dom/VideoStreamTrackBinding.h"
|
||||
|
||||
namespace mozilla {
|
||||
namespace dom {
|
||||
|
||||
JSObject*
|
||||
VideoStreamTrack::WrapObject(JSContext* aCx, JS::Handle<JSObject*> aGivenProto)
|
||||
{
|
||||
return VideoStreamTrack_Binding::Wrap(aCx, this, aGivenProto);
|
||||
}
|
||||
|
||||
void
|
||||
VideoStreamTrack::AddVideoOutput(MediaStreamVideoSink* aSink)
|
||||
{
|
||||
|
|
|
@ -23,10 +23,7 @@ public:
|
|||
const MediaTrackConstraints& aConstraints = MediaTrackConstraints())
|
||||
: MediaStreamTrack(aStream, aTrackID, aInputTrackID, aSource, aConstraints) {}
|
||||
|
||||
JSObject* WrapObject(JSContext* aCx, JS::Handle<JSObject*> aGivenProto) override;
|
||||
|
||||
VideoStreamTrack* AsVideoStreamTrack() override { return this; }
|
||||
|
||||
const VideoStreamTrack* AsVideoStreamTrack() const override { return this; }
|
||||
|
||||
void AddVideoOutput(MediaStreamVideoSink* aSink);
|
||||
|
|
|
@ -79,7 +79,7 @@ CaptureTask::AttachTrack()
|
|||
{
|
||||
MOZ_ASSERT(NS_IsMainThread());
|
||||
|
||||
dom::VideoStreamTrack* track = mImageCapture->GetVideoStreamTrack();
|
||||
dom::MediaStreamTrack* track = mImageCapture->GetVideoStreamTrack();
|
||||
track->AddPrincipalChangeObserver(this);
|
||||
track->AddListener(mEventListener.get());
|
||||
track->AddDirectListener(this);
|
||||
|
@ -90,7 +90,7 @@ CaptureTask::DetachTrack()
|
|||
{
|
||||
MOZ_ASSERT(NS_IsMainThread());
|
||||
|
||||
dom::VideoStreamTrack* track = mImageCapture->GetVideoStreamTrack();
|
||||
dom::MediaStreamTrack* track = mImageCapture->GetVideoStreamTrack();
|
||||
track->RemovePrincipalChangeObserver(this);
|
||||
track->RemoveListener(mEventListener.get());
|
||||
track->RemoveDirectListener(this);
|
||||
|
|
|
@ -28,7 +28,7 @@ LogModule* GetICLog()
|
|||
namespace dom {
|
||||
|
||||
NS_IMPL_CYCLE_COLLECTION_INHERITED(ImageCapture, DOMEventTargetHelper,
|
||||
mVideoStreamTrack)
|
||||
mTrack)
|
||||
|
||||
NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(ImageCapture)
|
||||
NS_INTERFACE_MAP_END_INHERITING(DOMEventTargetHelper)
|
||||
|
@ -36,14 +36,13 @@ NS_INTERFACE_MAP_END_INHERITING(DOMEventTargetHelper)
|
|||
NS_IMPL_ADDREF_INHERITED(ImageCapture, DOMEventTargetHelper)
|
||||
NS_IMPL_RELEASE_INHERITED(ImageCapture, DOMEventTargetHelper)
|
||||
|
||||
ImageCapture::ImageCapture(VideoStreamTrack* aVideoStreamTrack,
|
||||
ImageCapture::ImageCapture(VideoStreamTrack* aTrack,
|
||||
nsPIDOMWindowInner* aOwnerWindow)
|
||||
: DOMEventTargetHelper(aOwnerWindow)
|
||||
, mTrack(aTrack)
|
||||
{
|
||||
MOZ_ASSERT(aOwnerWindow);
|
||||
MOZ_ASSERT(aVideoStreamTrack);
|
||||
|
||||
mVideoStreamTrack = aVideoStreamTrack;
|
||||
MOZ_ASSERT(aTrack);
|
||||
}
|
||||
|
||||
ImageCapture::~ImageCapture()
|
||||
|
@ -53,7 +52,7 @@ ImageCapture::~ImageCapture()
|
|||
|
||||
already_AddRefed<ImageCapture>
|
||||
ImageCapture::Constructor(const GlobalObject& aGlobal,
|
||||
VideoStreamTrack& aTrack,
|
||||
MediaStreamTrack& aTrack,
|
||||
ErrorResult& aRv)
|
||||
{
|
||||
nsCOMPtr<nsPIDOMWindowInner> win = do_QueryInterface(aGlobal.GetAsSupports());
|
||||
|
@ -62,15 +61,20 @@ ImageCapture::Constructor(const GlobalObject& aGlobal,
|
|||
return nullptr;
|
||||
}
|
||||
|
||||
RefPtr<ImageCapture> object = new ImageCapture(&aTrack, win);
|
||||
if (!aTrack.AsVideoStreamTrack()) {
|
||||
aRv.Throw(NS_ERROR_DOM_NOT_SUPPORTED_ERR);
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
RefPtr<ImageCapture> object = new ImageCapture(aTrack.AsVideoStreamTrack(), win);
|
||||
|
||||
return object.forget();
|
||||
}
|
||||
|
||||
VideoStreamTrack*
|
||||
MediaStreamTrack*
|
||||
ImageCapture::GetVideoStreamTrack() const
|
||||
{
|
||||
return mVideoStreamTrack;
|
||||
return mTrack;
|
||||
}
|
||||
|
||||
nsresult
|
||||
|
@ -118,25 +122,25 @@ ImageCapture::TakePhotoByMediaEngine()
|
|||
mVideoTrack->RemovePrincipalChangeObserver(this);
|
||||
}
|
||||
|
||||
RefPtr<VideoStreamTrack> mVideoTrack;
|
||||
RefPtr<ImageCapture> mImageCapture;
|
||||
const RefPtr<VideoStreamTrack> mVideoTrack;
|
||||
const RefPtr<ImageCapture> mImageCapture;
|
||||
bool mPrincipalChanged;
|
||||
};
|
||||
|
||||
RefPtr<MediaEnginePhotoCallback> callback =
|
||||
new TakePhotoCallback(mVideoStreamTrack, this);
|
||||
return mVideoStreamTrack->GetSource().TakePhoto(callback);
|
||||
new TakePhotoCallback(mTrack, this);
|
||||
return mTrack->GetSource().TakePhoto(callback);
|
||||
}
|
||||
|
||||
void
|
||||
ImageCapture::TakePhoto(ErrorResult& aResult)
|
||||
{
|
||||
// According to spec, VideoStreamTrack.readyState must be "live"; however
|
||||
// According to spec, MediaStreamTrack.readyState must be "live"; however
|
||||
// gecko doesn't implement it yet (bug 910249). Instead of readyState, we
|
||||
// check VideoStreamTrack.enable before bug 910249 is fixed.
|
||||
// check MediaStreamTrack.enable before bug 910249 is fixed.
|
||||
// The error code should be INVALID_TRACK, but spec doesn't define it in
|
||||
// ImageCaptureError. So it returns PHOTO_ERROR here before spec updates.
|
||||
if (!mVideoStreamTrack->Enabled()) {
|
||||
if (!mTrack->Enabled()) {
|
||||
PostErrorEvent(ImageCaptureError::PHOTO_ERROR, NS_ERROR_FAILURE);
|
||||
return;
|
||||
}
|
||||
|
@ -211,7 +215,7 @@ ImageCapture::CheckPrincipal()
|
|||
{
|
||||
MOZ_ASSERT(NS_IsMainThread());
|
||||
|
||||
nsCOMPtr<nsIPrincipal> principal = mVideoStreamTrack->GetPrincipal();
|
||||
nsCOMPtr<nsIPrincipal> principal = mTrack->GetPrincipal();
|
||||
|
||||
if (!GetOwner()) {
|
||||
return false;
|
||||
|
|
|
@ -21,13 +21,14 @@ LogModule* GetICLog();
|
|||
namespace dom {
|
||||
|
||||
class Blob;
|
||||
class MediaStreamTrack;
|
||||
class VideoStreamTrack;
|
||||
|
||||
/**
|
||||
* Implementation of https://dvcs.w3.org/hg/dap/raw-file/default/media-stream-
|
||||
* capture/ImageCapture.html.
|
||||
* The ImageCapture accepts a VideoStreamTrack as input source. The image will
|
||||
* be sent back as a JPG format via Blob event.
|
||||
* The ImageCapture accepts a video MediaStreamTrack as input source. The image
|
||||
* will be sent back as a JPG format via Blob event.
|
||||
*
|
||||
* All the functions in ImageCapture are run in main thread.
|
||||
*
|
||||
|
@ -49,8 +50,8 @@ public:
|
|||
// WebIDL members.
|
||||
void TakePhoto(ErrorResult& aResult);
|
||||
|
||||
// The MediaStream passed into the constructor.
|
||||
VideoStreamTrack* GetVideoStreamTrack() const;
|
||||
// The MediaStreamTrack passed into the constructor.
|
||||
MediaStreamTrack* GetVideoStreamTrack() const;
|
||||
|
||||
// nsWrapperCache member
|
||||
JSObject* WrapObject(JSContext* aCx, JS::Handle<JSObject*> aGivenProto) override
|
||||
|
@ -62,10 +63,10 @@ public:
|
|||
nsPIDOMWindowInner* GetParentObject() { return GetOwner(); }
|
||||
|
||||
static already_AddRefed<ImageCapture> Constructor(const GlobalObject& aGlobal,
|
||||
VideoStreamTrack& aTrack,
|
||||
MediaStreamTrack& aTrack,
|
||||
ErrorResult& aRv);
|
||||
|
||||
ImageCapture(VideoStreamTrack* aVideoStreamTrack,
|
||||
ImageCapture(VideoStreamTrack* aTrack,
|
||||
nsPIDOMWindowInner* aOwnerWindow);
|
||||
|
||||
// Post a Blob event to script.
|
||||
|
@ -85,7 +86,7 @@ protected:
|
|||
// should return NS_ERROR_NOT_IMPLEMENTED.
|
||||
nsresult TakePhotoByMediaEngine();
|
||||
|
||||
RefPtr<VideoStreamTrack> mVideoStreamTrack;
|
||||
RefPtr<VideoStreamTrack> mTrack;
|
||||
};
|
||||
|
||||
} // namespace dom
|
||||
|
|
|
@ -71,8 +71,8 @@ AudioSink::~AudioSink()
|
|||
{
|
||||
}
|
||||
|
||||
RefPtr<GenericPromise>
|
||||
AudioSink::Init(const PlaybackParams& aParams)
|
||||
nsresult
|
||||
AudioSink::Init(const PlaybackParams& aParams, RefPtr<GenericPromise>& aEndPromise)
|
||||
{
|
||||
MOZ_ASSERT(mOwnerThread->IsCurrentThreadIn());
|
||||
|
||||
|
@ -86,12 +86,12 @@ AudioSink::Init(const PlaybackParams& aParams)
|
|||
// To ensure at least one audio packet will be popped from AudioQueue and
|
||||
// ready to be played.
|
||||
NotifyAudioNeeded();
|
||||
RefPtr<GenericPromise> p = mEndPromise.Ensure(__func__);
|
||||
aEndPromise = mEndPromise.Ensure(__func__);
|
||||
nsresult rv = InitializeAudioStream(aParams);
|
||||
if (NS_FAILED(rv)) {
|
||||
mEndPromise.Reject(rv, __func__);
|
||||
}
|
||||
return p;
|
||||
return rv;
|
||||
}
|
||||
|
||||
TimeUnit
|
||||
|
@ -197,7 +197,8 @@ AudioSink::InitializeAudioStream(const PlaybackParams& aParams)
|
|||
// mOutputChannels into SMPTE format, so there is no need to worry if
|
||||
// StaticPrefs::accessibility_monoaudio_enable() or
|
||||
// StaticPrefs::MediaForcestereoEnabled() is applied.
|
||||
nsresult rv = mAudioStream->Init(mOutputChannels, channelMap, mOutputRate);
|
||||
nsresult rv = mAudioStream->Init(mOutputChannels, channelMap,
|
||||
mOutputRate, aParams.mSink);
|
||||
if (NS_FAILED(rv)) {
|
||||
mAudioStream->Shutdown();
|
||||
mAudioStream = nullptr;
|
||||
|
@ -209,9 +210,7 @@ AudioSink::InitializeAudioStream(const PlaybackParams& aParams)
|
|||
mAudioStream->SetVolume(aParams.mVolume);
|
||||
mAudioStream->SetPlaybackRate(aParams.mPlaybackRate);
|
||||
mAudioStream->SetPreservesPitch(aParams.mPreservesPitch);
|
||||
mAudioStream->Start();
|
||||
|
||||
return NS_OK;
|
||||
return mAudioStream->Start();
|
||||
}
|
||||
|
||||
TimeUnit
|
||||
|
|
|
@ -38,7 +38,7 @@ public:
|
|||
|
||||
// Return a promise which will be resolved when AudioSink
|
||||
// finishes playing, or rejected if any error.
|
||||
RefPtr<GenericPromise> Init(const PlaybackParams& aParams);
|
||||
nsresult Init(const PlaybackParams& aParams, RefPtr<GenericPromise>& aEndPromise);
|
||||
|
||||
/*
|
||||
* All public functions are not thread-safe.
|
||||
|
|
|
@ -178,7 +178,7 @@ AudioSinkWrapper::SetPlaying(bool aPlaying)
|
|||
}
|
||||
}
|
||||
|
||||
void
|
||||
nsresult
|
||||
AudioSinkWrapper::Start(const TimeUnit& aStartTime, const MediaInfo& aInfo)
|
||||
{
|
||||
AssertOwnerThread();
|
||||
|
@ -191,9 +191,10 @@ AudioSinkWrapper::Start(const TimeUnit& aStartTime, const MediaInfo& aInfo)
|
|||
// no audio is equivalent to audio ended before video starts.
|
||||
mAudioEnded = !aInfo.HasAudio();
|
||||
|
||||
nsresult rv = NS_OK;
|
||||
if (aInfo.HasAudio()) {
|
||||
mAudioSink.reset(mCreator->Create());
|
||||
mEndPromise = mAudioSink->Init(mParams);
|
||||
rv = mAudioSink->Init(mParams, mEndPromise);
|
||||
|
||||
mEndPromise->Then(
|
||||
mOwnerThread.get(), __func__, this,
|
||||
|
@ -201,6 +202,7 @@ AudioSinkWrapper::Start(const TimeUnit& aStartTime, const MediaInfo& aInfo)
|
|||
&AudioSinkWrapper::OnAudioEnded
|
||||
)->Track(mAudioSinkPromise);
|
||||
}
|
||||
return rv;
|
||||
}
|
||||
|
||||
void
|
||||
|
|
|
@ -67,7 +67,7 @@ public:
|
|||
void SetPreservesPitch(bool aPreservesPitch) override;
|
||||
void SetPlaying(bool aPlaying) override;
|
||||
|
||||
void Start(const TimeUnit& aStartTime, const MediaInfo& aInfo) override;
|
||||
nsresult Start(const TimeUnit& aStartTime, const MediaInfo& aInfo) override;
|
||||
void Stop() override;
|
||||
bool IsStarted() const override;
|
||||
bool IsPlaying() const override;
|
||||
|
|
|
@ -304,7 +304,7 @@ DecodedStream::OnEnded(TrackType aType)
|
|||
return nullptr;
|
||||
}
|
||||
|
||||
void
|
||||
nsresult
|
||||
DecodedStream::Start(const TimeUnit& aStartTime, const MediaInfo& aInfo)
|
||||
{
|
||||
AssertOwnerThread();
|
||||
|
@ -371,6 +371,7 @@ DecodedStream::Start(const TimeUnit& aStartTime, const MediaInfo& aInfo)
|
|||
mData->SetPlaying(mPlaying);
|
||||
SendData();
|
||||
}
|
||||
return NS_OK;
|
||||
}
|
||||
|
||||
void
|
||||
|
|
|
@ -61,7 +61,7 @@ public:
|
|||
void SetPreservesPitch(bool aPreservesPitch) override;
|
||||
void SetPlaying(bool aPlaying) override;
|
||||
|
||||
void Start(const media::TimeUnit& aStartTime, const MediaInfo& aInfo) override;
|
||||
nsresult Start(const media::TimeUnit& aStartTime, const MediaInfo& aInfo) override;
|
||||
void Stop() override;
|
||||
bool IsStarted() const override;
|
||||
bool IsPlaying() const override;
|
||||
|
|
|
@ -7,10 +7,11 @@
|
|||
#ifndef MediaSink_h_
|
||||
#define MediaSink_h_
|
||||
|
||||
#include "AudioDeviceInfo.h"
|
||||
#include "MediaInfo.h"
|
||||
#include "mozilla/RefPtr.h"
|
||||
#include "mozilla/MozPromise.h"
|
||||
#include "nsISupportsImpl.h"
|
||||
#include "MediaInfo.h"
|
||||
|
||||
namespace mozilla {
|
||||
|
||||
|
@ -44,6 +45,7 @@ public:
|
|||
double mVolume;
|
||||
double mPlaybackRate;
|
||||
bool mPreservesPitch;
|
||||
RefPtr<AudioDeviceInfo> mSink;
|
||||
};
|
||||
|
||||
// Return the playback parameters of this sink.
|
||||
|
@ -100,7 +102,7 @@ public:
|
|||
|
||||
// Begin a playback session with the provided start time and media info.
|
||||
// Must be called when playback is stopped.
|
||||
virtual void Start(const TimeUnit& aStartTime, const MediaInfo& aInfo) = 0;
|
||||
virtual nsresult Start(const TimeUnit& aStartTime, const MediaInfo& aInfo) = 0;
|
||||
|
||||
// Finish a playback session.
|
||||
// Must be called after playback starts.
|
||||
|
|
|
@ -205,13 +205,13 @@ VideoSink::SetPlaying(bool aPlaying)
|
|||
EnsureHighResTimersOnOnlyIfPlaying();
|
||||
}
|
||||
|
||||
void
|
||||
nsresult
|
||||
VideoSink::Start(const TimeUnit& aStartTime, const MediaInfo& aInfo)
|
||||
{
|
||||
AssertOwnerThread();
|
||||
VSINK_LOG("[%s]", __func__);
|
||||
|
||||
mAudioSink->Start(aStartTime, aInfo);
|
||||
nsresult rv = mAudioSink->Start(aStartTime, aInfo);
|
||||
|
||||
mHasVideo = aInfo.HasVideo();
|
||||
|
||||
|
@ -247,6 +247,7 @@ VideoSink::Start(const TimeUnit& aStartTime, const MediaInfo& aInfo)
|
|||
// when video duration is 0.
|
||||
UpdateRenderedVideoFrames();
|
||||
}
|
||||
return rv;
|
||||
}
|
||||
|
||||
void
|
||||
|
|
|
@ -58,7 +58,7 @@ public:
|
|||
|
||||
void Redraw(const VideoInfo& aInfo) override;
|
||||
|
||||
void Start(const TimeUnit& aStartTime, const MediaInfo& aInfo) override;
|
||||
nsresult Start(const TimeUnit& aStartTime, const MediaInfo& aInfo) override;
|
||||
|
||||
void Stop() override;
|
||||
|
||||
|
|
|
@ -97,6 +97,7 @@ EXPORTS += [
|
|||
'AudioCompactor.h',
|
||||
'AudioConfig.h',
|
||||
'AudioConverter.h',
|
||||
'AudioDeviceInfo.h',
|
||||
'AudioMixer.h',
|
||||
'AudioPacketizer.h',
|
||||
'AudioSampleFormat.h',
|
||||
|
|
|
@ -358,3 +358,6 @@ skip-if = (android_version == '18')
|
|||
[test_peerConnection_nonDefaultRate.html]
|
||||
skip-if = android_version == '18' # android(Bug 1189784, timeouts on 4.3 emulator)
|
||||
[test_forceSampleRate.html]
|
||||
|
||||
[test_setSinkId.html]
|
||||
skip-if = os != 'linux' # the only platform with real devices
|
||||
|
|
|
@ -0,0 +1,55 @@
|
|||
<!DOCTYPE HTML>
|
||||
<html>
|
||||
<head>
|
||||
<script type="application/javascript" src="mediaStreamPlayback.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
<pre id="test">
|
||||
|
||||
<audio id="audio"></audio>
|
||||
|
||||
<script>
|
||||
createHTML({
|
||||
title: "SetSinkId in HTMLMediaElement",
|
||||
bug: "934425",
|
||||
});
|
||||
|
||||
/**
|
||||
* Run a test to verify set sink id in audio element.
|
||||
*/
|
||||
runTest(async () => {
|
||||
await pushPrefs(["media.setsinkid.enabled", true]);
|
||||
|
||||
if (!SpecialPowers.getCharPref("media.audio_loopback_dev", "")) {
|
||||
ok(false, "No loopback device set by framework. Try --use-test-media-devices");
|
||||
return;
|
||||
}
|
||||
|
||||
const allDevices = await navigator.mediaDevices.enumerateDevices();
|
||||
const audioDevices = allDevices.filter(({kind}) => kind == 'audiooutput');
|
||||
info(`Found ${audioDevices.length} output devices`);
|
||||
ok(audioDevices.length > 0, "More than one output device found");
|
||||
|
||||
is(audio.sinkId, "", "Initial value is empty string");
|
||||
|
||||
const p = audio.setSinkId(audioDevices[0].deviceId);
|
||||
is(audio.sinkId, "", "Value is unchanged upon function return");
|
||||
is(await p, undefined, "promise resolves with undefined");
|
||||
is(audio.sinkId, audioDevices[0].deviceId, `Sink device is set, id: ${audio.sinkId}`);
|
||||
|
||||
await audio.setSinkId(audioDevices[0].deviceId);
|
||||
ok(true, `Sink device is set for 2nd time for the same id: ${audio.sinkId}`);
|
||||
|
||||
try {
|
||||
await audio.setSinkId("dummy sink id");
|
||||
ok(false, "Never enter here, this must fail");
|
||||
} catch (error) {
|
||||
ok(true, `Set sink id expected to fail: ${error}`);
|
||||
is(error.name, "NotFoundError", "Verify correct error");
|
||||
}
|
||||
});
|
||||
|
||||
</script>
|
||||
</pre>
|
||||
</body>
|
||||
</html>
|
|
@ -580,7 +580,8 @@ MediaEngineDefault::EnumerateDevices(uint64_t aWindowId,
|
|||
aDevices->AppendElement(MakeRefPtr<MediaDevice>(
|
||||
newSource,
|
||||
newSource->GetName(),
|
||||
NS_ConvertUTF8toUTF16(newSource->GetUUID())));
|
||||
NS_ConvertUTF8toUTF16(newSource->GetUUID()),
|
||||
NS_LITERAL_STRING("")));
|
||||
return;
|
||||
}
|
||||
case dom::MediaSourceEnum::Microphone: {
|
||||
|
@ -591,7 +592,8 @@ MediaEngineDefault::EnumerateDevices(uint64_t aWindowId,
|
|||
aDevices->AppendElement(MakeRefPtr<MediaDevice>(
|
||||
source,
|
||||
source->GetName(),
|
||||
NS_ConvertUTF8toUTF16(source->GetUUID())));
|
||||
NS_ConvertUTF8toUTF16(source->GetUUID()),
|
||||
NS_LITERAL_STRING("")));
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -602,7 +604,8 @@ MediaEngineDefault::EnumerateDevices(uint64_t aWindowId,
|
|||
aDevices->AppendElement(MakeRefPtr<MediaDevice>(
|
||||
newSource,
|
||||
newSource->GetName(),
|
||||
NS_ConvertUTF8toUTF16(newSource->GetUUID())));
|
||||
NS_ConvertUTF8toUTF16(newSource->GetUUID()),
|
||||
NS_LITERAL_STRING("")));
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
|
|
@ -166,7 +166,8 @@ MediaEngineWebRTC::EnumerateVideoDevices(uint64_t aWindowId,
|
|||
aDevices->AppendElement(MakeRefPtr<MediaDevice>(
|
||||
vSource,
|
||||
vSource->GetName(),
|
||||
NS_ConvertUTF8toUTF16(vSource->GetUUID())));
|
||||
NS_ConvertUTF8toUTF16(vSource->GetUUID()),
|
||||
NS_LITERAL_STRING("")));
|
||||
}
|
||||
|
||||
if (mHasTabVideoSource || dom::MediaSourceEnum::Browser == aMediaSource) {
|
||||
|
@ -174,7 +175,8 @@ MediaEngineWebRTC::EnumerateVideoDevices(uint64_t aWindowId,
|
|||
aDevices->AppendElement(MakeRefPtr<MediaDevice>(
|
||||
tabVideoSource,
|
||||
tabVideoSource->GetName(),
|
||||
NS_ConvertUTF8toUTF16(tabVideoSource->GetUUID())));
|
||||
NS_ConvertUTF8toUTF16(tabVideoSource->GetUUID()),
|
||||
NS_LITERAL_STRING("")));
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -218,7 +220,8 @@ MediaEngineWebRTC::EnumerateMicrophoneDevices(uint64_t aWindowId,
|
|||
RefPtr<MediaDevice> device = MakeRefPtr<MediaDevice>(
|
||||
source,
|
||||
source->GetName(),
|
||||
NS_ConvertUTF8toUTF16(source->GetUUID()));
|
||||
NS_ConvertUTF8toUTF16(source->GetUUID()),
|
||||
NS_LITERAL_STRING(""));
|
||||
if (devices[i]->Preferred()) {
|
||||
#ifdef DEBUG
|
||||
if (!foundPreferredDevice) {
|
||||
|
@ -251,10 +254,7 @@ MediaEngineWebRTC::EnumerateSpeakerDevices(uint64_t aWindowId,
|
|||
// would be the same for both which ends up to create the same
|
||||
// deviceIDs (in JS).
|
||||
uuid.Append(NS_LITERAL_STRING("_Speaker"));
|
||||
aDevices->AppendElement(MakeRefPtr<MediaDevice>(
|
||||
device->Name(),
|
||||
dom::MediaDeviceKind::Audiooutput,
|
||||
uuid));
|
||||
aDevices->AppendElement(MakeRefPtr<MediaDevice>(device, uuid));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -278,7 +278,8 @@ MediaEngineWebRTC::EnumerateDevices(uint64_t aWindowId,
|
|||
aDevices->AppendElement(MakeRefPtr<MediaDevice>(
|
||||
audioCaptureSource,
|
||||
audioCaptureSource->GetName(),
|
||||
NS_ConvertUTF8toUTF16(audioCaptureSource->GetUUID())));
|
||||
NS_ConvertUTF8toUTF16(audioCaptureSource->GetUUID()),
|
||||
NS_LITERAL_STRING("")));
|
||||
} else if (aMediaSource == dom::MediaSourceEnum::Microphone) {
|
||||
MOZ_ASSERT(aMediaSource == dom::MediaSourceEnum::Microphone);
|
||||
EnumerateMicrophoneDevices(aWindowId, aDevices);
|
||||
|
|
|
@ -620,7 +620,8 @@ MediaConstraintsHelper::FindBadConstraint(
|
|||
AutoTArray<RefPtr<MediaDevice>, 1> devices;
|
||||
devices.AppendElement(MakeRefPtr<MediaDevice>(aMediaEngineSource,
|
||||
aMediaEngineSource->GetName(),
|
||||
aDeviceId));
|
||||
aDeviceId,
|
||||
NS_LITERAL_STRING("")));
|
||||
return FindBadConstraint(aConstraints, devices);
|
||||
}
|
||||
|
||||
|
|
|
@ -152,8 +152,6 @@ var interfaceNamesInGlobalScope =
|
|||
{name: "AudioProcessingEvent", insecureContext: true},
|
||||
// IMPORTANT: Do not change this list without review from a DOM peer!
|
||||
{name: "AudioScheduledSourceNode", insecureContext: true},
|
||||
// IMPORTANT: Do not change this list without review from a DOM peer!
|
||||
{name: "AudioStreamTrack", insecureContext: true},
|
||||
// IMPORTANT: Do not change this list without review from a DOM peer!
|
||||
{name: "AudioWorkletNode", insecureContext: false, disabled: true},
|
||||
// IMPORTANT: Do not change this list without review from a DOM peer!
|
||||
|
@ -1154,8 +1152,6 @@ var interfaceNamesInGlobalScope =
|
|||
{name: "ValidityState", insecureContext: true},
|
||||
// IMPORTANT: Do not change this list without review from a DOM peer!
|
||||
{name: "VideoPlaybackQuality", insecureContext: true},
|
||||
// IMPORTANT: Do not change this list without review from a DOM peer!
|
||||
{name: "VideoStreamTrack", insecureContext: true},
|
||||
// IMPORTANT: Do not change this list without review from a DOM peer!
|
||||
{name: "VisualViewport", insecureContext: true},
|
||||
// IMPORTANT: Do not change this list without review from a DOM peer!
|
||||
|
|
|
@ -1,16 +0,0 @@
|
|||
/* -*- Mode: IDL; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
|
||||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this file,
|
||||
* You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
*
|
||||
* The origin of this IDL file is
|
||||
* http://dev.w3.org/2011/webrtc/editor/getusermedia.html
|
||||
*
|
||||
* Copyright © 2012 W3C® (MIT, ERCIM, Keio), All Rights Reserved. W3C
|
||||
* liability, trademark and document use rules apply.
|
||||
*/
|
||||
|
||||
// [Constructor(optional MediaTrackConstraints audioConstraints)]
|
||||
interface AudioStreamTrack : MediaStreamTrack {
|
||||
// static sequence<DOMString> getSourceIds ();
|
||||
};
|
|
@ -220,6 +220,14 @@ partial interface HTMLMediaElement {
|
|||
boolean hasSuspendTaint();
|
||||
};
|
||||
|
||||
/* Audio Output Devices API */
|
||||
partial interface HTMLMediaElement {
|
||||
[Pref="media.setsinkid.enabled"]
|
||||
readonly attribute DOMString sinkId;
|
||||
[Throws, Pref="media.setsinkid.enabled"]
|
||||
Promise<void> setSinkId(DOMString sinkId);
|
||||
};
|
||||
|
||||
/*
|
||||
* API that exposes whether a call to HTMLMediaElement.play() would be
|
||||
* blocked by autoplay policies; whether the promise returned by play()
|
||||
|
|
|
@ -10,10 +10,10 @@
|
|||
* W3C liability, trademark and document use rules apply.
|
||||
*/
|
||||
|
||||
[Pref="dom.imagecapture.enabled", Constructor(VideoStreamTrack track)]
|
||||
[Pref="dom.imagecapture.enabled", Constructor(MediaStreamTrack track)]
|
||||
interface ImageCapture : EventTarget {
|
||||
// readonly attribute PhotoSettingsOptions photoSettingsOptions;
|
||||
readonly attribute VideoStreamTrack videoStreamTrack;
|
||||
readonly attribute MediaStreamTrack videoStreamTrack;
|
||||
attribute EventHandler onphoto;
|
||||
attribute EventHandler onerror;
|
||||
// attribute EventHandler onphotosettingschange;
|
||||
|
|
|
@ -29,8 +29,8 @@ dictionary MediaStreamConstraints {
|
|||
Constructor (sequence<MediaStreamTrack> tracks)]
|
||||
interface MediaStream : EventTarget {
|
||||
readonly attribute DOMString id;
|
||||
sequence<AudioStreamTrack> getAudioTracks ();
|
||||
sequence<VideoStreamTrack> getVideoTracks ();
|
||||
sequence<MediaStreamTrack> getAudioTracks ();
|
||||
sequence<MediaStreamTrack> getVideoTracks ();
|
||||
sequence<MediaStreamTrack> getTracks ();
|
||||
MediaStreamTrack? getTrackById (DOMString trackId);
|
||||
void addTrack (MediaStreamTrack track);
|
||||
|
|
|
@ -1,19 +0,0 @@
|
|||
/* -*- Mode: IDL; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
|
||||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this file,
|
||||
* You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
*
|
||||
* The origin of this IDL file is
|
||||
* http://dev.w3.org/2011/webrtc/editor/getusermedia.html
|
||||
*
|
||||
* Copyright © 2012 W3C® (MIT, ERCIM, Keio), All Rights Reserved. W3C
|
||||
* liability, trademark and document use rules apply.
|
||||
*/
|
||||
|
||||
// [Constructor(optional MediaTrackConstraints videoConstraints)]
|
||||
interface VideoStreamTrack : MediaStreamTrack {
|
||||
// static sequence<DOMString> getSourceIds ();
|
||||
// void takePhoto ();
|
||||
// attribute EventHandler onphoto;
|
||||
// attribute EventHandler onphotoerror;
|
||||
};
|
|
@ -387,7 +387,6 @@ WEBIDL_FILES = [
|
|||
'AudioParamMap.webidl',
|
||||
'AudioProcessingEvent.webidl',
|
||||
'AudioScheduledSourceNode.webidl',
|
||||
'AudioStreamTrack.webidl',
|
||||
'AudioTrack.webidl',
|
||||
'AudioTrackList.webidl',
|
||||
'AudioWorklet.webidl',
|
||||
|
@ -918,7 +917,6 @@ WEBIDL_FILES = [
|
|||
'URLSearchParams.webidl',
|
||||
'ValidityState.webidl',
|
||||
'VideoPlaybackQuality.webidl',
|
||||
'VideoStreamTrack.webidl',
|
||||
'VideoTrack.webidl',
|
||||
'VideoTrackList.webidl',
|
||||
'VisualViewport.webidl',
|
||||
|
|
|
@ -841,6 +841,14 @@ gfxPlatform::Init()
|
|||
/* this currently will only succeed on Windows */
|
||||
gfxInfo = services::GetGfxInfo();
|
||||
|
||||
if (XRE_IsParentProcess()) {
|
||||
// Some gfxVars must be initialized prior gPlatform for coherent results.
|
||||
gfxVars::SetDXInterop2Blocked(IsDXInterop2Blocked());
|
||||
gfxVars::SetDXNV12Blocked(IsDXNV12Blocked());
|
||||
gfxVars::SetDXP010Blocked(IsDXP010Blocked());
|
||||
gfxVars::SetDXP016Blocked(IsDXP016Blocked());
|
||||
}
|
||||
|
||||
#if defined(XP_WIN)
|
||||
gPlatform = new gfxWindowsPlatform;
|
||||
#elif defined(XP_MACOSX)
|
||||
|
@ -961,10 +969,6 @@ gfxPlatform::Init()
|
|||
InitOpenGLConfig();
|
||||
|
||||
if (XRE_IsParentProcess()) {
|
||||
gfxVars::SetDXInterop2Blocked(IsDXInterop2Blocked());
|
||||
gfxVars::SetDXNV12Blocked(IsDXNV12Blocked());
|
||||
gfxVars::SetDXP010Blocked(IsDXP010Blocked());
|
||||
gfxVars::SetDXP016Blocked(IsDXP016Blocked());
|
||||
Preferences::Unlock(FONT_VARIATIONS_PREF);
|
||||
if (!gPlatform->HasVariationFontSupport()) {
|
||||
// Ensure variation fonts are disabled and the pref is locked.
|
||||
|
|
|
@ -78,12 +78,9 @@ nsDOMCSSAttributeDeclaration::SetCSSDeclaration(DeclarationBlock* aDecl,
|
|||
// needed.
|
||||
MOZ_ASSERT_IF(!mIsSMILOverride, aClosureData);
|
||||
|
||||
// If the closure hasn't been called because the declaration wasn't changed,
|
||||
// we need to explicitly call it now to get InlineStyleDeclarationWillChange
|
||||
// notification before SetInlineStyleDeclaration.
|
||||
if (aClosureData && aClosureData->mClosure) {
|
||||
aClosureData->mClosure(aClosureData);
|
||||
}
|
||||
// The closure needs to have been called by now, otherwise we shouldn't be
|
||||
// getting here when the attribute hasn't changed.
|
||||
MOZ_ASSERT_IF(aClosureData, !aClosureData->mClosure);
|
||||
|
||||
aDecl->SetDirty();
|
||||
return mIsSMILOverride
|
||||
|
|
|
@ -127,12 +127,6 @@ nsDOMCSSDeclaration::SetCssText(const nsAString& aCssText,
|
|||
ParsingEnvironment servoEnv =
|
||||
GetParsingEnvironment(aSubjectPrincipal);
|
||||
if (!servoEnv.mUrlExtraData) {
|
||||
if (created) {
|
||||
// In case we can't set a new declaration, but one was
|
||||
// created for the old one, we need to set the old declaration to
|
||||
// get right style attribute handling.
|
||||
SetCSSDeclaration(olddecl, &closureData);
|
||||
}
|
||||
aRv.Throw(NS_ERROR_NOT_AVAILABLE);
|
||||
return;
|
||||
}
|
||||
|
@ -287,22 +281,12 @@ nsDOMCSSDeclaration::ModifyDeclaration(nsIPrincipal* aSubjectPrincipal,
|
|||
ParsingEnvironment servoEnv =
|
||||
GetParsingEnvironment(aSubjectPrincipal);
|
||||
if (!servoEnv.mUrlExtraData) {
|
||||
if (created) {
|
||||
// In case we can't set a new declaration, but one was
|
||||
// created for the old one, we need to set the old declaration to
|
||||
// get right style attribute handling.
|
||||
SetCSSDeclaration(olddecl, aClosureData);
|
||||
}
|
||||
return NS_ERROR_NOT_AVAILABLE;
|
||||
}
|
||||
|
||||
changed = aFunc(decl, servoEnv);
|
||||
|
||||
if (!changed) {
|
||||
if (created) {
|
||||
// See comment above about setting old declaration.
|
||||
SetCSSDeclaration(olddecl, aClosureData);
|
||||
}
|
||||
// Parsing failed -- but we don't throw an exception for that.
|
||||
return NS_OK;
|
||||
}
|
||||
|
|
|
@ -156,7 +156,7 @@ public final class NotificationClient implements NotificationListener {
|
|||
|
||||
if (!AppConstants.Versions.preO) {
|
||||
builder.setChannelId(NotificationHelper.getInstance(mContext)
|
||||
.getNotificationChannel(NotificationHelper.Channel.DEFAULT).getId());
|
||||
.getNotificationChannel(NotificationHelper.Channel.SITE_NOTIFICATIONS).getId());
|
||||
}
|
||||
|
||||
// Fetch icon.
|
||||
|
|
|
@ -116,6 +116,10 @@ public final class NotificationHelper implements BundleEventListener {
|
|||
* Leanplum notification channel - use only when <code>AppConstants.MOZ_ANDROID_MMA</code> is true.
|
||||
*/
|
||||
LP_DEFAULT,
|
||||
/**
|
||||
* HTML5 web site notifications
|
||||
*/
|
||||
SITE_NOTIFICATIONS,
|
||||
}
|
||||
|
||||
// Holds the mapping between the Channel enum used by the rest of our codebase and the
|
||||
|
@ -143,6 +147,9 @@ public final class NotificationHelper implements BundleEventListener {
|
|||
|
||||
final String SYNCED_TABS_CHANNEL_TAG = "synced-tabs-notification-channel";
|
||||
put(Channel.SYNCED_TABS, SYNCED_TABS_CHANNEL_TAG);
|
||||
|
||||
final String SITE_NOTIFICATIONS_CHANNEL_TAG = "site-notifications";
|
||||
put(Channel.SITE_NOTIFICATIONS, SITE_NOTIFICATIONS_CHANNEL_TAG);
|
||||
}};
|
||||
|
||||
// These are channels we no longer require and want to retire from Android's settings UI.
|
||||
|
@ -261,6 +268,13 @@ public final class NotificationHelper implements BundleEventListener {
|
|||
}
|
||||
break;
|
||||
|
||||
case SITE_NOTIFICATIONS: {
|
||||
channel = new NotificationChannel(mDefinedNotificationChannels.get(definedChannel),
|
||||
mContext.getString(R.string.site_notifications_channel),
|
||||
NotificationManager.IMPORTANCE_DEFAULT);
|
||||
}
|
||||
break;
|
||||
|
||||
case DEFAULT:
|
||||
default: {
|
||||
channel = new NotificationChannel(mDefinedNotificationChannels.get(definedChannel),
|
||||
|
|
|
@ -901,3 +901,6 @@ Picture-in-picture mini window -->
|
|||
<!ENTITY leanplum_default_notifications_channel "&brandShortName; Push notifications">
|
||||
<!ENTITY updater_notification_channel "App updates">
|
||||
<!ENTITY synced_tabs_notification_channel "Synced tabs">
|
||||
<!-- LOCALIZATION NOTE (site_notifications_channel): This is for system notifications displayed by
|
||||
web sites through the HTML Notifications API. -->
|
||||
<!ENTITY site_notifications_channel "Site notifications">
|
||||
|
|
|
@ -653,4 +653,5 @@
|
|||
<string name="leanplum_default_notifications_channel">&leanplum_default_notifications_channel;</string>
|
||||
<string name="updater_notification_channel">&updater_notification_channel;</string>
|
||||
<string name="synced_tabs_notification_channel">&synced_tabs_notification_channel;</string>
|
||||
<string name="site_notifications_channel">&site_notifications_channel;</string>
|
||||
</resources>
|
||||
|
|
|
@ -13,7 +13,7 @@ class LSANLeaks(object):
|
|||
in allocation stacks
|
||||
"""
|
||||
|
||||
def __init__(self, logger, scope=None, allowed=None):
|
||||
def __init__(self, logger, scope=None, allowed=None, maxNumRecordedFrames=None):
|
||||
self.logger = logger
|
||||
self.inReport = False
|
||||
self.fatalError = False
|
||||
|
@ -21,7 +21,7 @@ class LSANLeaks(object):
|
|||
self.foundFrames = set()
|
||||
self.recordMoreFrames = None
|
||||
self.currStack = None
|
||||
self.maxNumRecordedFrames = 4
|
||||
self.maxNumRecordedFrames = maxNumRecordedFrames if maxNumRecordedFrames else 4
|
||||
self.summaryData = None
|
||||
self.scope = scope
|
||||
self.allowedMatch = None
|
||||
|
@ -136,6 +136,7 @@ class LSANLeaks(object):
|
|||
self.logger.info("LeakSanitizer | To show the "
|
||||
"addresses of leaked objects add report_objects=1 to LSAN_OPTIONS\n"
|
||||
"This can be done in testing/mozbase/mozrunner/mozrunner/utils.py")
|
||||
self.logger.info("Allowed depth was %d" % self.maxNumRecordedFrames)
|
||||
|
||||
for frames, allowed in self.foundFrames:
|
||||
self.logger.lsan_leak(frames, scope=self.scope, allowed_match=allowed)
|
||||
|
|
|
@ -34,6 +34,8 @@ user_pref("extensions.legacy.enabled", true);
|
|||
user_pref("extensions.update.enabled", false);
|
||||
// Disable useragent updates.
|
||||
user_pref("general.useragent.updates.enabled", false);
|
||||
// Ensure WR doesn't get enabled in tests unless we do it explicitly with the MOZ_WEBRENDER envvar.
|
||||
user_pref("gfx.webrender.all.qualified", false);
|
||||
user_pref("hangmonitor.timeout", 0); // no hang monitor
|
||||
user_pref("media.gmp-manager.updateEnabled", false);
|
||||
// Make enablePrivilege continue to work for test code. :-(
|
||||
|
|
|
@ -74,6 +74,12 @@
|
|||
"c:\\windows\\prefetch\\{prefetch}.pf": {
|
||||
"ignore": true
|
||||
},
|
||||
"c:\\windows\\system32\\apphelp.dll": {
|
||||
"mincount": 0,
|
||||
"maxcount": 2,
|
||||
"minbytes": 0,
|
||||
"maxbytes": 32768
|
||||
},
|
||||
"c:\\windows\\system32\\windows.storage.dll": {
|
||||
"mincount": 2,
|
||||
"maxcount": 2,
|
||||
|
|
|
@ -327250,6 +327250,12 @@
|
|||
{}
|
||||
]
|
||||
],
|
||||
"audio-output/setSinkId.html": [
|
||||
[
|
||||
"/audio-output/setSinkId.html",
|
||||
{}
|
||||
]
|
||||
],
|
||||
"audio-output/setSinkId.https.html": [
|
||||
[
|
||||
"/audio-output/setSinkId.https.html",
|
||||
|
@ -442345,6 +442351,10 @@
|
|||
"a083cdf09232110039d3bb825e207c678b336114",
|
||||
"manual"
|
||||
],
|
||||
"audio-output/setSinkId.html": [
|
||||
"bd5d8e43b0fd9d0c9f1e078ed97a1bbd18b7b0be",
|
||||
"testharness"
|
||||
],
|
||||
"audio-output/setSinkId.https.html": [
|
||||
"2ce0b482b7eea61c0c56c49ec14dc1630b7b9a9c",
|
||||
"testharness"
|
||||
|
|
|
@ -0,0 +1,2 @@
|
|||
[setSinkId.html]
|
||||
prefs: [media.setsinkid.enabled:true]
|
|
@ -1,10 +1,2 @@
|
|||
[setSinkId.https.html]
|
||||
[setSinkId on default audio output should always work]
|
||||
expected: FAIL
|
||||
|
||||
[setSinkId fails with NotFoundError on made up deviceid]
|
||||
expected: FAIL
|
||||
|
||||
[List media devices]
|
||||
expected: FAIL
|
||||
|
||||
prefs: [media.setsinkid.enabled:true]
|
||||
|
|
|
@ -1,4 +0,0 @@
|
|||
[cssstyledeclaration-mutationrecord-002.html]
|
||||
[CSSStyleDeclaration.setPropertyValue doesn't queue a mutation record when setting invalid values]
|
||||
expected: FAIL
|
||||
|
|
@ -5,16 +5,10 @@
|
|||
[MediaStreamTrack interface: attribute onoverconstrained]
|
||||
expected: FAIL
|
||||
|
||||
[MediaStreamTrack must be primary interface of [object AudioStreamTrack\]]
|
||||
[MediaStreamTrack interface: [object MediaStreamTrack\] must inherit property "getCapabilities()" with the proper type]
|
||||
expected: FAIL
|
||||
|
||||
[Stringification of [object AudioStreamTrack\]]
|
||||
expected: FAIL
|
||||
|
||||
[MediaStreamTrack interface: [object AudioStreamTrack\] must inherit property "getCapabilities()" with the proper type]
|
||||
expected: FAIL
|
||||
|
||||
[MediaStreamTrack interface: [object AudioStreamTrack\] must inherit property "onoverconstrained" with the proper type]
|
||||
[MediaStreamTrack interface: [object MediaStreamTrack\] must inherit property "onoverconstrained" with the proper type]
|
||||
expected: FAIL
|
||||
|
||||
[OverconstrainedErrorEvent interface: existence and properties of interface object]
|
||||
|
|
|
@ -1,2 +1,2 @@
|
|||
prefs: [dom.serviceWorkers.enabled:true]
|
||||
lsan-allowed: [Alloc, Create, CreateInner, MakeUnique, Malloc, NewChannelFromURIWithProxyFlagsInternal, NewEmptyScopeData, NewPage, OrInsert, PLDHashTable::Add, Realloc, SharedMutex, __rdl_alloc, __rdl_realloc, js_new, js_pod_calloc, js_pod_malloc, js_pod_realloc, mozilla::BasePrincipal::CreateCodebasePrincipal, mozilla::ThrottledEventQueue::Create, mozilla::WeakPtr, mozilla::dom::ChromeUtils::GenerateQI, mozilla::dom::Performance::CreateForMainThread, mozilla::dom::PerformanceStorageWorker::Create, mozilla::dom::WorkerPrivate::WorkerPrivate, mozilla::net::HttpBaseChannel::HttpBaseChannel, mozilla::net::HttpChannelChild::HttpChannelChild, mozilla::net::nsHttpAuthIdentity::Set, mozilla::net::nsHttpHandler::NewProxiedChannel2, nsNodeSupportsWeakRefTearoff::GetWeakReference, nsPrefetchService::Preload, nsSegmentedBuffer::AppendNewSegment]
|
||||
lsan-allowed: [Alloc, Create, CreateInner, MakeUnique, Malloc, NewChannelFromURIWithProxyFlagsInternal, NewEmptyScopeData, NewPage, OrInsert, PLDHashTable::Add, Realloc, SharedMutex, __rdl_alloc, __rdl_realloc, js_new, js_pod_calloc, js_pod_malloc, js_pod_realloc, mozilla::BasePrincipal::CreateCodebasePrincipal, mozilla::ThrottledEventQueue::Create, mozilla::WeakPtr, mozilla::dom::ChromeUtils::GenerateQI, mozilla::dom::Performance::CreateForMainThread, mozilla::dom::PerformanceStorageWorker::Create, mozilla::dom::WorkerPrivate::WorkerPrivate, mozilla::net::HttpBaseChannel::HttpBaseChannel, mozilla::net::HttpChannelChild::HttpChannelChild, mozilla::net::nsHttpAuthIdentity::Set, mozilla::net::nsHttpHandler::NewProxiedChannel2, nsNodeSupportsWeakRefTearoff::GetWeakReference, nsPrefetchService::Preload, nsSegmentedBuffer::AppendNewSegment, nsDocShell::Create]
|
||||
|
|
|
@ -1 +1,2 @@
|
|||
lsan-allowed: [Alloc, Create, Malloc, NewPage, PLDHashTable::Add, PLDHashTable::ChangeTable, Realloc, RecvOnAcknowledge, RecvOnStop, mozilla::BasePrincipal::CreateCodebasePrincipal, mozilla::SchedulerGroup::CreateEventTargetFor, mozilla::ThrottledEventQueue::Create, mozilla::WeakPtr, mozilla::dom::WebSocket::WebSocket, mozilla::dom::WorkerCSPEventListener::Create, mozilla::dom::nsIContentChild::GetConstructedEventTarget, mozilla::net::WebSocketChannelChild::RecvOnServerClose, nsAtomTable::Atomize, mozilla::net::nsStandardURL::TemplatedMutator]
|
||||
lsan-allowed: [Alloc, Create, Malloc, NewPage, PLDHashTable::Add, PLDHashTable::ChangeTable, Realloc, RecvOnAcknowledge, RecvOnStop, mozilla::BasePrincipal::CreateCodebasePrincipal, mozilla::SchedulerGroup::CreateEventTargetFor, mozilla::ThrottledEventQueue::Create, mozilla::WeakPtr, mozilla::dom::WebSocket::WebSocket, mozilla::dom::WorkerCSPEventListener::Create, mozilla::dom::nsIContentChild::GetConstructedEventTarget, mozilla::net::WebSocketChannelChild::RecvOnServerClose, nsAtomTable::Atomize, mozilla::net::nsStandardURL::TemplatedMutator, nsDocShell::Create]
|
||||
lsan-max-stack-depth: 7
|
||||
|
|
|
@ -0,0 +1,44 @@
|
|||
<!doctype html>
|
||||
<meta charset=utf-8>
|
||||
<title>Test setSinkId behavior </title>
|
||||
<div id='log'></div>
|
||||
<script src=/resources/testharness.js></script>
|
||||
<script src=/resources/testharnessreport.js></script>
|
||||
<script>
|
||||
"use strict";
|
||||
|
||||
const audio = new Audio();
|
||||
|
||||
promise_test(t => audio.setSinkId(""), "setSinkId on default audio output should always work");
|
||||
|
||||
promise_test(t => promise_rejects(t, "NotFoundError", audio.setSinkId("nonexistent_device_id")),
|
||||
"setSinkId fails with NotFoundError on made up deviceid");
|
||||
|
||||
promise_test(async t => {
|
||||
const list = await navigator.mediaDevices.enumerateDevices();
|
||||
const outputDevicesList = list.filter(({kind}) => kind == "audiooutput");
|
||||
assert_not_equals(outputDevicesList.length, 0,
|
||||
"media device list includes at least one audio output device");
|
||||
|
||||
let acceptedDevices = 0;
|
||||
for (const {deviceId} of outputDevicesList) {
|
||||
const {deviceId} = outputDevicesList[0];
|
||||
const p1 = audio.setSinkId(deviceId);
|
||||
assert_equals(audio.sinkId, "", "before it resolves, setSinkId is unchanged");
|
||||
try {
|
||||
let r = await p1;
|
||||
assert_equals(acceptedDevices, 0, "only the default sink device can be set");
|
||||
acceptedDevices++;
|
||||
assert_equals(r, undefined, "setSinkId resolves with undefined");
|
||||
assert_equals(audio.sinkId, deviceId, "when it resolves, setSinkId updates sinkId to the requested deviceId");
|
||||
r = await audio.setSinkId(deviceId);
|
||||
assert_equals(r, undefined, "resetting sinkid on same current value should always work");
|
||||
r = await audio.setSinkId("");
|
||||
assert_equals(r, undefined, "resetting sinkid on default audio output should always work");
|
||||
} catch (e) {
|
||||
assert_equals(e.name, "NotAllowedError", "Non-default devices are failing with NotAllowed error");
|
||||
}
|
||||
}
|
||||
}, "List device, setSinkId should on the default, the rest of the devices will get a NotAlowedError");
|
||||
|
||||
</script>
|
|
@ -1,45 +1,46 @@
|
|||
<!doctype html>
|
||||
<html>
|
||||
<head>
|
||||
<title>Test setSinkId behavior </title>
|
||||
<link rel="author" title="Dominique Hazael-Massieux" href="mailto:dom@w3.org"/>
|
||||
<link rel="help" href="https://www.w3.org/TR/audio-output/#dom-htmlmediaelement-setsinkid">
|
||||
</head>
|
||||
<body>
|
||||
<h1 class="instructions">Description</h1>
|
||||
<p class="instructions">This test checks that <code>setSinkId</code> follows the algorithm (but does not consider actual rendering of the audio which needs to be manual).</p>
|
||||
<div id='log'></div>
|
||||
<script src=/resources/testharness.js></script>
|
||||
<script src=/resources/testharnessreport.js></script>
|
||||
<script>
|
||||
"use strict";
|
||||
|
||||
const is_output = d => d.kind === "audiooutput";
|
||||
const audio = new Audio();
|
||||
|
||||
promise_test(t => audio.setSinkId(""), "setSinkId on default audio output should always work");
|
||||
|
||||
promise_test(t => promise_rejects(t, "NotFoundError", audio.setSinkId("inexistent_device_id")), "setSinkId fails with NotFoundError on made up deviceid");
|
||||
promise_test(t => promise_rejects(t, "NotFoundError", audio.setSinkId("nonexistent_device_id")),
|
||||
"setSinkId fails with NotFoundError on made up deviceid");
|
||||
|
||||
promise_test(t =>
|
||||
navigator.mediaDevices.enumerateDevices().then(list => {
|
||||
assert_not_equals(list.find(is_output), undefined, "media device list includes at least one audio output device");
|
||||
// since we haven't gained any specific permission,
|
||||
// for all listed audio output devices, calling setSinkId with device id can
|
||||
// either create a security exception or work and thus reflect the deviceId
|
||||
let acceptedDevice = 0;
|
||||
list.filter(is_output).forEach((d,i) => promise_test(td => audio.setSinkId(d.deviceId).then(r => {
|
||||
promise_test(async t => {
|
||||
const list = await navigator.mediaDevices.enumerateDevices();
|
||||
const outputDevicesList = list.filter(({kind}) => kind == "audiooutput");
|
||||
assert_not_equals(outputDevicesList.length, 0,
|
||||
"media device list includes at least one audio output device");
|
||||
|
||||
let acceptedDevices = 0;
|
||||
for (const {deviceId} of outputDevicesList) {
|
||||
const {deviceId} = outputDevicesList[0];
|
||||
const p1 = audio.setSinkId(deviceId);
|
||||
assert_equals(audio.sinkId, "", "before it resolves, setSinkId is unchanged");
|
||||
try {
|
||||
let r = await p1;
|
||||
assert_equals(acceptedDevices, 0, "only the default sink device can be set");
|
||||
acceptedDevices++;
|
||||
assert_equals(r, undefined, "setSinkId resolves with undefined");
|
||||
assert_equals(audio.sinkId, d.deviceId, "when it resolves, setSinkId updates sinkId to the requested deviceId");
|
||||
assert_equals(acceptedDevice, 0, "only one output device can be set without permission");
|
||||
acceptedDevice++;
|
||||
promise_test(t => audio.setSinkId(d.deviceId), "resetting sinkid on same current value should always work");
|
||||
promise_test(t => audio.setSinkId(""), "resetting sinkid on default audio output should always work");
|
||||
}, e => {
|
||||
assert_equals(e.name, "SecurityError", "On known devices, the only possible failure of setSinkId is a securityerror"); // assuming AbortError can't happen in the test environment by default
|
||||
}), "Correctly reacts to setting known deviceid as sinkid " + i));
|
||||
}), "List media devices");
|
||||
assert_equals(audio.sinkId, deviceId, "when it resolves, setSinkId updates sinkId to the requested deviceId");
|
||||
r = await audio.setSinkId(deviceId);
|
||||
assert_equals(r, undefined, "resetting sinkid on same current value should always work");
|
||||
r = await audio.setSinkId("");
|
||||
assert_equals(r, undefined, "resetting sinkid on default audio output should always work");
|
||||
} catch (e) {
|
||||
assert_equals(e.name, "NotAllowedError", "Non-default devices are failing with NotAllowed error");
|
||||
}
|
||||
}
|
||||
}, "List device, setSinkId should on the default, the rest of the devices will get a NotAlowedError");
|
||||
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
<!doctype html>
|
||||
<meta charset="utf-8">
|
||||
<title>CSSOM: CSSStyleDeclaration.setPropertyValue doesn't queue a mutation record for invalid values</title>
|
||||
<link rel="help" href="https://drafts.csswg.org/cssom/#dom-cssstyledeclaration-setpropertyvalue">
|
||||
<link rel="help" href="https://drafts.csswg.org/cssom/#dom-cssstyledeclaration-setproperty">
|
||||
<link rel="author" title="Emilio Cobos Álvarez" href="mailto:emilio@crisal.io">
|
||||
<script src=/resources/testharness.js></script>
|
||||
<script src=/resources/testharnessreport.js></script>
|
||||
|
|
|
@ -0,0 +1,16 @@
|
|||
<!doctype html>
|
||||
<html style="color: inherit">
|
||||
<meta charset="utf-8">
|
||||
<title>CSSOM: CSSStyleDeclaration.setPropertyValue doesn't queue a mutation record for invalid values</title>
|
||||
<link rel="help" href="https://drafts.csswg.org/cssom/#dom-cssstyledeclaration-setproperty">
|
||||
<link rel="author" title="Emilio Cobos Álvarez" href="mailto:emilio@crisal.io">
|
||||
<script src=/resources/testharness.js></script>
|
||||
<script src=/resources/testharnessreport.js></script>
|
||||
<script>
|
||||
let test = async_test("CSSStyleDeclaration.setPropertyValue doesn't queue a mutation record when setting invalid values");
|
||||
let m = new MutationObserver(test.unreached_func("shouldn't queue a mutation record"));
|
||||
m.observe(document.documentElement, { attributes: true });
|
||||
|
||||
document.documentElement.style.width = "-100px";
|
||||
requestAnimationFrame(() => test.done());
|
||||
</script>
|
|
@ -0,0 +1,18 @@
|
|||
<!doctype html>
|
||||
<title>CSSOM test: declaration block after setting via CSSOM</title>
|
||||
<link rel="help" href="https://drafts.csswg.org/cssom/#dom-cssstyledeclaration-setproperty">
|
||||
<script src="/resources/testharness.js"></script>
|
||||
<script src="/resources/testharnessreport.js"></script>
|
||||
<link rel="author" title="Emilio Cobos Álvarez" href="mailto:emilio@crisal.io">
|
||||
<script>
|
||||
test(function() {
|
||||
let element = document.createElement("div");
|
||||
element.style.setProperty("doesntexist", "0");
|
||||
assert_false(element.hasAttribute("style"));
|
||||
}, "Setting an invalid property via the declaration setter doesn't create a declaration");
|
||||
test(function() {
|
||||
let element = document.createElement("div");
|
||||
element.style.setProperty("width", "-100");
|
||||
assert_false(element.hasAttribute("style"));
|
||||
}, "Setting an invalid value via the declaration setter doesn't create a declaration");
|
||||
</script>
|
|
@ -189,6 +189,7 @@ class FirefoxBrowser(Browser):
|
|||
|
||||
self.asan = asan
|
||||
self.lsan_allowed = None
|
||||
self.lsan_max_stack_depth = None
|
||||
self.leak_check = leak_check
|
||||
self.leak_report_file = None
|
||||
self.lsan_handler = None
|
||||
|
@ -198,6 +199,7 @@ class FirefoxBrowser(Browser):
|
|||
|
||||
def settings(self, test):
|
||||
self.lsan_allowed = test.lsan_allowed
|
||||
self.lsan_max_stack_depth = test.lsan_max_stack_depth
|
||||
return {"check_leaks": self.leak_check and not test.leaks,
|
||||
"lsan_allowed": test.lsan_allowed}
|
||||
|
||||
|
@ -213,7 +215,8 @@ class FirefoxBrowser(Browser):
|
|||
print "Setting up LSAN"
|
||||
self.lsan_handler = mozleak.LSANLeaks(self.logger,
|
||||
scope=group_metadata.get("scope", "/"),
|
||||
allowed=self.lsan_allowed)
|
||||
allowed=self.lsan_allowed,
|
||||
maxNumRecordedFrames=self.lsan_max_stack_depth)
|
||||
|
||||
env = test_environment(xrePath=os.path.dirname(self.binary),
|
||||
debugger=self.debug_info is not None,
|
||||
|
|
|
@ -156,6 +156,10 @@ class ExpectedManifest(ManifestItem):
|
|||
def lsan_allowed(self):
|
||||
return lsan_allowed(self)
|
||||
|
||||
@property
|
||||
def lsan_max_stack_depth(self):
|
||||
return int_prop("lsan-max-stack-depth", self)
|
||||
|
||||
|
||||
class DirectoryManifest(ManifestItem):
|
||||
@property
|
||||
|
@ -190,6 +194,9 @@ class DirectoryManifest(ManifestItem):
|
|||
def lsan_allowed(self):
|
||||
return lsan_allowed(self)
|
||||
|
||||
@property
|
||||
def lsan_max_stack_depth(self):
|
||||
return int_prop("lsan-max-stack-depth", self)
|
||||
|
||||
class TestNode(ManifestItem):
|
||||
def __init__(self, name):
|
||||
|
@ -251,6 +258,10 @@ class TestNode(ManifestItem):
|
|||
def lsan_allowed(self):
|
||||
return lsan_allowed(self)
|
||||
|
||||
@property
|
||||
def lsan_max_stack_depth(self):
|
||||
return int_prop("lsan-max-stack-depth", self)
|
||||
|
||||
def append(self, node):
|
||||
"""Add a subtest to the current test
|
||||
|
||||
|
|
|
@ -18,6 +18,10 @@ min-asserts: 1
|
|||
tags: [b, c]
|
||||
"""
|
||||
|
||||
dir_ini_2 = """\
|
||||
lsan-max-stack-depth: 42
|
||||
"""
|
||||
|
||||
test_0 = """\
|
||||
[0.html]
|
||||
prefs: [c:d]
|
||||
|
@ -33,6 +37,11 @@ test_1 = """\
|
|||
if os == 'win': FAIL
|
||||
"""
|
||||
|
||||
test_2 = """\
|
||||
[2.html]
|
||||
lsan-max-stack-depth: 42
|
||||
"""
|
||||
|
||||
|
||||
def test_metadata_inherit():
|
||||
tests = make_mock_manifest(("test", "a", 10), ("test", "a/b", 10),
|
||||
|
@ -72,3 +81,40 @@ def test_conditional():
|
|||
test_obj = wpttest.from_manifest(test, [], test_metadata.get_test(test.id))
|
||||
assert test_obj.prefs == {"a": "b", "c": "d"}
|
||||
assert test_obj.expected() == "FAIL"
|
||||
|
||||
def test_metadata_lsan_stack_depth():
|
||||
tests = make_mock_manifest(("test", "a", 10), ("test", "a/b", 10))
|
||||
|
||||
test_metadata = manifestexpected.static.compile(BytesIO(test_2),
|
||||
{},
|
||||
data_cls_getter=manifestexpected.data_cls_getter,
|
||||
test_path="a",
|
||||
url_base="")
|
||||
|
||||
test = tests[2][2].pop()
|
||||
test_obj = wpttest.from_manifest(test, [], test_metadata.get_test(test.id))
|
||||
|
||||
assert test_obj.lsan_max_stack_depth == 42
|
||||
|
||||
test = tests[1][2].pop()
|
||||
test_obj = wpttest.from_manifest(test, [], test_metadata.get_test(test.id))
|
||||
|
||||
assert test_obj.lsan_max_stack_depth == None
|
||||
|
||||
test_metadata = manifestexpected.static.compile(BytesIO(test_0),
|
||||
{},
|
||||
data_cls_getter=manifestexpected.data_cls_getter,
|
||||
test_path="a",
|
||||
url_base="")
|
||||
|
||||
inherit_metadata = [
|
||||
manifestexpected.static.compile(
|
||||
BytesIO(dir_ini_2),
|
||||
{},
|
||||
data_cls_getter=lambda x,y: manifestexpected.DirectoryManifest)
|
||||
]
|
||||
|
||||
test = tests[0][2].pop()
|
||||
test_obj = wpttest.from_manifest(test, inherit_metadata, test_metadata.get_test(test.id))
|
||||
|
||||
assert test_obj.lsan_max_stack_depth == 42
|
||||
|
|
|
@ -234,6 +234,14 @@ class Test(object):
|
|||
break
|
||||
return lsan_allowed
|
||||
|
||||
@property
|
||||
def lsan_max_stack_depth(self):
|
||||
for meta in self.itermeta(None):
|
||||
depth = meta.lsan_max_stack_depth
|
||||
if depth is not None:
|
||||
return depth
|
||||
return None
|
||||
|
||||
@property
|
||||
def tags(self):
|
||||
tags = set()
|
||||
|
|
|
@ -1868,7 +1868,7 @@ async function placesBookmarkToSyncBookmark(db, bookmarkItem) {
|
|||
// Converts a Sync bookmark object to a Places bookmark or livemark object.
|
||||
// This function maps record IDs to Places GUIDs, and filters out extra Sync
|
||||
// properties like keywords, tags. Returns an object that can be passed to
|
||||
// `PlacesUtils.livemarks.addLivemark` or `PlacesUtils.bookmarks.{insert, update}`.
|
||||
// `PlacesUtils.bookmarks.{insert, update}`.
|
||||
function syncBookmarkToPlacesBookmark(info) {
|
||||
let bookmarkInfo = {
|
||||
source: SOURCE_SYNC,
|
||||
|
|
|
@ -1,516 +0,0 @@
|
|||
/* Any copyright is dedicated to the Public Domain.
|
||||
http://creativecommons.org/publicdomain/zero/1.0/ */
|
||||
|
||||
// Tests functionality of the mozIAsyncLivemarks interface.
|
||||
|
||||
const FEED_URI = NetUtil.newURI("http://feed.rss/");
|
||||
const SITE_URI = NetUtil.newURI("http://site.org/");
|
||||
|
||||
let unfiledFolderId;
|
||||
|
||||
// This test must be the first one, since it's testing the cache.
|
||||
add_task(async function test_livemark_cache() {
|
||||
unfiledFolderId =
|
||||
await PlacesUtils.promiseItemId(PlacesUtils.bookmarks.unfiledGuid);
|
||||
|
||||
// Add a livemark through other APIs.
|
||||
let folder = await PlacesUtils.bookmarks.insert({
|
||||
type: PlacesUtils.bookmarks.TYPE_FOLDER,
|
||||
title: "test",
|
||||
parentGuid: PlacesUtils.bookmarks.unfiledGuid,
|
||||
});
|
||||
let id = await PlacesUtils.promiseItemId(folder.guid);
|
||||
PlacesUtils.annotations
|
||||
.setItemAnnotation(id, PlacesUtils.LMANNO_FEEDURI,
|
||||
"http://example.com/feed",
|
||||
0, PlacesUtils.annotations.EXPIRE_NEVER);
|
||||
PlacesUtils.annotations
|
||||
.setItemAnnotation(id, PlacesUtils.LMANNO_SITEURI,
|
||||
"http://example.com/site",
|
||||
0, PlacesUtils.annotations.EXPIRE_NEVER);
|
||||
|
||||
let livemark = await PlacesUtils.livemarks.getLivemark({ guid: folder.guid });
|
||||
Assert.equal(folder.guid, livemark.guid);
|
||||
Assert.equal(folder.dateAdded * 1000, livemark.dateAdded);
|
||||
Assert.equal(folder.parentGuid, livemark.parentGuid);
|
||||
Assert.equal(folder.index, livemark.index);
|
||||
Assert.equal(folder.title, livemark.title);
|
||||
Assert.equal(id, livemark.id);
|
||||
Assert.equal(unfiledFolderId, livemark.parentId);
|
||||
Assert.equal("http://example.com/feed", livemark.feedURI.spec);
|
||||
Assert.equal("http://example.com/site", livemark.siteURI.spec);
|
||||
|
||||
await PlacesUtils.livemarks.removeLivemark(livemark);
|
||||
});
|
||||
|
||||
add_task(async function test_addLivemark_noArguments_throws() {
|
||||
try {
|
||||
await PlacesUtils.livemarks.addLivemark();
|
||||
do_throw("Invoking addLivemark with no arguments should throw");
|
||||
} catch (ex) {
|
||||
Assert.equal(ex.result, Cr.NS_ERROR_XPC_NOT_ENOUGH_ARGS);
|
||||
}
|
||||
});
|
||||
|
||||
add_task(async function test_addLivemark_emptyObject_throws() {
|
||||
try {
|
||||
await PlacesUtils.livemarks.addLivemark({});
|
||||
do_throw("Invoking addLivemark with empty object should throw");
|
||||
} catch (ex) {
|
||||
Assert.equal(ex.result, Cr.NS_ERROR_INVALID_ARG);
|
||||
}
|
||||
});
|
||||
|
||||
add_task(async function test_addLivemark_badParentId_throws() {
|
||||
try {
|
||||
await PlacesUtils.livemarks.addLivemark({ parentId: "test" });
|
||||
do_throw("Invoking addLivemark with a bad parent id should throw");
|
||||
} catch (ex) {
|
||||
Assert.equal(ex.result, Cr.NS_ERROR_INVALID_ARG);
|
||||
}
|
||||
});
|
||||
|
||||
add_task(async function test_addLivemark_invalidParentId_throws() {
|
||||
try {
|
||||
await PlacesUtils.livemarks.addLivemark({ parentId: -2 });
|
||||
do_throw("Invoking addLivemark with an invalid parent id should throw");
|
||||
} catch (ex) {
|
||||
Assert.equal(ex.result, Cr.NS_ERROR_INVALID_ARG);
|
||||
}
|
||||
});
|
||||
|
||||
add_task(async function test_addLivemark_noIndex_throws() {
|
||||
try {
|
||||
await PlacesUtils.livemarks.addLivemark({
|
||||
parentId: unfiledFolderId });
|
||||
do_throw("Invoking addLivemark with no index should throw");
|
||||
} catch (ex) {
|
||||
Assert.equal(ex.result, Cr.NS_ERROR_INVALID_ARG);
|
||||
}
|
||||
});
|
||||
|
||||
add_task(async function test_addLivemark_badIndex_throws() {
|
||||
try {
|
||||
await PlacesUtils.livemarks.addLivemark(
|
||||
{ parentId: unfiledFolderId,
|
||||
index: "test" });
|
||||
do_throw("Invoking addLivemark with a bad index should throw");
|
||||
} catch (ex) {
|
||||
Assert.equal(ex.result, Cr.NS_ERROR_INVALID_ARG);
|
||||
}
|
||||
});
|
||||
|
||||
add_task(async function test_addLivemark_invalidIndex_throws() {
|
||||
try {
|
||||
await PlacesUtils.livemarks.addLivemark(
|
||||
{ parentId: unfiledFolderId,
|
||||
index: -2,
|
||||
});
|
||||
do_throw("Invoking addLivemark with an invalid index should throw");
|
||||
} catch (ex) {
|
||||
Assert.equal(ex.result, Cr.NS_ERROR_INVALID_ARG);
|
||||
}
|
||||
});
|
||||
|
||||
add_task(async function test_addLivemark_noFeedURI_throws() {
|
||||
try {
|
||||
await PlacesUtils.livemarks.addLivemark(
|
||||
{ parentGuid: PlacesUtils.bookmarks.unfiledGuid });
|
||||
do_throw("Invoking addLivemark with no feedURI should throw");
|
||||
} catch (ex) {
|
||||
Assert.equal(ex.result, Cr.NS_ERROR_INVALID_ARG);
|
||||
}
|
||||
});
|
||||
|
||||
add_task(async function test_addLivemark_badFeedURI_throws() {
|
||||
try {
|
||||
await PlacesUtils.livemarks.addLivemark(
|
||||
{ parentGuid: PlacesUtils.bookmarks.unfiledGuid,
|
||||
feedURI: "test" });
|
||||
do_throw("Invoking addLivemark with a bad feedURI should throw");
|
||||
} catch (ex) {
|
||||
Assert.equal(ex.result, Cr.NS_ERROR_INVALID_ARG);
|
||||
}
|
||||
});
|
||||
|
||||
add_task(async function test_addLivemark_badSiteURI_throws() {
|
||||
try {
|
||||
await PlacesUtils.livemarks.addLivemark(
|
||||
{ parentGuid: PlacesUtils.bookmarks.unfiledGuid,
|
||||
feedURI: FEED_URI,
|
||||
siteURI: "test" });
|
||||
do_throw("Invoking addLivemark with a bad siteURI should throw");
|
||||
} catch (ex) {
|
||||
Assert.equal(ex.result, Cr.NS_ERROR_INVALID_ARG);
|
||||
}
|
||||
});
|
||||
|
||||
add_task(async function test_addLivemark_badGuid_throws() {
|
||||
try {
|
||||
await PlacesUtils.livemarks.addLivemark(
|
||||
{ parentGuid: PlacesUtils.bookmarks.unfiledGuid,
|
||||
feedURI: FEED_URI,
|
||||
guid: "123456" });
|
||||
do_throw("Invoking addLivemark with a bad guid should throw");
|
||||
} catch (ex) {
|
||||
Assert.equal(ex.result, Cr.NS_ERROR_INVALID_ARG);
|
||||
}
|
||||
});
|
||||
|
||||
add_task(async function test_addLivemark_parentId_succeeds() {
|
||||
let onItemAddedCalled = false;
|
||||
let listener = events => {
|
||||
Assert.equal(events.length, 1);
|
||||
let event = events[0];
|
||||
onItemAddedCalled = true;
|
||||
PlacesUtils.observers.removeListener(["bookmark-added"], listener);
|
||||
Assert.equal(event.parentId, unfiledFolderId);
|
||||
Assert.equal(event.index, 0);
|
||||
Assert.equal(event.itemType, PlacesUtils.bookmarks.TYPE_FOLDER);
|
||||
Assert.equal(event.title, "test");
|
||||
};
|
||||
PlacesUtils.observers.addListener(["bookmark-added"], listener);
|
||||
|
||||
await PlacesUtils.livemarks.addLivemark(
|
||||
{ title: "test",
|
||||
parentId: unfiledFolderId,
|
||||
feedURI: FEED_URI });
|
||||
Assert.ok(onItemAddedCalled);
|
||||
});
|
||||
|
||||
|
||||
add_task(async function test_addLivemark_noSiteURI_succeeds() {
|
||||
let livemark = await PlacesUtils.livemarks.addLivemark(
|
||||
{ title: "test",
|
||||
parentGuid: PlacesUtils.bookmarks.unfiledGuid,
|
||||
feedURI: FEED_URI,
|
||||
});
|
||||
Assert.ok(livemark.id > 0);
|
||||
do_check_valid_places_guid(livemark.guid);
|
||||
Assert.equal(livemark.title, "test");
|
||||
Assert.equal(livemark.parentId, unfiledFolderId);
|
||||
Assert.equal(livemark.parentGuid, PlacesUtils.bookmarks.unfiledGuid);
|
||||
Assert.ok(livemark.feedURI.equals(FEED_URI));
|
||||
Assert.equal(livemark.siteURI, null);
|
||||
Assert.ok(livemark.lastModified > 0);
|
||||
Assert.equal(livemark.dateAdded, livemark.lastModified);
|
||||
|
||||
let bookmark = await PlacesUtils.bookmarks.fetch(livemark.guid);
|
||||
Assert.equal(livemark.index, bookmark.index);
|
||||
Assert.equal(livemark.dateAdded, bookmark.dateAdded * 1000);
|
||||
});
|
||||
|
||||
add_task(async function test_addLivemark_succeeds() {
|
||||
let livemark = await PlacesUtils.livemarks.addLivemark(
|
||||
{ title: "test",
|
||||
parentGuid: PlacesUtils.bookmarks.unfiledGuid,
|
||||
feedURI: FEED_URI,
|
||||
siteURI: SITE_URI,
|
||||
});
|
||||
|
||||
Assert.ok(livemark.id > 0);
|
||||
do_check_valid_places_guid(livemark.guid);
|
||||
Assert.equal(livemark.title, "test");
|
||||
Assert.equal(livemark.parentId, unfiledFolderId);
|
||||
Assert.equal(livemark.parentGuid, PlacesUtils.bookmarks.unfiledGuid);
|
||||
Assert.ok(livemark.feedURI.equals(FEED_URI));
|
||||
Assert.ok(livemark.siteURI.equals(SITE_URI));
|
||||
Assert.ok(PlacesUtils.annotations
|
||||
.itemHasAnnotation(livemark.id,
|
||||
PlacesUtils.LMANNO_FEEDURI));
|
||||
Assert.ok(PlacesUtils.annotations
|
||||
.itemHasAnnotation(livemark.id,
|
||||
PlacesUtils.LMANNO_SITEURI));
|
||||
});
|
||||
|
||||
add_task(async function test_addLivemark_bogusid_succeeds() {
|
||||
let livemark = await PlacesUtils.livemarks.addLivemark(
|
||||
{ id: 100, // Should be ignored.
|
||||
title: "test",
|
||||
parentGuid: PlacesUtils.bookmarks.unfiledGuid,
|
||||
feedURI: FEED_URI,
|
||||
siteURI: SITE_URI,
|
||||
});
|
||||
Assert.ok(livemark.id > 0);
|
||||
Assert.notEqual(livemark.id, 100);
|
||||
});
|
||||
|
||||
add_task(async function test_addLivemark_bogusParentId_fails() {
|
||||
try {
|
||||
await PlacesUtils.livemarks.addLivemark(
|
||||
{ title: "test",
|
||||
parentId: 187,
|
||||
feedURI: FEED_URI,
|
||||
});
|
||||
do_throw("Adding a livemark with a bogus parent should fail");
|
||||
} catch (ex) {}
|
||||
});
|
||||
|
||||
add_task(async function test_addLivemark_bogusParentGuid_fails() {
|
||||
try {
|
||||
await PlacesUtils.livemarks.addLivemark(
|
||||
{ title: "test",
|
||||
parentGuid: "123456789012",
|
||||
feedURI: FEED_URI,
|
||||
});
|
||||
do_throw("Adding a livemark with a bogus parent should fail");
|
||||
} catch (ex) {}
|
||||
});
|
||||
|
||||
add_task(async function test_addLivemark_intoLivemark_fails() {
|
||||
let livemark = await PlacesUtils.livemarks.addLivemark(
|
||||
{ title: "test",
|
||||
parentGuid: PlacesUtils.bookmarks.unfiledGuid,
|
||||
feedURI: FEED_URI,
|
||||
});
|
||||
|
||||
try {
|
||||
await PlacesUtils.livemarks.addLivemark(
|
||||
{ title: "test",
|
||||
parentGuid: livemark.guid,
|
||||
feedURI: FEED_URI,
|
||||
});
|
||||
do_throw("Adding a livemark into a livemark should fail");
|
||||
} catch (ex) {
|
||||
Assert.equal(ex.result, Cr.NS_ERROR_INVALID_ARG);
|
||||
}
|
||||
});
|
||||
|
||||
add_task(async function test_addLivemark_forceGuid_succeeds() {
|
||||
let livemark = await PlacesUtils.livemarks.addLivemark(
|
||||
{ title: "test",
|
||||
parentGuid: PlacesUtils.bookmarks.unfiledGuid,
|
||||
feedURI: FEED_URI,
|
||||
guid: "1234567890AB",
|
||||
});
|
||||
Assert.equal(livemark.guid, "1234567890AB");
|
||||
do_check_guid_for_bookmark(livemark.id, "1234567890AB");
|
||||
});
|
||||
|
||||
add_task(async function test_addLivemark_dateAdded_succeeds() {
|
||||
let dateAdded = new Date("2013-03-01T01:10:00") * 1000;
|
||||
let livemark = await PlacesUtils.livemarks.addLivemark(
|
||||
{ title: "test",
|
||||
parentGuid: PlacesUtils.bookmarks.unfiledGuid,
|
||||
feedURI: FEED_URI,
|
||||
dateAdded,
|
||||
});
|
||||
Assert.equal(livemark.dateAdded, dateAdded);
|
||||
});
|
||||
|
||||
add_task(async function test_addLivemark_lastModified_succeeds() {
|
||||
let now = Date.now() * 1000;
|
||||
let livemark = await PlacesUtils.livemarks.addLivemark(
|
||||
{ title: "test",
|
||||
parentGuid: PlacesUtils.bookmarks.unfiledGuid,
|
||||
feedURI: FEED_URI,
|
||||
lastModified: now,
|
||||
});
|
||||
Assert.equal(livemark.dateAdded, now);
|
||||
Assert.equal(livemark.lastModified, now);
|
||||
});
|
||||
|
||||
add_task(async function test_removeLivemark_emptyObject_throws() {
|
||||
try {
|
||||
await PlacesUtils.livemarks.removeLivemark({});
|
||||
do_throw("Invoking removeLivemark with empty object should throw");
|
||||
} catch (ex) {
|
||||
Assert.equal(ex.result, Cr.NS_ERROR_INVALID_ARG);
|
||||
}
|
||||
});
|
||||
|
||||
add_task(async function test_removeLivemark_noValidId_throws() {
|
||||
try {
|
||||
await PlacesUtils.livemarks.removeLivemark({ id: -10, guid: "test"});
|
||||
do_throw("Invoking removeLivemark with no valid id should throw");
|
||||
} catch (ex) {
|
||||
Assert.equal(ex.result, Cr.NS_ERROR_INVALID_ARG);
|
||||
}
|
||||
});
|
||||
|
||||
add_task(async function test_removeLivemark_nonExistent_fails() {
|
||||
try {
|
||||
await PlacesUtils.livemarks.removeLivemark({ id: 1337 });
|
||||
do_throw("Removing a non-existent livemark should fail");
|
||||
} catch (ex) {
|
||||
}
|
||||
});
|
||||
|
||||
add_task(async function test_removeLivemark_guid_succeeds() {
|
||||
let livemark = await PlacesUtils.livemarks.addLivemark(
|
||||
{ title: "test",
|
||||
parentGuid: PlacesUtils.bookmarks.unfiledGuid,
|
||||
feedURI: FEED_URI,
|
||||
guid: "234567890ABC",
|
||||
});
|
||||
|
||||
Assert.equal(livemark.guid, "234567890ABC");
|
||||
|
||||
await PlacesUtils.livemarks.removeLivemark({
|
||||
id: 789, guid: "234567890ABC",
|
||||
});
|
||||
|
||||
Assert.equal((await PlacesUtils.bookmarks.fetch("234567890ABC")), null);
|
||||
});
|
||||
|
||||
add_task(async function test_removeLivemark_id_succeeds() {
|
||||
let livemark = await PlacesUtils.livemarks.addLivemark(
|
||||
{ title: "test",
|
||||
parentGuid: PlacesUtils.bookmarks.unfiledGuid,
|
||||
feedURI: FEED_URI,
|
||||
});
|
||||
|
||||
await PlacesUtils.livemarks.removeLivemark({ id: livemark.id });
|
||||
|
||||
Assert.equal((await PlacesUtils.bookmarks.fetch("234567890ABC")), null);
|
||||
});
|
||||
|
||||
add_task(async function test_getLivemark_emptyObject_throws() {
|
||||
try {
|
||||
await PlacesUtils.livemarks.getLivemark({});
|
||||
do_throw("Invoking getLivemark with empty object should throw");
|
||||
} catch (ex) {
|
||||
Assert.equal(ex.result, Cr.NS_ERROR_INVALID_ARG);
|
||||
}
|
||||
});
|
||||
|
||||
add_task(async function test_getLivemark_noValidId_throws() {
|
||||
try {
|
||||
await PlacesUtils.livemarks.getLivemark({ id: -10, guid: "test"});
|
||||
do_throw("Invoking getLivemark with no valid id should throw");
|
||||
} catch (ex) {
|
||||
Assert.equal(ex.result, Cr.NS_ERROR_INVALID_ARG);
|
||||
}
|
||||
});
|
||||
|
||||
add_task(async function test_getLivemark_nonExistentId_fails() {
|
||||
try {
|
||||
await PlacesUtils.livemarks.getLivemark({ id: 1234 });
|
||||
do_throw("getLivemark for a non existent id should fail");
|
||||
} catch (ex) {}
|
||||
});
|
||||
|
||||
add_task(async function test_getLivemark_nonExistentGUID_fails() {
|
||||
try {
|
||||
await PlacesUtils.livemarks.getLivemark({ guid: "34567890ABCD" });
|
||||
do_throw("getLivemark for a non-existent guid should fail");
|
||||
} catch (ex) {}
|
||||
});
|
||||
|
||||
add_task(async function test_getLivemark_guid_succeeds() {
|
||||
await PlacesUtils.livemarks.addLivemark(
|
||||
{ title: "test",
|
||||
parentGuid: PlacesUtils.bookmarks.unfiledGuid,
|
||||
feedURI: FEED_URI,
|
||||
guid: "34567890ABCD" });
|
||||
|
||||
// invalid id to check the guid wins.
|
||||
let livemark =
|
||||
await PlacesUtils.livemarks.getLivemark({ id: 789, guid: "34567890ABCD" });
|
||||
|
||||
Assert.equal(livemark.title, "test");
|
||||
Assert.equal(livemark.parentId, unfiledFolderId);
|
||||
Assert.equal(livemark.parentGuid, PlacesUtils.bookmarks.unfiledGuid);
|
||||
Assert.ok(livemark.feedURI.equals(FEED_URI));
|
||||
Assert.equal(livemark.siteURI, null);
|
||||
Assert.equal(livemark.guid, "34567890ABCD");
|
||||
|
||||
let bookmark = await PlacesUtils.bookmarks.fetch("34567890ABCD");
|
||||
Assert.equal(livemark.index, bookmark.index);
|
||||
});
|
||||
|
||||
add_task(async function test_getLivemark_id_succeeds() {
|
||||
let livemark = await PlacesUtils.livemarks.addLivemark(
|
||||
{ title: "test",
|
||||
parentGuid: PlacesUtils.bookmarks.unfiledGuid,
|
||||
feedURI: FEED_URI,
|
||||
});
|
||||
|
||||
livemark = await PlacesUtils.livemarks.getLivemark({ id: livemark.id });
|
||||
|
||||
Assert.equal(livemark.title, "test");
|
||||
Assert.equal(livemark.parentId, unfiledFolderId);
|
||||
Assert.equal(livemark.parentGuid, PlacesUtils.bookmarks.unfiledGuid);
|
||||
Assert.ok(livemark.feedURI.equals(FEED_URI));
|
||||
Assert.equal(livemark.siteURI, null);
|
||||
do_check_guid_for_bookmark(livemark.id, livemark.guid);
|
||||
|
||||
let bookmark = await PlacesUtils.bookmarks.fetch(livemark.guid);
|
||||
Assert.equal(livemark.index, bookmark.index);
|
||||
});
|
||||
|
||||
add_task(async function test_getLivemark_removeItem_contention() {
|
||||
// do not yield.
|
||||
PlacesUtils.livemarks.addLivemark({ title: "test",
|
||||
parentGuid: PlacesUtils.bookmarks.unfiledGuid,
|
||||
feedURI: FEED_URI,
|
||||
}).catch(() => { /* swallow errors*/ });
|
||||
await PlacesUtils.bookmarks.eraseEverything();
|
||||
let livemark = await PlacesUtils.livemarks.addLivemark(
|
||||
{ title: "test",
|
||||
parentGuid: PlacesUtils.bookmarks.unfiledGuid,
|
||||
feedURI: FEED_URI,
|
||||
});
|
||||
|
||||
livemark = await PlacesUtils.livemarks.getLivemark({ guid: livemark.guid });
|
||||
|
||||
Assert.equal(livemark.title, "test");
|
||||
Assert.equal(livemark.parentId, unfiledFolderId);
|
||||
Assert.ok(livemark.feedURI.equals(FEED_URI));
|
||||
Assert.equal(livemark.siteURI, null);
|
||||
do_check_guid_for_bookmark(livemark.id, livemark.guid);
|
||||
});
|
||||
|
||||
add_task(async function test_title_change() {
|
||||
let livemark = await PlacesUtils.livemarks.addLivemark(
|
||||
{ title: "test",
|
||||
parentGuid: PlacesUtils.bookmarks.unfiledGuid,
|
||||
feedURI: FEED_URI,
|
||||
});
|
||||
|
||||
await PlacesUtils.bookmarks.update({ guid: livemark.guid,
|
||||
title: "test2" });
|
||||
// Poll for the title change.
|
||||
while (true) {
|
||||
let lm = await PlacesUtils.livemarks.getLivemark({ guid: livemark.guid });
|
||||
if (lm.title == "test2")
|
||||
break;
|
||||
await new Promise(resolve => do_timeout(resolve, 100));
|
||||
}
|
||||
});
|
||||
|
||||
add_task(async function test_livemark_move() {
|
||||
let livemark = await PlacesUtils.livemarks.addLivemark(
|
||||
{ title: "test",
|
||||
parentGuid: PlacesUtils.bookmarks.unfiledGuid,
|
||||
feedURI: FEED_URI } );
|
||||
|
||||
await PlacesUtils.bookmarks.update({ guid: livemark.guid,
|
||||
parentGuid: PlacesUtils.bookmarks.toolbarGuid,
|
||||
index: PlacesUtils.bookmarks.DEFAULT_INDEX });
|
||||
// Poll for the parent change.
|
||||
while (true) {
|
||||
let lm = await PlacesUtils.livemarks.getLivemark({ guid: livemark.guid });
|
||||
if (lm.parentGuid == PlacesUtils.bookmarks.toolbarGuid)
|
||||
break;
|
||||
await new Promise(resolve => do_timeout(resolve, 100));
|
||||
}
|
||||
});
|
||||
|
||||
add_task(async function test_livemark_removed() {
|
||||
let livemark = await PlacesUtils.livemarks.addLivemark(
|
||||
{ title: "test",
|
||||
parentGuid: PlacesUtils.bookmarks.unfiledGuid,
|
||||
feedURI: FEED_URI } );
|
||||
|
||||
await PlacesUtils.bookmarks.remove(livemark.guid);
|
||||
// Poll for the livemark removal.
|
||||
while (true) {
|
||||
try {
|
||||
await PlacesUtils.livemarks.getLivemark({ guid: livemark.guid });
|
||||
} catch (ex) {
|
||||
break;
|
||||
}
|
||||
await new Promise(resolve => do_timeout(resolve, 100));
|
||||
}
|
||||
});
|
Загрузка…
Ссылка в новой задаче