Source code

Revision control

Copy as Markdown

Other Tools

var { PermissionTestUtils } = ChromeUtils.importESModule(
);
const PREF_PERMISSION_FAKE = "media.navigator.permission.fake";
const PREF_AUDIO_LOOPBACK = "media.audio_loopback_dev";
const PREF_VIDEO_LOOPBACK = "media.video_loopback_dev";
const PREF_FAKE_STREAMS = "media.navigator.streams.fake";
const PREF_FOCUS_SOURCE = "media.getusermedia.window.focus_source.enabled";
const STATE_CAPTURE_ENABLED = Ci.nsIMediaManagerService.STATE_CAPTURE_ENABLED;
const STATE_CAPTURE_DISABLED = Ci.nsIMediaManagerService.STATE_CAPTURE_DISABLED;
const ALLOW_SILENCING_NOTIFICATIONS = Services.prefs.getBoolPref(
"privacy.webrtc.allowSilencingNotifications",
false
);
const SHOW_GLOBAL_MUTE_TOGGLES = Services.prefs.getBoolPref(
"privacy.webrtc.globalMuteToggles",
false
);
let IsIndicatorDisabled =
AppConstants.isPlatformAndVersionAtLeast("macosx", 14.0) &&
!Services.prefs.getBoolPref(
"privacy.webrtc.showIndicatorsOnMacos14AndAbove",
false
);
const IS_MAC = AppConstants.platform == "macosx";
const SHARE_SCREEN = 1;
const SHARE_WINDOW = 2;
let observerTopics = [
"getUserMedia:response:allow",
"getUserMedia:revoke",
"getUserMedia:response:deny",
"getUserMedia:request",
"recording-device-events",
"recording-window-ended",
];
// Structured hierarchy of subframes. Keys are frame id:s, The children member
// contains nested sub frames if any. The noTest member make a frame be ignored
// for testing if true.
let gObserveSubFrames = {};
// Object of subframes to test. Each element contains the members bc and id, for
// the frames BrowsingContext and id, respectively.
let gSubFramesToTest = [];
let gBrowserContextsToObserve = [];
function whenDelayedStartupFinished(aWindow) {
return TestUtils.topicObserved(
"browser-delayed-startup-finished",
subject => subject == aWindow
);
}
function promiseIndicatorWindow() {
let startTime = performance.now();
return new Promise(resolve => {
Services.obs.addObserver(function obs(win) {
win.addEventListener(
"load",
function () {
if (win.location.href !== INDICATOR_PATH) {
info("ignoring a window with this url: " + win.location.href);
return;
}
Services.obs.removeObserver(obs, "domwindowopened");
executeSoon(() => {
ChromeUtils.addProfilerMarker("promiseIndicatorWindow", {
startTime,
category: "Test",
});
resolve(win);
});
},
{ once: true }
);
}, "domwindowopened");
});
}
async function assertWebRTCIndicatorStatus(expected) {
let ui = ChromeUtils.importESModule(
).webrtcUI;
let expectedState = expected ? "visible" : "hidden";
let msg = "WebRTC indicator " + expectedState;
if (!expected && ui.showGlobalIndicator) {
// It seems the global indicator is not always removed synchronously
// in some cases.
await TestUtils.waitForCondition(
() => !ui.showGlobalIndicator,
"waiting for the global indicator to be hidden"
);
}
is(ui.showGlobalIndicator, !!expected, msg);
let expectVideo = false,
expectAudio = false,
expectScreen = "";
if (expected && !IsIndicatorDisabled) {
if (expected.video) {
expectVideo = true;
}
if (expected.audio) {
expectAudio = true;
}
if (expected.screen) {
expectScreen = expected.screen;
}
}
is(
Boolean(ui.showCameraIndicator),
expectVideo,
"camera global indicator as expected"
);
is(
Boolean(ui.showMicrophoneIndicator),
expectAudio,
"microphone global indicator as expected"
);
is(
ui.showScreenSharingIndicator,
expectScreen,
"screen global indicator as expected"
);
for (let win of Services.wm.getEnumerator("navigator:browser")) {
let menu = win.document.getElementById("tabSharingMenu");
is(
!!menu && !menu.hidden,
!!expected,
"WebRTC menu should be " + expectedState
);
}
if (!expected) {
let win = Services.wm.getMostRecentWindow("Browser:WebRTCGlobalIndicator");
if (win) {
await new Promise(resolve => {
win.addEventListener("unload", function listener(e) {
if (e.target == win.document) {
win.removeEventListener("unload", listener);
executeSoon(resolve);
}
});
});
}
}
let indicator = Services.wm.getEnumerator("Browser:WebRTCGlobalIndicator");
let hasWindow = indicator.hasMoreElements();
is(hasWindow, !!expected, "popup " + msg);
if (hasWindow) {
let document = indicator.getNext().document;
let docElt = document.documentElement;
if (document.readyState != "complete") {
info("Waiting for the sharing indicator's document to load");
await new Promise(resolve => {
document.addEventListener(
"readystatechange",
function onReadyStateChange() {
if (document.readyState != "complete") {
return;
}
document.removeEventListener(
"readystatechange",
onReadyStateChange
);
executeSoon(resolve);
}
);
});
}
if (expected.screen && expected.screen.startsWith("Window")) {
// These tests were originally written to express window sharing by
// having expected.screen start with "Window". This meant that the
// legacy indicator is expected to have the "sharingscreen" attribute
// set to true when sharing a window.
//
// The new indicator, however, differentiates between screen, window
// and browser window sharing. If we're using the new indicator, we
// update the expectations accordingly. This can be removed once we
// are able to remove the tests for the legacy indicator.
expected.screen = null;
expected.window = true;
}
if (!SHOW_GLOBAL_MUTE_TOGGLES) {
expected.video = false;
expected.audio = false;
let visible = docElt.getAttribute("visible") == "true";
if (!expected.screen && !expected.window && !expected.browserwindow) {
ok(!visible, "Indicator should not be visible in this configuation.");
} else {
ok(visible, "Indicator should be visible.");
}
}
for (let item of ["video", "audio", "screen", "window", "browserwindow"]) {
let expectedValue;
expectedValue = expected && expected[item] ? "true" : null;
is(
docElt.getAttribute("sharing" + item),
expectedValue,
item + " global indicator attribute as expected"
);
}
ok(!indicator.hasMoreElements(), "only one global indicator window");
}
}
function promiseNotificationShown(notification) {
let win = notification.browser.ownerGlobal;
if (win.PopupNotifications.panel.state == "open") {
return Promise.resolve();
}
let panelPromise = BrowserTestUtils.waitForPopupEvent(
win.PopupNotifications.panel,
"shown"
);
notification.reshow();
return panelPromise;
}
function ignoreEvent(aSubject, aTopic, aData) {
// With e10s disabled, our content script receives notifications for the
// preview displayed in our screen sharing permission prompt; ignore them.
const kBrowserURL = AppConstants.BROWSER_CHROME_URL;
const nsIPropertyBag = Ci.nsIPropertyBag;
if (
aTopic == "recording-device-events" &&
aSubject.QueryInterface(nsIPropertyBag).getProperty("requestURL") ==
kBrowserURL
) {
return true;
}
if (aTopic == "recording-window-ended") {
let win = Services.wm.getOuterWindowWithId(aData).top;
if (win.document.documentURI == kBrowserURL) {
return true;
}
}
return false;
}
function expectObserverCalledInProcess(aTopic, aCount = 1) {
let promises = [];
for (let count = aCount; count > 0; count--) {
promises.push(TestUtils.topicObserved(aTopic, ignoreEvent));
}
return promises;
}
function expectObserverCalled(
aTopic,
aCount = 1,
browser = gBrowser.selectedBrowser
) {
if (!gMultiProcessBrowser) {
return expectObserverCalledInProcess(aTopic, aCount);
}
let browsingContext = Element.isInstance(browser)
? browser.browsingContext
: browser;
return BrowserTestUtils.contentTopicObserved(browsingContext, aTopic, aCount);
}
// This is a special version of expectObserverCalled that should only
// be used when expecting a notification upon closing a window. It uses
// the per-process message manager instead of actors to send the
// notifications.
function expectObserverCalledOnClose(
aTopic,
aCount = 1,
browser = gBrowser.selectedBrowser
) {
if (!gMultiProcessBrowser) {
return expectObserverCalledInProcess(aTopic, aCount);
}
let browsingContext = Element.isInstance(browser)
? browser.browsingContext
: browser;
return new Promise(resolve => {
BrowserTestUtils.sendAsyncMessage(
browsingContext,
"BrowserTestUtils:ObserveTopic",
{
topic: aTopic,
count: 1,
filterFunctionSource: ((subject, topic) => {
Services.cpmm.sendAsyncMessage("WebRTCTest:ObserverCalled", {
topic,
});
return true;
}).toSource(),
}
);
function observerCalled(message) {
if (message.data.topic == aTopic) {
Services.ppmm.removeMessageListener(
"WebRTCTest:ObserverCalled",
observerCalled
);
resolve();
}
}
Services.ppmm.addMessageListener(
"WebRTCTest:ObserverCalled",
observerCalled
);
});
}
function promiseMessage(
aMessage,
aAction,
aCount = 1,
browser = gBrowser.selectedBrowser
) {
let startTime = performance.now();
let promise = ContentTask.spawn(
browser,
[aMessage, aCount],
async function ([expectedMessage, expectedCount]) {
return new Promise(resolve => {
function listenForMessage({ data }) {
if (
(!expectedMessage || data == expectedMessage) &&
--expectedCount == 0
) {
content.removeEventListener("message", listenForMessage);
resolve(data);
}
}
content.addEventListener("message", listenForMessage);
});
}
);
if (aAction) {
aAction();
}
return promise.then(data => {
ChromeUtils.addProfilerMarker(
"promiseMessage",
{ startTime, category: "Test" },
data
);
return data;
});
}
function promisePopupNotificationShown(aName, aAction, aWindow = window) {
let startTime = performance.now();
return new Promise(resolve => {
aWindow.PopupNotifications.panel.addEventListener(
"popupshown",
function () {
ok(
!!aWindow.PopupNotifications.getNotification(aName),
aName + " notification shown"
);
ok(aWindow.PopupNotifications.isPanelOpen, "notification panel open");
ok(
!!aWindow.PopupNotifications.panel.firstElementChild,
"notification panel populated"
);
executeSoon(() => {
ChromeUtils.addProfilerMarker(
"promisePopupNotificationShown",
{ startTime, category: "Test" },
aName
);
resolve();
});
},
{ once: true }
);
if (aAction) {
aAction();
}
});
}
async function promisePopupNotification(aName) {
return TestUtils.waitForCondition(
() => PopupNotifications.getNotification(aName),
aName + " notification appeared"
);
}
async function promiseNoPopupNotification(aName) {
return TestUtils.waitForCondition(
() => !PopupNotifications.getNotification(aName),
aName + " notification removed"
);
}
const kActionAlways = 1;
const kActionDeny = 2;
const kActionNever = 3;
async function activateSecondaryAction(aAction) {
let notification = PopupNotifications.panel.firstElementChild;
switch (aAction) {
case kActionNever:
if (notification.notification.secondaryActions.length > 1) {
// "Always Block" is the first (and only) item in the menupopup.
await Promise.all([
BrowserTestUtils.waitForEvent(notification.menupopup, "popupshown"),
notification.menubutton.click(),
]);
notification.menupopup.querySelector("menuitem").click();
return;
}
if (!notification.checkbox.checked) {
notification.checkbox.click();
}
// fallthrough
case kActionDeny:
notification.secondaryButton.click();
break;
case kActionAlways:
if (!notification.checkbox.checked) {
notification.checkbox.click();
}
notification.button.click();
break;
}
}
async function getMediaCaptureState() {
let startTime = performance.now();
function gatherBrowsingContexts(aBrowsingContext) {
let list = [aBrowsingContext];
let children = aBrowsingContext.children;
for (let child of children) {
list.push(...gatherBrowsingContexts(child));
}
return list;
}
function combine(x, y) {
if (
x == Ci.nsIMediaManagerService.STATE_CAPTURE_ENABLED ||
y == Ci.nsIMediaManagerService.STATE_CAPTURE_ENABLED
) {
return Ci.nsIMediaManagerService.STATE_CAPTURE_ENABLED;
}
if (
x == Ci.nsIMediaManagerService.STATE_CAPTURE_DISABLED ||
y == Ci.nsIMediaManagerService.STATE_CAPTURE_DISABLED
) {
return Ci.nsIMediaManagerService.STATE_CAPTURE_DISABLED;
}
return Ci.nsIMediaManagerService.STATE_NOCAPTURE;
}
let video = Ci.nsIMediaManagerService.STATE_NOCAPTURE;
let audio = Ci.nsIMediaManagerService.STATE_NOCAPTURE;
let screen = Ci.nsIMediaManagerService.STATE_NOCAPTURE;
let window = Ci.nsIMediaManagerService.STATE_NOCAPTURE;
let browser = Ci.nsIMediaManagerService.STATE_NOCAPTURE;
for (let bc of gatherBrowsingContexts(
gBrowser.selectedBrowser.browsingContext
)) {
let state = await SpecialPowers.spawn(bc, [], async function () {
let mediaManagerService = Cc[
"@mozilla.org/mediaManagerService;1"
].getService(Ci.nsIMediaManagerService);
let hasCamera = {};
let hasMicrophone = {};
let hasScreenShare = {};
let hasWindowShare = {};
let hasBrowserShare = {};
let devices = {};
mediaManagerService.mediaCaptureWindowState(
content,
hasCamera,
hasMicrophone,
hasScreenShare,
hasWindowShare,
hasBrowserShare,
devices,
false
);
return {
video: hasCamera.value,
audio: hasMicrophone.value,
screen: hasScreenShare.value,
window: hasWindowShare.value,
browser: hasBrowserShare.value,
};
});
video = combine(state.video, video);
audio = combine(state.audio, audio);
screen = combine(state.screen, screen);
window = combine(state.window, window);
browser = combine(state.browser, browser);
}
let result = {};
if (video != Ci.nsIMediaManagerService.STATE_NOCAPTURE) {
result.video = true;
}
if (audio != Ci.nsIMediaManagerService.STATE_NOCAPTURE) {
result.audio = true;
}
if (screen != Ci.nsIMediaManagerService.STATE_NOCAPTURE) {
result.screen = "Screen";
} else if (window != Ci.nsIMediaManagerService.STATE_NOCAPTURE) {
result.window = true;
} else if (browser != Ci.nsIMediaManagerService.STATE_NOCAPTURE) {
result.browserwindow = true;
}
ChromeUtils.addProfilerMarker("getMediaCaptureState", {
startTime,
category: "Test",
});
return result;
}
async function stopSharing(
aType = "camera",
aShouldKeepSharing = false,
aFrameBC,
aWindow = window
) {
let promiseRecordingEvent = expectObserverCalled(
"recording-device-events",
1,
aFrameBC
);
let observerPromise1 = expectObserverCalled(
"getUserMedia:revoke",
1,
aFrameBC
);
// If we are stopping screen sharing and expect to still have another stream,
// "recording-window-ended" won't be fired.
let observerPromise2 = null;
if (!aShouldKeepSharing) {
observerPromise2 = expectObserverCalled(
"recording-window-ended",
1,
aFrameBC
);
}
await revokePermission(aType, aShouldKeepSharing, aFrameBC, aWindow);
await promiseRecordingEvent;
await observerPromise1;
await observerPromise2;
if (!aShouldKeepSharing) {
await checkNotSharing();
}
}
async function revokePermission(
aType = "camera",
aShouldKeepSharing = false,
aFrameBC,
aWindow = window
) {
aWindow.gPermissionPanel._identityPermissionBox.click();
let popup = aWindow.gPermissionPanel._permissionPopup;
// If the popup gets hidden before being shown, by stray focus/activate
// events, don't bother failing the test. It's enough to know that we
// started showing the popup.
let hiddenEvent = BrowserTestUtils.waitForEvent(popup, "popuphidden");
let shownEvent = BrowserTestUtils.waitForEvent(popup, "popupshown");
await Promise.race([hiddenEvent, shownEvent]);
let doc = aWindow.document;
let permissions = doc.getElementById("permission-popup-permission-list");
let cancelButton = permissions.querySelector(
".permission-popup-permission-icon." +
aType +
"-icon ~ " +
".permission-popup-permission-remove-button"
);
cancelButton.click();
popup.hidePopup();
if (!aShouldKeepSharing) {
await checkNotSharing();
}
}
function getBrowsingContextForFrame(aBrowsingContext, aFrameId) {
if (!aFrameId) {
return aBrowsingContext;
}
return SpecialPowers.spawn(aBrowsingContext, [aFrameId], frameId => {
return content.document.getElementById(frameId).browsingContext;
});
}
async function getBrowsingContextsAndFrameIdsForSubFrames(
aBrowsingContext,
aSubFrames
) {
let pendingBrowserSubFrames = [
{ bc: aBrowsingContext, subFrames: aSubFrames },
];
let browsingContextsAndFrames = [];
while (pendingBrowserSubFrames.length) {
let { bc, subFrames } = pendingBrowserSubFrames.shift();
for (let id of Object.keys(subFrames)) {
let subBc = await getBrowsingContextForFrame(bc, id);
if (subFrames[id].children) {
pendingBrowserSubFrames.push({
bc: subBc,
subFrames: subFrames[id].children,
});
}
if (subFrames[id].noTest) {
continue;
}
let observeBC = subFrames[id].observe ? subBc : undefined;
browsingContextsAndFrames.push({ bc: subBc, id, observeBC });
}
}
return browsingContextsAndFrames;
}
async function promiseRequestDevice(
aRequestAudio,
aRequestVideo,
aFrameId,
aType,
aBrowsingContext,
aBadDevice = false
) {
info("requesting devices");
let bc =
aBrowsingContext ??
(await getBrowsingContextForFrame(gBrowser.selectedBrowser, aFrameId));
return SpecialPowers.spawn(
bc,
[{ aRequestAudio, aRequestVideo, aType, aBadDevice }],
async function (args) {
let global = content.wrappedJSObject;
global.requestDevice(
args.aRequestAudio,
args.aRequestVideo,
args.aType,
args.aBadDevice
);
}
);
}
async function promiseRequestAudioOutput(options) {
info("requesting audio output");
const bc = gBrowser.selectedBrowser;
return SpecialPowers.spawn(bc, [options], async function (opts) {
const global = content.wrappedJSObject;
global.requestAudioOutput(Cu.cloneInto(opts, content));
});
}
async function stopTracks(
aKind,
aAlreadyStopped,
aLastTracks,
aFrameId,
aBrowsingContext,
aBrowsingContextToObserve
) {
// If the observers are listening to other frames, listen for a notification
// on the right subframe.
let frameBC =
aBrowsingContext ??
(await getBrowsingContextForFrame(
gBrowser.selectedBrowser.browsingContext,
aFrameId
));
let observerPromises = [];
if (!aAlreadyStopped) {
observerPromises.push(
expectObserverCalled(
"recording-device-events",
1,
aBrowsingContextToObserve
)
);
}
if (aLastTracks) {
observerPromises.push(
expectObserverCalled(
"recording-window-ended",
1,
aBrowsingContextToObserve
)
);
}
info(`Stopping all ${aKind} tracks`);
await SpecialPowers.spawn(frameBC, [aKind], async function (kind) {
content.wrappedJSObject.stopTracks(kind);
});
await Promise.all(observerPromises);
}
async function closeStream(
aAlreadyClosed,
aFrameId,
aDontFlushObserverVerification,
aBrowsingContext,
aBrowsingContextToObserve
) {
// Check that spurious notifications that occur while closing the
// stream are handled separately. Tests that use skipObserverVerification
// should pass true for aDontFlushObserverVerification.
if (!aDontFlushObserverVerification) {
await disableObserverVerification();
await enableObserverVerification();
}
// If the observers are listening to other frames, listen for a notification
// on the right subframe.
let frameBC =
aBrowsingContext ??
(await getBrowsingContextForFrame(
gBrowser.selectedBrowser.browsingContext,
aFrameId
));
let observerPromises = [];
if (!aAlreadyClosed) {
observerPromises.push(
expectObserverCalled(
"recording-device-events",
1,
aBrowsingContextToObserve
)
);
observerPromises.push(
expectObserverCalled(
"recording-window-ended",
1,
aBrowsingContextToObserve
)
);
}
info("closing the stream");
await SpecialPowers.spawn(frameBC, [], async function () {
content.wrappedJSObject.closeStream();
});
await Promise.all(observerPromises);
await assertWebRTCIndicatorStatus(null);
}
async function reloadAsUser() {
info("reloading as a user");
const reloadButton = document.getElementById("reload-button");
await TestUtils.waitForCondition(() => !reloadButton.disabled);
// Disable observers as the page is being reloaded which can destroy
// the actors listening to the notifications.
await disableObserverVerification();
let loadedPromise = BrowserTestUtils.browserLoaded(gBrowser.selectedBrowser);
reloadButton.click();
await loadedPromise;
await enableObserverVerification();
}
async function reloadFromContent() {
info("reloading from content");
// Disable observers as the page is being reloaded which can destroy
// the actors listening to the notifications.
await disableObserverVerification();
let loadedPromise = BrowserTestUtils.browserLoaded(gBrowser.selectedBrowser);
await ContentTask.spawn(gBrowser.selectedBrowser, null, () =>
content.location.reload()
);
await loadedPromise;
await enableObserverVerification();
}
async function reloadAndAssertClosedStreams() {
await reloadFromContent();
await checkNotSharing();
}
/**
* @param {("microphone"|"camera"|"screen")[]} aExpectedTypes
* @param {Window} [aWindow]
*/
function checkDeviceSelectors(aExpectedTypes, aWindow = window) {
for (const type of aExpectedTypes) {
if (!["microphone", "camera", "screen", "speaker"].includes(type)) {
throw new Error(`Bad device type name ${type}`);
}
}
let document = aWindow.document;
let expectedDescribedBy = "webRTC-shareDevices-notification-description";
for (let type of ["Camera", "Microphone", "Speaker"]) {
let selector = document.getElementById(`webRTC-select${type}`);
if (!aExpectedTypes.includes(type.toLowerCase())) {
ok(selector.hidden, `${type} selector hidden`);
continue;
}
ok(!selector.hidden, `${type} selector visible`);
let tagName = type == "Speaker" ? "richlistbox" : "menulist";
let selectorList = document.getElementById(
`webRTC-select${type}-${tagName}`
);
let label = document.getElementById(
`webRTC-select${type}-single-device-label`
);
// If there's only 1 device listed, then we should show the label
// instead of the menulist.
if (selectorList.itemCount == 1) {
ok(selectorList.hidden, `${type} selector list should be hidden.`);
ok(!label.hidden, `${type} selector label should not be hidden.`);
let itemLabel =
tagName == "richlistbox"
? selectorList.selectedItem.firstElementChild.getAttribute("value")
: selectorList.selectedItem.getAttribute("label");
is(
label.value,
itemLabel,
`${type} label should be showing the lone device label.`
);
expectedDescribedBy += ` webRTC-select${type}-icon webRTC-select${type}-single-device-label`;
} else {
ok(!selectorList.hidden, `${type} selector list should not be hidden.`);
ok(label.hidden, `${type} selector label should be hidden.`);
}
}
let ariaDescribedby =
aWindow.PopupNotifications.panel.getAttribute("aria-describedby");
is(ariaDescribedby, expectedDescribedBy, "aria-describedby");
let screenSelector = document.getElementById("webRTC-selectWindowOrScreen");
if (aExpectedTypes.includes("screen")) {
ok(!screenSelector.hidden, "screen selector visible");
} else {
ok(screenSelector.hidden, "screen selector hidden");
}
}
/**
* Tests the siteIdentity icons, the permission panel and the global indicator
* UI state.
* @param {Object} aExpected - Expected state for the current tab.
* @param {window} [aWin] - Top level chrome window to test state of.
* @param {Object} [aExpectedGlobal] - Expected state for all tabs.
* @param {Object} [aExpectedPerm] - Expected permission states keyed by device
* type.
*/
async function checkSharingUI(
aExpected,
aWin = window,
aExpectedGlobal = null,
aExpectedPerm = null
) {
function isPaused(streamState) {
if (typeof streamState == "string") {
return streamState.includes("Paused");
}
return streamState == STATE_CAPTURE_DISABLED;
}
let doc = aWin.document;
// First check the icon above the control center (i) icon.
let permissionBox = doc.getElementById("identity-permission-box");
let webrtcSharingIcon = doc.getElementById("webrtc-sharing-icon");
ok(webrtcSharingIcon.hasAttribute("sharing"), "sharing attribute is set");
let sharing = webrtcSharingIcon.getAttribute("sharing");
if (aExpected.screen && !IsIndicatorDisabled) {
is(sharing, "screen", "showing screen icon in the identity block");
} else if (aExpected.video == STATE_CAPTURE_ENABLED && !IsIndicatorDisabled) {
is(sharing, "camera", "showing camera icon in the identity block");
} else if (aExpected.audio == STATE_CAPTURE_ENABLED && !IsIndicatorDisabled) {
is(sharing, "microphone", "showing mic icon in the identity block");
} else if (aExpected.video && !IsIndicatorDisabled) {
is(sharing, "camera", "showing camera icon in the identity block");
} else if (aExpected.audio && !IsIndicatorDisabled) {
is(sharing, "microphone", "showing mic icon in the identity block");
}
let allStreamsPaused = Object.values(aExpected).every(isPaused);
is(
webrtcSharingIcon.hasAttribute("paused"),
allStreamsPaused,
"sharing icon(s) should be in paused state when paused"
);
// Then check the sharing indicators inside the permission popup.
permissionBox.click();
let popup = aWin.gPermissionPanel._permissionPopup;
// If the popup gets hidden before being shown, by stray focus/activate
// events, don't bother failing the test. It's enough to know that we
// started showing the popup.
let hiddenEvent = BrowserTestUtils.waitForEvent(popup, "popuphidden");
let shownEvent = BrowserTestUtils.waitForEvent(popup, "popupshown");
await Promise.race([hiddenEvent, shownEvent]);
let permissions = doc.getElementById("permission-popup-permission-list");
for (let id of ["microphone", "camera", "screen"]) {
let convertId = idToConvert => {
if (idToConvert == "camera") {
return "video";
}
if (idToConvert == "microphone") {
return "audio";
}
return idToConvert;
};
let expected = aExpected[convertId(id)];
// Extract the expected permission for the device type.
// Defaults to temporary allow.
let { state, scope } = aExpectedPerm?.[convertId(id)] || {};
if (state == null) {
state = SitePermissions.ALLOW;
}
if (scope == null) {
scope = SitePermissions.SCOPE_TEMPORARY;
}
is(
!!aWin.gPermissionPanel._sharingState.webRTC[id],
!!expected,
"sharing state for " + id + " as expected"
);
let item = permissions.querySelectorAll(
".permission-popup-permission-item-" + id
);
let stateLabel = item?.[0]?.querySelector(
".permission-popup-permission-state-label"
);
let icon = permissions.querySelectorAll(
".permission-popup-permission-icon." + id + "-icon"
);
if (expected) {
is(item.length, 1, "should show " + id + " item in permission panel");
is(
stateLabel?.textContent,
SitePermissions.getCurrentStateLabel(state, id, scope),
"should show correct item label for " + id
);
is(icon.length, 1, "should show " + id + " icon in permission panel");
is(
icon[0].classList.contains("in-use"),
expected && !isPaused(expected),
"icon should have the in-use class, unless paused"
);
} else if (!icon.length && !item.length && !stateLabel) {
ok(true, "should not show " + id + " item in the permission panel");
ok(true, "should not show " + id + " icon in the permission panel");
ok(
true,
"should not show " + id + " state label in the permission panel"
);
} else {
// This will happen if there are persistent permissions set.
ok(
!icon[0].classList.contains("in-use"),
"if shown, the " + id + " icon should not have the in-use class"
);
is(item.length, 1, "should not show more than 1 " + id + " item");
is(icon.length, 1, "should not show more than 1 " + id + " icon");
}
}
aWin.gPermissionPanel._permissionPopup.hidePopup();
await TestUtils.waitForCondition(
() => permissionPopupHidden(aWin),
"identity popup should be hidden"
);
// Check the global indicators.
await assertWebRTCIndicatorStatus(aExpectedGlobal || aExpected);
}
async function checkNotSharing() {
Assert.deepEqual(
await getMediaCaptureState(),
{},
"expected nothing to be shared"
);
ok(
!document.getElementById("webrtc-sharing-icon").hasAttribute("sharing"),
"no sharing indicator on the control center icon"
);
await assertWebRTCIndicatorStatus(null);
}
async function checkNotSharingWithinGracePeriod() {
Assert.deepEqual(
await getMediaCaptureState(),
{},
"expected nothing to be shared"
);
ok(
document.getElementById("webrtc-sharing-icon").hasAttribute("sharing"),
"has sharing indicator on the control center icon"
);
ok(
document.getElementById("webrtc-sharing-icon").hasAttribute("paused"),
"sharing indicator is paused"
);
await assertWebRTCIndicatorStatus(null);
}
async function promiseReloadFrame(aFrameId, aBrowsingContext) {
let loadedPromise = BrowserTestUtils.browserLoaded(
gBrowser.selectedBrowser,
true,
() => {
return true;
}
);
let bc =
aBrowsingContext ??
(await getBrowsingContextForFrame(
gBrowser.selectedBrowser.browsingContext,
aFrameId
));
await SpecialPowers.spawn(bc, [], async function () {
content.location.reload();
});
return loadedPromise;
}
function promiseChangeLocationFrame(aFrameId, aNewLocation) {
return SpecialPowers.spawn(
gBrowser.selectedBrowser.browsingContext,
[{ aFrameId, aNewLocation }],
async function (args) {
let frame = content.wrappedJSObject.document.getElementById(
args.aFrameId
);
return new Promise(resolve => {
function listener() {
frame.removeEventListener("load", listener, true);
resolve();
}
frame.addEventListener("load", listener, true);
content.wrappedJSObject.document.getElementById(
args.aFrameId
).contentWindow.location = args.aNewLocation;
});
}
);
}
async function openNewTestTab(leaf = "get_user_media.html") {
let rootDir = getRootDirectory(gTestPath);
rootDir = rootDir.replace(
);
let absoluteURI = rootDir + leaf;
let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, absoluteURI);
return tab.linkedBrowser;
}
// Enabling observer verification adds listeners for all of the webrtc
// observer topics. If any notifications occur for those topics that
// were not explicitly requested, a failure will occur.
async function enableObserverVerification(browser = gBrowser.selectedBrowser) {
// Skip these checks in single process mode as it isn't worth implementing it.
if (!gMultiProcessBrowser) {
return;
}
gBrowserContextsToObserve = [browser.browsingContext];
// A list of subframe indicies to also add observers to. This only
// supports one nested level.
if (gObserveSubFrames) {
let bcsAndFrameIds = await getBrowsingContextsAndFrameIdsForSubFrames(
browser,
gObserveSubFrames
);
for (let { observeBC } of bcsAndFrameIds) {
if (observeBC) {
gBrowserContextsToObserve.push(observeBC);
}
}
}
for (let bc of gBrowserContextsToObserve) {
await BrowserTestUtils.startObservingTopics(bc, observerTopics);
}
}
async function disableObserverVerification() {
if (!gMultiProcessBrowser) {
return;
}
for (let bc of gBrowserContextsToObserve) {
await BrowserTestUtils.stopObservingTopics(bc, observerTopics).catch(
reason => {
ok(false, "Failed " + reason);
}
);
}
}
function permissionPopupHidden(win = window) {
let popup = win.gPermissionPanel._permissionPopup;
return !popup || popup.state == "closed";
}
async function runTests(tests, options = {}) {
let browser = await openNewTestTab(options.relativeURI);
is(
PopupNotifications._currentNotifications.length,
0,
"should start the test without any prior popup notification"
);
ok(
permissionPopupHidden(),
"should start the test with the permission panel hidden"
);
// Set prefs so that permissions prompts are shown and loopback devices
// are not used. To test the chrome we want prompts to be shown, and
// these tests are flakey when using loopback devices (though it would
// be desirable to make them work with loopback in future). See bug 1643711.
let prefs = [
[PREF_PERMISSION_FAKE, true],
[PREF_AUDIO_LOOPBACK, ""],
[PREF_VIDEO_LOOPBACK, ""],
[PREF_FAKE_STREAMS, true],
[PREF_FOCUS_SOURCE, false],
];
await SpecialPowers.pushPrefEnv({ set: prefs });
// When the frames are in different processes, add observers to each frame,
// to ensure that the notifications don't get sent in the wrong process.
gObserveSubFrames = SpecialPowers.useRemoteSubframes ? options.subFrames : {};
for (let testCase of tests) {
let startTime = performance.now();
info(testCase.desc);
if (
!testCase.skipObserverVerification &&
!options.skipObserverVerification
) {
await enableObserverVerification();
}
await testCase.run(browser, options.subFrames);
if (
!testCase.skipObserverVerification &&
!options.skipObserverVerification
) {
await disableObserverVerification();
}
if (options.cleanup) {
await options.cleanup();
}
ChromeUtils.addProfilerMarker(
"browser-test",
{ startTime, category: "Test" },
testCase.desc
);
}
// Some tests destroy the original tab and leave a new one in its place.
BrowserTestUtils.removeTab(gBrowser.selectedTab);
}
/**
* Given a browser from a tab in this window, chooses to share
* some combination of camera, mic or screen.
*
* @param {<xul:browser} browser - The browser to share devices with.
* @param {boolean} camera - True to share a camera device.
* @param {boolean} mic - True to share a microphone device.
* @param {Number} [screenOrWin] - One of either SHARE_WINDOW or SHARE_SCREEN
* to share a window or screen. Defaults to neither.
* @param {boolean} remember - True to persist the permission to the
* SitePermissions database as SitePermissions.SCOPE_PERSISTENT. Note that
* callers are responsible for clearing this persistent permission.
* @return {Promise}
* @resolves {undefined} - Once the sharing is complete.
*/
async function shareDevices(
browser,
camera,
mic,
screenOrWin = 0,
remember = false
) {
if (camera || mic) {
let promise = promisePopupNotificationShown(
"webRTC-shareDevices",
null,
window
);
await promiseRequestDevice(mic, camera, null, null, browser);
await promise;
const expectedDeviceSelectorTypes = [
camera && "camera",
mic && "microphone",
].filter(x => x);
checkDeviceSelectors(expectedDeviceSelectorTypes);
let observerPromise1 = expectObserverCalled("getUserMedia:response:allow");
let observerPromise2 = expectObserverCalled("recording-device-events");
let rememberCheck = PopupNotifications.panel.querySelector(
".popup-notification-checkbox"
);
rememberCheck.checked = remember;
promise = promiseMessage("ok", () => {
PopupNotifications.panel.firstElementChild.button.click();
});
await observerPromise1;
await observerPromise2;
await promise;
}
if (screenOrWin) {
let promise = promisePopupNotificationShown(
"webRTC-shareDevices",
null,
window
);
await promiseRequestDevice(false, true, null, "screen", browser);
await promise;
checkDeviceSelectors(["screen"], window);
let document = window.document;
let menulist = document.getElementById("webRTC-selectWindow-menulist");
let displayMediaSource;
if (screenOrWin == SHARE_SCREEN) {
displayMediaSource = "screen";
} else if (screenOrWin == SHARE_WINDOW) {
displayMediaSource = "window";
} else {
throw new Error("Got an invalid argument to shareDevices.");
}
let menuitem = null;
for (let i = 0; i < menulist.itemCount; ++i) {
let current = menulist.getItemAtIndex(i);
if (current.mediaSource == displayMediaSource) {
menuitem = current;
break;
}
}
Assert.ok(menuitem, "Should have found an appropriate display menuitem");
menuitem.doCommand();
let notification = window.PopupNotifications.panel.firstElementChild;
let observerPromise1 = expectObserverCalled("getUserMedia:response:allow");
let observerPromise2 = expectObserverCalled("recording-device-events");
await promiseMessage(
"ok",
() => {
notification.button.click();
},
1,
browser
);
await observerPromise1;
await observerPromise2;
}
}