summaryrefslogtreecommitdiffstats
path: root/dom/media/doctor/test/browser
diff options
context:
space:
mode:
Diffstat (limited to 'dom/media/doctor/test/browser')
-rw-r--r--dom/media/doctor/test/browser/browser.ini7
-rw-r--r--dom/media/doctor/test/browser/browser_decoderDoctor.js356
-rw-r--r--dom/media/doctor/test/browser/browser_doctor_notification.js265
3 files changed, 628 insertions, 0 deletions
diff --git a/dom/media/doctor/test/browser/browser.ini b/dom/media/doctor/test/browser/browser.ini
new file mode 100644
index 0000000000..3dda42c4e5
--- /dev/null
+++ b/dom/media/doctor/test/browser/browser.ini
@@ -0,0 +1,7 @@
+[DEFAULT]
+subsuite = media-bc
+tags = decoderdoctor
+support-files =
+
+[browser_decoderDoctor.js]
+[browser_doctor_notification.js]
diff --git a/dom/media/doctor/test/browser/browser_decoderDoctor.js b/dom/media/doctor/test/browser/browser_decoderDoctor.js
new file mode 100644
index 0000000000..c502131fec
--- /dev/null
+++ b/dom/media/doctor/test/browser/browser_decoderDoctor.js
@@ -0,0 +1,356 @@
+"use strict";
+
+// 'data' contains the notification data object:
+// - data.type must be provided.
+// - data.isSolved and data.decoderDoctorReportId will be added if not provided
+// (false and "testReportId" resp.)
+// - Other fields (e.g.: data.formats) may be provided as needed.
+// 'notificationMessage': Expected message in the notification bar.
+// Falsy if nothing is expected after the notification is sent, in which case
+// we won't have further checks, so the following parameters are not needed.
+// 'label': Expected button label. Falsy if no button is expected, in which case
+// we won't have further checks, so the following parameters are not needed.
+// 'accessKey': Expected access key for the button.
+// 'tabChecker': function(openedTab) called with the opened tab that resulted
+// from clicking the button.
+async function test_decoder_doctor_notification(
+ data,
+ notificationMessage,
+ label,
+ accessKey,
+ isLink,
+ tabChecker
+) {
+ const TEST_URL = "https://example.org";
+ // A helper closure to test notifications in same or different origins.
+ // 'test_cross_origin' is used to determine if the observers used in the test
+ // are notified in the same frame (when false) or in a cross origin iframe
+ // (when true).
+ async function create_tab_and_test(test_cross_origin) {
+ await BrowserTestUtils.withNewTab(
+ { gBrowser, url: TEST_URL },
+ async function (browser) {
+ let awaitNotificationBar;
+ if (notificationMessage) {
+ awaitNotificationBar = BrowserTestUtils.waitForNotificationBar(
+ gBrowser,
+ browser,
+ "decoder-doctor-notification"
+ );
+ }
+
+ await SpecialPowers.spawn(
+ browser,
+ [data, test_cross_origin],
+ /* eslint-disable-next-line no-shadow */
+ async function (data, test_cross_origin) {
+ if (!test_cross_origin) {
+ // Notify in the same origin.
+ Services.obs.notifyObservers(
+ content.window,
+ "decoder-doctor-notification",
+ JSON.stringify(data)
+ );
+ return;
+ // Done notifying in the same origin.
+ }
+
+ // Notify in a different origin.
+ const CROSS_ORIGIN_URL = "https://example.com";
+ let frame = content.document.createElement("iframe");
+ frame.src = CROSS_ORIGIN_URL;
+ await new Promise(resolve => {
+ frame.addEventListener("load", () => {
+ resolve();
+ });
+ content.document.body.appendChild(frame);
+ });
+
+ await content.SpecialPowers.spawn(
+ frame,
+ [data],
+ async function (
+ /* eslint-disable-next-line no-shadow */
+ data
+ ) {
+ Services.obs.notifyObservers(
+ content.window,
+ "decoder-doctor-notification",
+ JSON.stringify(data)
+ );
+ }
+ );
+ // Done notifying in a different origin.
+ }
+ );
+
+ if (!notificationMessage) {
+ ok(
+ true,
+ "Tested notifying observers with a nonsensical message, no effects expected"
+ );
+ return;
+ }
+
+ let notification;
+ try {
+ notification = await awaitNotificationBar;
+ } catch (ex) {
+ ok(false, ex);
+ return;
+ }
+ ok(notification, "Got decoder-doctor-notification notification");
+ if (label?.l10nId) {
+ // Without the following statement, the
+ // test_cannot_initialize_pulseaudio
+ // will permanently fail on Linux.
+ if (label.l10nId === "moz-support-link-text") {
+ MozXULElement.insertFTLIfNeeded(
+ "browser/components/mozSupportLink.ftl"
+ );
+ }
+ label = await document.l10n.formatValue(label.l10nId);
+ }
+ if (isLink) {
+ let link = notification.messageText.querySelector("a");
+ if (link) {
+ // Seems to be a Windows specific quirk, but without this
+ // mutation observer the notification.messageText.textContent
+ // will not be updated. This will cause consistent failures
+ // on Windows.
+ await BrowserTestUtils.waitForMutationCondition(
+ link,
+ { childList: true },
+ () => link.textContent.trim()
+ );
+ }
+ }
+ is(
+ notification.messageText.textContent,
+ notificationMessage + (isLink && label ? ` ${label}` : ""),
+ "notification message should match expectation"
+ );
+
+ let button = notification.buttonContainer.querySelector("button");
+ let link = notification.messageText.querySelector("a");
+ if (!label) {
+ ok(!button, "There should not be a button");
+ ok(!link, "There should not be a link");
+ return;
+ }
+
+ if (isLink) {
+ ok(!button, "There should not be a button");
+ is(link.innerText, label, `notification link should be '${label}'`);
+ ok(
+ !link.hasAttribute("accesskey"),
+ "notification link should not have accesskey"
+ );
+ } else {
+ ok(!link, "There should not be a link");
+ is(
+ button.getAttribute("label"),
+ label,
+ `notification button should be '${label}'`
+ );
+ is(
+ button.getAttribute("accesskey"),
+ accessKey,
+ "notification button should have accesskey"
+ );
+ }
+
+ if (!tabChecker) {
+ ok(false, "Test implementation error: Missing tabChecker");
+ return;
+ }
+ let awaitNewTab = BrowserTestUtils.waitForNewTab(gBrowser);
+ if (button) {
+ button.click();
+ } else {
+ link.click();
+ }
+ let openedTab = await awaitNewTab;
+ tabChecker(openedTab);
+ BrowserTestUtils.removeTab(openedTab);
+ }
+ );
+ }
+
+ if (typeof data.type === "undefined") {
+ ok(false, "Test implementation error: data.type must be provided");
+ return;
+ }
+ data.isSolved = data.isSolved || false;
+ if (typeof data.decoderDoctorReportId === "undefined") {
+ data.decoderDoctorReportId = "testReportId";
+ }
+
+ // Test same origin.
+ await create_tab_and_test(false);
+ // Test cross origin.
+ await create_tab_and_test(true);
+}
+
+function tab_checker_for_sumo(expectedPath) {
+ return function (openedTab) {
+ let baseURL = Services.urlFormatter.formatURLPref("app.support.baseURL");
+ let url = baseURL + expectedPath;
+ is(
+ openedTab.linkedBrowser.currentURI.spec,
+ url,
+ `Expected '${url}' in new tab`
+ );
+ };
+}
+
+function tab_checker_for_webcompat(expectedParams) {
+ return function (openedTab) {
+ let urlString = openedTab.linkedBrowser.currentURI.spec;
+ let endpoint = Services.prefs.getStringPref(
+ "media.decoder-doctor.new-issue-endpoint",
+ ""
+ );
+ ok(
+ urlString.startsWith(endpoint),
+ `Expected URL starting with '${endpoint}', got '${urlString}'`
+ );
+ let params = new URL(urlString).searchParams;
+ for (let k in expectedParams) {
+ if (!params.has(k)) {
+ ok(false, `Expected ${k} in webcompat URL`);
+ } else {
+ is(
+ params.get(k),
+ expectedParams[k],
+ `Expected ${k}='${expectedParams[k]}' in webcompat URL`
+ );
+ }
+ }
+ };
+}
+
+add_task(async function test_platform_decoder_not_found() {
+ let message = "";
+ let decoderDoctorReportId = "";
+ let isLinux = AppConstants.platform == "linux";
+ if (isLinux) {
+ message = gNavigatorBundle.getString("decoder.noCodecsLinux.message");
+ decoderDoctorReportId = "MediaPlatformDecoderNotFound";
+ } else if (AppConstants.platform == "win") {
+ message = gNavigatorBundle.getString("decoder.noHWAcceleration.message");
+ decoderDoctorReportId = "MediaWMFNeeded";
+ }
+
+ await test_decoder_doctor_notification(
+ {
+ type: "platform-decoder-not-found",
+ decoderDoctorReportId,
+ formats: "testFormat",
+ },
+ message,
+ isLinux ? "" : { l10nId: "moz-support-link-text" },
+ isLinux ? "" : gNavigatorBundle.getString("decoder.noCodecs.accesskey"),
+ true,
+ tab_checker_for_sumo("fix-video-audio-problems-firefox-windows")
+ );
+});
+
+add_task(async function test_cannot_initialize_pulseaudio() {
+ let message = "";
+ // This is only sent on Linux.
+ if (AppConstants.platform == "linux") {
+ message = gNavigatorBundle.getString("decoder.noPulseAudio.message");
+ }
+
+ await test_decoder_doctor_notification(
+ { type: "cannot-initialize-pulseaudio", formats: "testFormat" },
+ message,
+ { l10nId: "moz-support-link-text" },
+ gNavigatorBundle.getString("decoder.noCodecs.accesskey"),
+ true,
+ tab_checker_for_sumo("fix-common-audio-and-video-issues")
+ );
+});
+
+add_task(async function test_unsupported_libavcodec() {
+ let message = "";
+ // This is only sent on Linux.
+ if (AppConstants.platform == "linux") {
+ message = gNavigatorBundle.getString(
+ "decoder.unsupportedLibavcodec.message"
+ );
+ }
+
+ await test_decoder_doctor_notification(
+ { type: "unsupported-libavcodec", formats: "testFormat" },
+ message
+ );
+});
+
+add_task(async function test_decode_error() {
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ [
+ "media.decoder-doctor.new-issue-endpoint",
+ "http://example.com/webcompat",
+ ],
+ ["browser.fixup.fallback-to-https", false],
+ ],
+ });
+ let message = gNavigatorBundle.getString("decoder.decodeError.message");
+ await test_decoder_doctor_notification(
+ {
+ type: "decode-error",
+ decodeIssue: "DecodeIssue",
+ docURL: "DocURL",
+ resourceURL: "ResURL",
+ },
+ message,
+ gNavigatorBundle.getString("decoder.decodeError.button"),
+ gNavigatorBundle.getString("decoder.decodeError.accesskey"),
+ false,
+ tab_checker_for_webcompat({
+ url: "DocURL",
+ label: "type-media",
+ problem_type: "video_bug",
+ details: JSON.stringify({
+ "Technical Information:": "DecodeIssue",
+ "Resource:": "ResURL",
+ }),
+ })
+ );
+});
+
+add_task(async function test_decode_warning() {
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ [
+ "media.decoder-doctor.new-issue-endpoint",
+ "http://example.com/webcompat",
+ ],
+ ],
+ });
+ let message = gNavigatorBundle.getString("decoder.decodeWarning.message");
+ await test_decoder_doctor_notification(
+ {
+ type: "decode-warning",
+ decodeIssue: "DecodeIssue",
+ docURL: "DocURL",
+ resourceURL: "ResURL",
+ },
+ message,
+ gNavigatorBundle.getString("decoder.decodeError.button"),
+ gNavigatorBundle.getString("decoder.decodeError.accesskey"),
+ false,
+ tab_checker_for_webcompat({
+ url: "DocURL",
+ label: "type-media",
+ problem_type: "video_bug",
+ details: JSON.stringify({
+ "Technical Information:": "DecodeIssue",
+ "Resource:": "ResURL",
+ }),
+ })
+ );
+});
diff --git a/dom/media/doctor/test/browser/browser_doctor_notification.js b/dom/media/doctor/test/browser/browser_doctor_notification.js
new file mode 100644
index 0000000000..5789622e23
--- /dev/null
+++ b/dom/media/doctor/test/browser/browser_doctor_notification.js
@@ -0,0 +1,265 @@
+/**
+ * This test is used to test whether the decoder doctor would report the error
+ * on the notification banner (checking that by observing message) or on the web
+ * console (checking that by listening to the test event).
+ * Error should be reported after calling `DecoderDoctorDiagnostics::StoreXXX`
+ * methods.
+ * - StoreFormatDiagnostics() [for checking if type is supported]
+ * - StoreDecodeError() [when decode error occurs]
+ * - StoreEvent() [for reporting audio sink error]
+ */
+
+// Only types being listed here would be allowed to display on a
+// notification banner. Otherwise, the error would only be showed on the
+// web console.
+var gAllowedNotificationTypes =
+ "MediaWMFNeeded,MediaFFMpegNotFound,MediaUnsupportedLibavcodec,MediaDecodeError,MediaCannotInitializePulseAudio,";
+
+// Used to check if the mime type in the notification is equal to what we set
+// before. This mime type doesn't reflect the real world siutation, i.e. not
+// every error listed in this test would happen on this type. An example, ffmpeg
+// not found would only happen on H264/AAC media.
+const gMimeType = "video/mp4";
+
+add_task(async function setupTestingPref() {
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["media.decoder-doctor.testing", true],
+ ["media.decoder-doctor.verbose", true],
+ ["media.decoder-doctor.notifications-allowed", gAllowedNotificationTypes],
+ ],
+ });
+ // transfer types to lower cases in order to match with `DecoderDoctorReportType`
+ gAllowedNotificationTypes = gAllowedNotificationTypes.toLowerCase();
+});
+
+add_task(async function testWMFIsNeeded() {
+ const tab = await createTab("about:blank");
+ await setFormatDiagnosticsReportForMimeType(tab, {
+ type: "platform-decoder-not-found",
+ decoderDoctorReportId: "mediawmfneeded",
+ formats: gMimeType,
+ });
+ BrowserTestUtils.removeTab(tab);
+});
+
+add_task(async function testFFMpegNotFound() {
+ const tab = await createTab("about:blank");
+ await setFormatDiagnosticsReportForMimeType(tab, {
+ type: "platform-decoder-not-found",
+ decoderDoctorReportId: "mediaplatformdecodernotfound",
+ formats: gMimeType,
+ });
+ BrowserTestUtils.removeTab(tab);
+});
+
+add_task(async function testLibAVCodecUnsupported() {
+ const tab = await createTab("about:blank");
+ await setFormatDiagnosticsReportForMimeType(tab, {
+ type: "unsupported-libavcodec",
+ decoderDoctorReportId: "mediaunsupportedlibavcodec",
+ formats: gMimeType,
+ });
+ BrowserTestUtils.removeTab(tab);
+});
+
+add_task(async function testCanNotPlayNoDecoder() {
+ const tab = await createTab("about:blank");
+ await setFormatDiagnosticsReportForMimeType(tab, {
+ type: "cannot-play",
+ decoderDoctorReportId: "mediacannotplaynodecoders",
+ formats: gMimeType,
+ });
+ BrowserTestUtils.removeTab(tab);
+});
+
+add_task(async function testNoDecoder() {
+ const tab = await createTab("about:blank");
+ await setFormatDiagnosticsReportForMimeType(tab, {
+ type: "can-play-but-some-missing-decoders",
+ decoderDoctorReportId: "medianodecoders",
+ formats: gMimeType,
+ });
+ BrowserTestUtils.removeTab(tab);
+});
+
+const gErrorList = [
+ "NS_ERROR_DOM_MEDIA_ABORT_ERR",
+ "NS_ERROR_DOM_MEDIA_NOT_ALLOWED_ERR",
+ "NS_ERROR_DOM_MEDIA_NOT_SUPPORTED_ERR",
+ "NS_ERROR_DOM_MEDIA_DECODE_ERR",
+ "NS_ERROR_DOM_MEDIA_FATAL_ERR",
+ "NS_ERROR_DOM_MEDIA_METADATA_ERR",
+ "NS_ERROR_DOM_MEDIA_OVERFLOW_ERR",
+ "NS_ERROR_DOM_MEDIA_MEDIASINK_ERR",
+ "NS_ERROR_DOM_MEDIA_DEMUXER_ERR",
+ "NS_ERROR_DOM_MEDIA_CDM_ERR",
+ "NS_ERROR_DOM_MEDIA_CUBEB_INITIALIZATION_ERR",
+];
+
+add_task(async function testDecodeError() {
+ const type = "decode-error";
+ const decoderDoctorReportId = "mediadecodeerror";
+ for (let error of gErrorList) {
+ const tab = await createTab("about:blank");
+ info(`first to try if the error is not allowed to be reported`);
+ // No error is allowed to be reported in the notification banner.
+ await SpecialPowers.pushPrefEnv({
+ set: [["media.decoder-doctor.decode-errors-allowed", ""]],
+ });
+ await setDecodeError(tab, {
+ type,
+ decoderDoctorReportId,
+ error,
+ shouldReportNotification: false,
+ });
+
+ // If the notification type is `MediaDecodeError` and the error type is
+ // listed in the pref, then the error would be reported to the
+ // notification banner.
+ info(`Then to try if the error is allowed to be reported`);
+ await SpecialPowers.pushPrefEnv({
+ set: [["media.decoder-doctor.decode-errors-allowed", error]],
+ });
+ await setDecodeError(tab, {
+ type,
+ decoderDoctorReportId,
+ error,
+ shouldReportNotification: true,
+ });
+ BrowserTestUtils.removeTab(tab);
+ }
+});
+
+add_task(async function testAudioSinkFailedStartup() {
+ const tab = await createTab("about:blank");
+ await setAudioSinkFailedStartup(tab, {
+ type: "cannot-initialize-pulseaudio",
+ decoderDoctorReportId: "mediacannotinitializepulseaudio",
+ // This error comes with `*`, see `DecoderDoctorDiagnostics::StoreEvent`
+ formats: "*",
+ });
+ BrowserTestUtils.removeTab(tab);
+});
+
+/**
+ * Following are helper functions
+ */
+async function createTab(url) {
+ let tab = await BrowserTestUtils.openNewForegroundTab(window.gBrowser, url);
+ // Create observer in the content process in order to check the decoder
+ // doctor's notification that would be sent when an error occurs.
+ await SpecialPowers.spawn(tab.linkedBrowser, [], _ => {
+ content._notificationName = "decoder-doctor-notification";
+ content._obs = {
+ observe(subject, topic, data) {
+ let { type, decoderDoctorReportId, formats } = JSON.parse(data);
+ decoderDoctorReportId = decoderDoctorReportId.toLowerCase();
+ info(`received '${type}:${decoderDoctorReportId}:${formats}'`);
+ if (!this._resolve) {
+ ok(false, "receive unexpected notification?");
+ }
+ if (
+ type == this._type &&
+ decoderDoctorReportId == this._decoderDoctorReportId &&
+ formats == this._formats
+ ) {
+ ok(true, `received correct notification`);
+ Services.obs.removeObserver(content._obs, content._notificationName);
+ this._resolve();
+ this._resolve = null;
+ }
+ },
+ // Return a promise that will be resolved once receiving a notification
+ // which has equal data with the input parameters.
+ waitFor({ type, decoderDoctorReportId, formats }) {
+ if (this._resolve) {
+ ok(false, "already has a pending promise!");
+ return Promise.reject();
+ }
+ Services.obs.addObserver(content._obs, content._notificationName);
+ return new Promise(resolve => {
+ info(`waiting for '${type}:${decoderDoctorReportId}:${formats}'`);
+ this._resolve = resolve;
+ this._type = type;
+ this._decoderDoctorReportId = decoderDoctorReportId;
+ this._formats = formats;
+ });
+ },
+ };
+ content._waitForReport = (params, shouldReportNotification) => {
+ const reportToConsolePromise = new Promise(r => {
+ content.document.addEventListener(
+ "mozreportmediaerror",
+ _ => {
+ r();
+ },
+ { once: true }
+ );
+ });
+ const reportToNotificationBannerPromise = shouldReportNotification
+ ? content._obs.waitFor(params)
+ : Promise.resolve();
+ info(
+ `waitForConsole=true, waitForNotificationBanner=${shouldReportNotification}`
+ );
+ return Promise.all([
+ reportToConsolePromise,
+ reportToNotificationBannerPromise,
+ ]);
+ };
+ });
+ return tab;
+}
+
+async function setFormatDiagnosticsReportForMimeType(tab, params) {
+ const shouldReportNotification = gAllowedNotificationTypes.includes(
+ params.decoderDoctorReportId
+ );
+ await SpecialPowers.spawn(
+ tab.linkedBrowser,
+ [params, shouldReportNotification],
+ async (params, shouldReportNotification) => {
+ const video = content.document.createElement("video");
+ SpecialPowers.wrap(video).setFormatDiagnosticsReportForMimeType(
+ params.formats,
+ params.decoderDoctorReportId
+ );
+ await content._waitForReport(params, shouldReportNotification);
+ }
+ );
+ ok(true, `finished check for ${params.decoderDoctorReportId}`);
+}
+
+async function setDecodeError(tab, params) {
+ info(`start check for ${params.error}`);
+ await SpecialPowers.spawn(
+ tab.linkedBrowser,
+ [params],
+ async (params, shouldReportNotification) => {
+ const video = content.document.createElement("video");
+ SpecialPowers.wrap(video).setDecodeError(params.error);
+ await content._waitForReport(params, params.shouldReportNotification);
+ }
+ );
+ ok(true, `finished check for ${params.error}`);
+}
+
+async function setAudioSinkFailedStartup(tab, params) {
+ const shouldReportNotification = gAllowedNotificationTypes.includes(
+ params.decoderDoctorReportId
+ );
+ await SpecialPowers.spawn(
+ tab.linkedBrowser,
+ [params, shouldReportNotification],
+ async (params, shouldReportNotification) => {
+ const video = content.document.createElement("video");
+ const waitPromise = content._waitForReport(
+ params,
+ shouldReportNotification
+ );
+ SpecialPowers.wrap(video).setAudioSinkFailedStartup();
+ await waitPromise;
+ }
+ );
+}