From 26a029d407be480d791972afb5975cf62c9360a6 Mon Sep 17 00:00:00 2001 From: Daniel Baumann Date: Fri, 19 Apr 2024 02:47:55 +0200 Subject: Adding upstream version 124.0.1. Signed-off-by: Daniel Baumann --- dom/midi/tests/MIDITestUtils.js | 94 +++ dom/midi/tests/blank.html | 5 + dom/midi/tests/browser.toml | 23 + dom/midi/tests/browser_midi_permission_gated.js | 839 +++++++++++++++++++++ dom/midi/tests/browser_refresh_port_list.js | 71 ++ dom/midi/tests/browser_stable_midi_port_ids.js | 52 ++ dom/midi/tests/file_midi_permission_gated.html | 15 + dom/midi/tests/mochitest.toml | 41 + dom/midi/tests/port_ids_page_1.html | 17 + dom/midi/tests/port_ids_page_2.html | 17 + dom/midi/tests/refresh_port_list.html | 49 ++ .../tests/test_midi_device_connect_disconnect.html | 54 ++ dom/midi/tests/test_midi_device_enumeration.html | 46 ++ .../test_midi_device_explicit_open_close.html | 94 +++ .../test_midi_device_implicit_open_close.html | 54 ++ dom/midi/tests/test_midi_device_pending.html | 118 +++ dom/midi/tests/test_midi_device_sysex.html | 57 ++ dom/midi/tests/test_midi_device_system_rt.html | 39 + dom/midi/tests/test_midi_message_event.html | 45 ++ .../tests/test_midi_packet_timing_sorting.html | 47 ++ dom/midi/tests/test_midi_permission_allow.html | 26 + dom/midi/tests/test_midi_permission_deny.html | 26 + dom/midi/tests/test_midi_permission_gated.html | 181 +++++ dom/midi/tests/test_midi_permission_prompt.html | 24 + dom/midi/tests/test_midi_send_messages.html | 112 +++ 25 files changed, 2146 insertions(+) create mode 100644 dom/midi/tests/MIDITestUtils.js create mode 100644 dom/midi/tests/blank.html create mode 100644 dom/midi/tests/browser.toml create mode 100644 dom/midi/tests/browser_midi_permission_gated.js create mode 100644 dom/midi/tests/browser_refresh_port_list.js create mode 100644 dom/midi/tests/browser_stable_midi_port_ids.js create mode 100644 dom/midi/tests/file_midi_permission_gated.html create mode 100644 dom/midi/tests/mochitest.toml create mode 100644 dom/midi/tests/port_ids_page_1.html create mode 100644 dom/midi/tests/port_ids_page_2.html create mode 100644 dom/midi/tests/refresh_port_list.html create mode 100644 dom/midi/tests/test_midi_device_connect_disconnect.html create mode 100644 dom/midi/tests/test_midi_device_enumeration.html create mode 100644 dom/midi/tests/test_midi_device_explicit_open_close.html create mode 100644 dom/midi/tests/test_midi_device_implicit_open_close.html create mode 100644 dom/midi/tests/test_midi_device_pending.html create mode 100644 dom/midi/tests/test_midi_device_sysex.html create mode 100644 dom/midi/tests/test_midi_device_system_rt.html create mode 100644 dom/midi/tests/test_midi_message_event.html create mode 100644 dom/midi/tests/test_midi_packet_timing_sorting.html create mode 100644 dom/midi/tests/test_midi_permission_allow.html create mode 100644 dom/midi/tests/test_midi_permission_deny.html create mode 100644 dom/midi/tests/test_midi_permission_gated.html create mode 100644 dom/midi/tests/test_midi_permission_prompt.html create mode 100644 dom/midi/tests/test_midi_send_messages.html (limited to 'dom/midi/tests') diff --git a/dom/midi/tests/MIDITestUtils.js b/dom/midi/tests/MIDITestUtils.js new file mode 100644 index 0000000000..779a961991 --- /dev/null +++ b/dom/midi/tests/MIDITestUtils.js @@ -0,0 +1,94 @@ +var MIDITestUtils = { + permissionSetup: allow => { + let permPromiseRes; + let permPromise = new Promise((res, rej) => { + permPromiseRes = res; + }); + SpecialPowers.pushPrefEnv( + { + set: [ + ["dom.webmidi.enabled", true], + ["midi.testing", true], + ["midi.prompt.testing", true], + ["media.navigator.permission.disabled", allow], + ], + }, + () => { + permPromiseRes(); + } + ); + return permPromise; + }, + // This list needs to stay synced with the ports in + // dom/midi/TestMIDIPlatformService. + inputInfo: { + get id() { + return MIDITestUtils.stableId(this); + }, + name: "Test Control MIDI Device Input Port", + manufacturer: "Test Manufacturer", + version: "1.0.0", + }, + outputInfo: { + get id() { + return MIDITestUtils.stableId(this); + }, + name: "Test Control MIDI Device Output Port", + manufacturer: "Test Manufacturer", + version: "1.0.0", + }, + stateTestInputInfo: { + get id() { + return MIDITestUtils.stableId(this); + }, + name: "Test State MIDI Device Input Port", + manufacturer: "Test Manufacturer", + version: "1.0.0", + }, + stateTestOutputInfo: { + get id() { + return MIDITestUtils.stableId(this); + }, + name: "Test State MIDI Device Output Port", + manufacturer: "Test Manufacturer", + version: "1.0.0", + }, + alwaysClosedTestOutputInfo: { + get id() { + return MIDITestUtils.stableId(this); + }, + name: "Always Closed MIDI Device Output Port", + manufacturer: "Test Manufacturer", + version: "1.0.0", + }, + checkPacket: (expected, actual) => { + if (expected.length != actual.length) { + ok(false, "Packet " + expected + " length not same as packet " + actual); + } + for (var i = 0; i < expected.length; ++i) { + is(expected[i], actual[i], "Packet value " + expected[i] + " matches."); + } + }, + stableId: async info => { + // This computes the stable ID of a MIDI port according to the logic we + // use in the Web MIDI implementation. See MIDIPortChild::GenerateStableId() + // and nsContentUtils::AnonymizeId(). + const id = info.name + info.manufacturer + info.version; + const encoder = new TextEncoder(); + const data = encoder.encode(id); + const keyBytes = encoder.encode(self.origin); + const key = await crypto.subtle.importKey( + "raw", + keyBytes, + { name: "HMAC", hash: "SHA-256" }, + false, + ["sign"] + ); + const result = new Uint8Array(await crypto.subtle.sign("HMAC", key, data)); + let resultString = ""; + for (let i = 0; i < result.length; i++) { + resultString += String.fromCharCode(result[i]); + } + return btoa(resultString); + }, +}; diff --git a/dom/midi/tests/blank.html b/dom/midi/tests/blank.html new file mode 100644 index 0000000000..7fdd0621f6 --- /dev/null +++ b/dom/midi/tests/blank.html @@ -0,0 +1,5 @@ + + + + + diff --git a/dom/midi/tests/browser.toml b/dom/midi/tests/browser.toml new file mode 100644 index 0000000000..1741a9e25f --- /dev/null +++ b/dom/midi/tests/browser.toml @@ -0,0 +1,23 @@ +[DEFAULT] +prefs = [ + "dom.webmidi.enabled=true", + "midi.testing=true", + "midi.prompt.testing=true", + "media.navigator.permission.disabled=true", + "dom.sitepermsaddon-provider.enabled=true", +] + +["browser_midi_permission_gated.js"] +support-files = ["blank.html"] +skip-if = ["a11y_checks"] # Bug 1858041 clicked popup-notification-primary-button may not be focusable, intermittent results (passes on Try, fails on Autoland) + +["browser_refresh_port_list.js"] +run-if = ["os != 'android'"] +support-files = ["refresh_port_list.html"] + +["browser_stable_midi_port_ids.js"] +run-if = ["os != 'android'"] +support-files = [ + "port_ids_page_1.html", + "port_ids_page_2.html", +] diff --git a/dom/midi/tests/browser_midi_permission_gated.js b/dom/midi/tests/browser_midi_permission_gated.js new file mode 100644 index 0000000000..2bdce51a2d --- /dev/null +++ b/dom/midi/tests/browser_midi_permission_gated.js @@ -0,0 +1,839 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +const EXAMPLE_COM_URL = + "https://example.com/document-builder.sjs?html=

Test midi permission with synthetic site permission addon

"; +const PAGE_WITH_IFRAMES_URL = `https://example.org/document-builder.sjs?html= +

Test midi permission with synthetic site permission addon in iframes

+ + `; + +const l10n = new Localization( + [ + "browser/addonNotifications.ftl", + "toolkit/global/extensions.ftl", + "toolkit/global/extensionPermissions.ftl", + "branding/brand.ftl", + ], + true +); + +const { HttpServer } = ChromeUtils.importESModule( + "resource://testing-common/httpd.sys.mjs" +); +ChromeUtils.defineESModuleGetters(this, { + AddonTestUtils: "resource://testing-common/AddonTestUtils.sys.mjs", +}); + +add_setup(async function () { + await SpecialPowers.pushPrefEnv({ + set: [["midi.prompt.testing", false]], + }); + + AddonTestUtils.initMochitest(this); + AddonTestUtils.hookAMTelemetryEvents(); + + // Once the addon is installed, a dialog is displayed as a confirmation. + // This could interfere with tests running after this one, so we set up a listener + // that will always accept post install dialogs so we don't have to deal with them in + // the test. + alwaysAcceptAddonPostInstallDialogs(); + + registerCleanupFunction(async () => { + // Remove the permission. + await SpecialPowers.removePermission("midi-sysex", { + url: EXAMPLE_COM_URL, + }); + await SpecialPowers.removePermission("midi-sysex", { + url: PAGE_WITH_IFRAMES_URL, + }); + await SpecialPowers.removePermission("midi", { + url: EXAMPLE_COM_URL, + }); + await SpecialPowers.removePermission("midi", { + url: PAGE_WITH_IFRAMES_URL, + }); + await SpecialPowers.removePermission("install", { + url: EXAMPLE_COM_URL, + }); + + while (gBrowser.tabs.length > 1) { + BrowserTestUtils.removeTab(gBrowser.selectedTab); + } + }); +}); + +add_task(async function testRequestMIDIAccess() { + gBrowser.selectedTab = BrowserTestUtils.addTab(gBrowser, EXAMPLE_COM_URL); + await BrowserTestUtils.browserLoaded(gBrowser.selectedBrowser); + const testPageHost = gBrowser.selectedTab.linkedBrowser.documentURI.host; + Services.fog.testResetFOG(); + + info("Check that midi-sysex isn't set"); + ok( + await SpecialPowers.testPermission( + "midi-sysex", + SpecialPowers.Services.perms.UNKNOWN_ACTION, + { url: EXAMPLE_COM_URL } + ), + "midi-sysex value should have UNKNOWN permission" + ); + + info("Request midi-sysex access"); + let onAddonInstallBlockedNotification = waitForNotification( + "addon-install-blocked" + ); + await SpecialPowers.spawn(gBrowser.selectedBrowser, [], () => { + content.midiAccessRequestPromise = content.navigator.requestMIDIAccess({ + sysex: true, + }); + }); + + info("Deny site permission addon install in first popup"); + let addonInstallPanel = await onAddonInstallBlockedNotification; + const [installPopupHeader, installPopupMessage] = + addonInstallPanel.querySelectorAll( + "description.popup-notification-description" + ); + is( + installPopupHeader.textContent, + l10n.formatValueSync("site-permission-install-first-prompt-midi-header"), + "First popup has expected header text" + ); + is( + installPopupMessage.textContent, + l10n.formatValueSync("site-permission-install-first-prompt-midi-message"), + "First popup has expected message" + ); + + let notification = addonInstallPanel.childNodes[0]; + // secondaryButton is the "Don't allow" button + notification.secondaryButton.click(); + + let rejectionMessage = await SpecialPowers.spawn( + gBrowser.selectedBrowser, + [], + async () => { + let errorMessage; + try { + await content.midiAccessRequestPromise; + } catch (e) { + errorMessage = `${e.name}: ${e.message}`; + } + + delete content.midiAccessRequestPromise; + return errorMessage; + } + ); + is( + rejectionMessage, + "SecurityError: WebMIDI requires a site permission add-on to activate" + ); + + assertSitePermissionInstallTelemetryEvents(["site_warning", "cancelled"]); + + info("Deny site permission addon install in second popup"); + onAddonInstallBlockedNotification = waitForNotification( + "addon-install-blocked" + ); + await SpecialPowers.spawn(gBrowser.selectedBrowser, [], () => { + content.midiAccessRequestPromise = content.navigator.requestMIDIAccess({ + sysex: true, + }); + }); + addonInstallPanel = await onAddonInstallBlockedNotification; + notification = addonInstallPanel.childNodes[0]; + let dialogPromise = waitForInstallDialog(); + notification.button.click(); + let installDialog = await dialogPromise; + is( + installDialog.querySelector(".popup-notification-description").textContent, + l10n.formatValueSync( + "webext-site-perms-header-with-gated-perms-midi-sysex", + { hostname: testPageHost } + ), + "Install dialog has expected header text" + ); + is( + installDialog.querySelector("popupnotificationcontent description") + .textContent, + l10n.formatValueSync("webext-site-perms-description-gated-perms-midi"), + "Install dialog has expected description" + ); + + // secondaryButton is the "Cancel" button + installDialog.secondaryButton.click(); + + rejectionMessage = await SpecialPowers.spawn( + gBrowser.selectedBrowser, + [], + async () => { + let errorMessage; + try { + await content.midiAccessRequestPromise; + } catch (e) { + errorMessage = `${e.name}: ${e.message}`; + } + + delete content.midiAccessRequestPromise; + return errorMessage; + } + ); + is( + rejectionMessage, + "SecurityError: WebMIDI requires a site permission add-on to activate" + ); + + assertSitePermissionInstallTelemetryEvents([ + "site_warning", + "permissions_prompt", + "cancelled", + ]); + + info("Request midi-sysex access again"); + onAddonInstallBlockedNotification = waitForNotification( + "addon-install-blocked" + ); + await SpecialPowers.spawn(gBrowser.selectedBrowser, [], () => { + content.midiAccessRequestPromise = content.navigator.requestMIDIAccess({ + sysex: true, + }); + }); + + info("Accept site permission addon install"); + addonInstallPanel = await onAddonInstallBlockedNotification; + notification = addonInstallPanel.childNodes[0]; + dialogPromise = waitForInstallDialog(); + notification.button.click(); + installDialog = await dialogPromise; + installDialog.button.click(); + + info("Wait for the midi-sysex access request promise to resolve"); + let accessGranted = await SpecialPowers.spawn( + gBrowser.selectedBrowser, + [], + async () => { + try { + await content.midiAccessRequestPromise; + return true; + } catch (e) {} + + delete content.midiAccessRequestPromise; + return false; + } + ); + ok(accessGranted, "requestMIDIAccess resolved"); + + info("Check that midi-sysex is now set"); + ok( + await SpecialPowers.testPermission( + "midi-sysex", + SpecialPowers.Services.perms.ALLOW_ACTION, + { url: EXAMPLE_COM_URL } + ), + "midi-sysex value should have ALLOW permission" + ); + ok( + await SpecialPowers.testPermission( + "midi", + SpecialPowers.Services.perms.UNKNOWN_ACTION, + { url: EXAMPLE_COM_URL } + ), + "but midi should have UNKNOWN permission" + ); + + info("Check that we don't prompt user again once they installed the addon"); + const accessPromiseState = await SpecialPowers.spawn( + gBrowser.selectedBrowser, + [], + async () => { + return content.navigator + .requestMIDIAccess({ sysex: true }) + .then(() => "resolved"); + } + ); + is( + accessPromiseState, + "resolved", + "requestMIDIAccess resolved without user prompt" + ); + + assertSitePermissionInstallTelemetryEvents([ + "site_warning", + "permissions_prompt", + "completed", + ]); + + info("Request midi access without sysex"); + onAddonInstallBlockedNotification = waitForNotification( + "addon-install-blocked" + ); + await SpecialPowers.spawn(gBrowser.selectedBrowser, [], () => { + content.midiNoSysexAccessRequestPromise = + content.navigator.requestMIDIAccess(); + }); + + info("Accept site permission addon install"); + addonInstallPanel = await onAddonInstallBlockedNotification; + notification = addonInstallPanel.childNodes[0]; + + is( + notification + .querySelector("#addon-install-blocked-info") + .getAttribute("href"), + Services.urlFormatter.formatURLPref("app.support.baseURL") + + "site-permission-addons", + "Got the expected SUMO page as a learn more link in the addon-install-blocked panel" + ); + + dialogPromise = waitForInstallDialog(); + notification.button.click(); + installDialog = await dialogPromise; + + is( + installDialog.querySelector(".popup-notification-description").textContent, + l10n.formatValueSync("webext-site-perms-header-with-gated-perms-midi", { + hostname: testPageHost, + }), + "Install dialog has expected header text" + ); + is( + installDialog.querySelector("popupnotificationcontent description") + .textContent, + l10n.formatValueSync("webext-site-perms-description-gated-perms-midi"), + "Install dialog has expected description" + ); + + installDialog.button.click(); + + info("Wait for the midi access request promise to resolve"); + accessGranted = await SpecialPowers.spawn( + gBrowser.selectedBrowser, + [], + async () => { + try { + await content.midiNoSysexAccessRequestPromise; + return true; + } catch (e) {} + + delete content.midiNoSysexAccessRequestPromise; + return false; + } + ); + ok(accessGranted, "requestMIDIAccess resolved"); + + info("Check that both midi-sysex and midi are now set"); + ok( + await SpecialPowers.testPermission( + "midi-sysex", + SpecialPowers.Services.perms.ALLOW_ACTION, + { url: EXAMPLE_COM_URL } + ), + "midi-sysex value should have ALLOW permission" + ); + ok( + await SpecialPowers.testPermission( + "midi", + SpecialPowers.Services.perms.ALLOW_ACTION, + { url: EXAMPLE_COM_URL } + ), + "and midi value should also have ALLOW permission" + ); + + assertSitePermissionInstallTelemetryEvents([ + "site_warning", + "permissions_prompt", + "completed", + ]); + + info("Check that we don't prompt user again when they perm denied"); + // remove permission to have a clean state + await SpecialPowers.removePermission("midi-sysex", { + url: EXAMPLE_COM_URL, + }); + + onAddonInstallBlockedNotification = waitForNotification( + "addon-install-blocked" + ); + await SpecialPowers.spawn(gBrowser.selectedBrowser, [], () => { + content.midiAccessRequestPromise = content.navigator.requestMIDIAccess({ + sysex: true, + }); + }); + + info("Perm-deny site permission addon install"); + addonInstallPanel = await onAddonInstallBlockedNotification; + // Click the "Report Suspicious Site" menuitem, which has the same effect as + // "Never Allow" and also submits a telemetry event (which we check below). + notification.menupopup.querySelectorAll("menuitem")[1].click(); + + rejectionMessage = await SpecialPowers.spawn( + gBrowser.selectedBrowser, + [], + async () => { + let errorMessage; + try { + await content.midiAccessRequestPromise; + } catch (e) { + errorMessage = e.name; + } + + delete content.midiAccessRequestPromise; + return errorMessage; + } + ); + is(rejectionMessage, "SecurityError", "requestMIDIAccess was rejected"); + + info("Request midi-sysex access again"); + let denyIntervalStart = performance.now(); + rejectionMessage = await SpecialPowers.spawn( + gBrowser.selectedBrowser, + [], + async () => { + let errorMessage; + try { + await content.navigator.requestMIDIAccess({ + sysex: true, + }); + } catch (e) { + errorMessage = e.name; + } + return errorMessage; + } + ); + is( + rejectionMessage, + "SecurityError", + "requestMIDIAccess was rejected without user prompt" + ); + let denyIntervalElapsed = performance.now() - denyIntervalStart; + Assert.greaterOrEqual( + denyIntervalElapsed, + 3000, + `Rejection should be delayed by a randomized interval no less than 3 seconds (got ${ + denyIntervalElapsed / 1000 + } seconds)` + ); + + Assert.deepEqual( + [{ suspicious_site: "example.com" }], + AddonTestUtils.getAMGleanEvents("reportSuspiciousSite"), + "Expected Glean event recorded." + ); + + // Invoking getAMTelemetryEvents resets the mocked event array, and we want + // to test two different things here, so we cache it. + let events = AddonTestUtils.getAMTelemetryEvents(); + Assert.deepEqual( + events.filter(evt => evt.method == "reportSuspiciousSite")[0], + { + method: "reportSuspiciousSite", + object: "suspiciousSite", + value: "example.com", + extra: undefined, + } + ); + assertSitePermissionInstallTelemetryEvents( + ["site_warning", "cancelled"], + events + ); +}); + +add_task(async function testIframeRequestMIDIAccess() { + gBrowser.selectedTab = BrowserTestUtils.addTab( + gBrowser, + PAGE_WITH_IFRAMES_URL + ); + await BrowserTestUtils.browserLoaded(gBrowser.selectedBrowser); + + info("Check that midi-sysex isn't set"); + ok( + await SpecialPowers.testPermission( + "midi-sysex", + SpecialPowers.Services.perms.UNKNOWN_ACTION, + { url: PAGE_WITH_IFRAMES_URL } + ), + "midi-sysex value should have UNKNOWN permission" + ); + + info("Request midi-sysex access from the same-origin iframe"); + const sameOriginIframeBrowsingContext = await SpecialPowers.spawn( + gBrowser.selectedBrowser, + [], + async () => { + return content.document.getElementById("sameOrigin").browsingContext; + } + ); + + let onAddonInstallBlockedNotification = waitForNotification( + "addon-install-blocked" + ); + await SpecialPowers.spawn(sameOriginIframeBrowsingContext, [], () => { + content.midiAccessRequestPromise = content.navigator.requestMIDIAccess({ + sysex: true, + }); + }); + + info("Accept site permission addon install"); + const addonInstallPanel = await onAddonInstallBlockedNotification; + const notification = addonInstallPanel.childNodes[0]; + const dialogPromise = waitForInstallDialog(); + notification.button.click(); + let installDialog = await dialogPromise; + installDialog.button.click(); + + info("Wait for the midi-sysex access request promise to resolve"); + const accessGranted = await SpecialPowers.spawn( + sameOriginIframeBrowsingContext, + [], + async () => { + try { + await content.midiAccessRequestPromise; + return true; + } catch (e) {} + + delete content.midiAccessRequestPromise; + return false; + } + ); + ok(accessGranted, "requestMIDIAccess resolved"); + + info("Check that midi-sysex is now set"); + ok( + await SpecialPowers.testPermission( + "midi-sysex", + SpecialPowers.Services.perms.ALLOW_ACTION, + { url: PAGE_WITH_IFRAMES_URL } + ), + "midi-sysex value should have ALLOW permission" + ); + + info( + "Check that we don't prompt user again once they installed the addon from the same-origin iframe" + ); + const accessPromiseState = await SpecialPowers.spawn( + gBrowser.selectedBrowser, + [], + async () => { + return content.navigator + .requestMIDIAccess({ sysex: true }) + .then(() => "resolved"); + } + ); + is( + accessPromiseState, + "resolved", + "requestMIDIAccess resolved without user prompt" + ); + + assertSitePermissionInstallTelemetryEvents([ + "site_warning", + "permissions_prompt", + "completed", + ]); + + info("Check that request is rejected when done from a cross-origin iframe"); + const crossOriginIframeBrowsingContext = await SpecialPowers.spawn( + gBrowser.selectedBrowser, + [], + async () => { + return content.document.getElementById("crossOrigin").browsingContext; + } + ); + + const onConsoleErrorMessage = new Promise(resolve => { + const errorListener = { + observe(error) { + if (error.message.includes("WebMIDI access request was denied")) { + resolve(error); + Services.console.unregisterListener(errorListener); + } + }, + }; + Services.console.registerListener(errorListener); + }); + + const rejectionMessage = await SpecialPowers.spawn( + crossOriginIframeBrowsingContext, + [], + async () => { + let errorName; + try { + await content.navigator.requestMIDIAccess({ + sysex: true, + }); + } catch (e) { + errorName = e.name; + } + return errorName; + } + ); + + is( + rejectionMessage, + "SecurityError", + "requestMIDIAccess from the remote iframe was rejected" + ); + + const consoleErrorMessage = await onConsoleErrorMessage; + ok( + consoleErrorMessage.message.includes( + `WebMIDI access request was denied: ❝SitePermsAddons can't be installed from cross origin subframes❞`, + "an error message is sent to the console" + ) + ); + assertSitePermissionInstallTelemetryEvents([]); +}); + +add_task(async function testRequestMIDIAccessLocalhost() { + const httpServer = new HttpServer(); + httpServer.start(-1); + httpServer.registerPathHandler(`/test`, function (request, response) { + response.setStatusLine(request.httpVersion, 200, "OK"); + response.write(` + + +

Test requestMIDIAccess on lcoalhost

`); + }); + const localHostTestUrl = `http://localhost:${httpServer.identity.primaryPort}/test`; + + registerCleanupFunction(async function cleanup() { + await new Promise(resolve => httpServer.stop(resolve)); + }); + + gBrowser.selectedTab = BrowserTestUtils.addTab(gBrowser, localHostTestUrl); + await BrowserTestUtils.browserLoaded(gBrowser.selectedBrowser); + + info("Check that midi-sysex isn't set"); + ok( + await SpecialPowers.testPermission( + "midi-sysex", + SpecialPowers.Services.perms.UNKNOWN_ACTION, + { url: localHostTestUrl } + ), + "midi-sysex value should have UNKNOWN permission" + ); + + info( + "Request midi-sysex access should not prompt for addon install on locahost, but for permission" + ); + let popupShown = BrowserTestUtils.waitForEvent( + PopupNotifications.panel, + "popupshown" + ); + await SpecialPowers.spawn(gBrowser.selectedBrowser, [], () => { + content.midiAccessRequestPromise = content.navigator.requestMIDIAccess({ + sysex: true, + }); + }); + await popupShown; + is( + PopupNotifications.panel.querySelector("popupnotification").id, + "midi-notification", + "midi notification was displayed" + ); + + info("Accept permission"); + PopupNotifications.panel + .querySelector(".popup-notification-primary-button") + .click(); + + info("Wait for the midi-sysex access request promise to resolve"); + const accessGranted = await SpecialPowers.spawn( + gBrowser.selectedBrowser, + [], + async () => { + try { + await content.midiAccessRequestPromise; + return true; + } catch (e) {} + + delete content.midiAccessRequestPromise; + return false; + } + ); + ok(accessGranted, "requestMIDIAccess resolved"); + + info("Check that we prompt user again even if they accepted before"); + popupShown = BrowserTestUtils.waitForEvent( + PopupNotifications.panel, + "popupshown" + ); + await SpecialPowers.spawn(gBrowser.selectedBrowser, [], async () => { + content.navigator.requestMIDIAccess({ sysex: true }); + }); + await popupShown; + is( + PopupNotifications.panel.querySelector("popupnotification").id, + "midi-notification", + "midi notification was displayed again" + ); + + assertSitePermissionInstallTelemetryEvents([]); +}); + +add_task(async function testDisabledRequestMIDIAccessFile() { + let dir = getChromeDir(getResolvedURI(gTestPath)); + dir.append("blank.html"); + const fileSchemeTestUri = Services.io.newFileURI(dir).spec; + + gBrowser.selectedTab = BrowserTestUtils.addTab(gBrowser, fileSchemeTestUri); + await BrowserTestUtils.browserLoaded(gBrowser.selectedBrowser); + + info("Check that requestMIDIAccess isn't set on navigator on file scheme"); + const isRequestMIDIAccessDefined = await SpecialPowers.spawn( + gBrowser.selectedBrowser, + [], + () => { + return "requestMIDIAccess" in content.wrappedJSObject.navigator; + } + ); + is( + isRequestMIDIAccessDefined, + false, + "navigator.requestMIDIAccess is not defined on file scheme" + ); +}); + +// Ignore any additional telemetry events collected in this file. +// Unfortunately it doesn't work to have this in a cleanup function. +// Keep this as the last task done. +add_task(function teardown_telemetry_events() { + AddonTestUtils.getAMTelemetryEvents(); +}); + +/** + * Check that the expected sitepermission install events are recorded. + * + * @param {Array} expectedSteps: An array of the expected extra.step values recorded. + */ +function assertSitePermissionInstallTelemetryEvents( + expectedSteps, + events = null +) { + let amInstallEvents = (events ?? AddonTestUtils.getAMTelemetryEvents()) + .filter(evt => evt.method === "install" && evt.object === "sitepermission") + .map(evt => evt.extra.step); + + Assert.deepEqual(amInstallEvents, expectedSteps); +} + +async function waitForInstallDialog(id = "addon-webext-permissions") { + let panel = await waitForNotification(id); + return panel.childNodes[0]; +} + +/** + * Adds an event listener that will listen for post-install dialog event and automatically + * close the dialogs. + */ +function alwaysAcceptAddonPostInstallDialogs() { + // Once the addon is installed, a dialog is displayed as a confirmation. + // This could interfere with tests running after this one, so we set up a listener + // that will always accept post install dialogs so we don't have to deal with them in + // the test. + const abortController = new AbortController(); + + const { AppMenuNotifications } = ChromeUtils.importESModule( + "resource://gre/modules/AppMenuNotifications.sys.mjs" + ); + info("Start listening and accept addon post-install notifications"); + PanelUI.notificationPanel.addEventListener( + "popupshown", + async function popupshown() { + let notification = AppMenuNotifications.activeNotification; + if (!notification || notification.id !== "addon-installed") { + return; + } + + let popupnotificationID = PanelUI._getPopupId(notification); + if (popupnotificationID) { + info("Accept post-install dialog"); + let popupnotification = document.getElementById(popupnotificationID); + popupnotification?.button.click(); + } + }, + { + signal: abortController.signal, + } + ); + + registerCleanupFunction(async () => { + // Clear the listener at the end of the test file, to prevent it to stay + // around when the same browser instance may be running other unrelated + // test files. + abortController.abort(); + }); +} + +const PROGRESS_NOTIFICATION = "addon-progress"; +async function waitForNotification(notificationId) { + info(`Waiting for ${notificationId} notification`); + + let topic = getObserverTopic(notificationId); + + let observerPromise; + if (notificationId !== "addon-webext-permissions") { + observerPromise = new Promise(resolve => { + Services.obs.addObserver(function observer(aSubject, aTopic, aData) { + // Ignore the progress notification unless that is the notification we want + if ( + notificationId != PROGRESS_NOTIFICATION && + aTopic == getObserverTopic(PROGRESS_NOTIFICATION) + ) { + return; + } + Services.obs.removeObserver(observer, topic); + resolve(); + }, topic); + }); + } + + let panelEventPromise = new Promise(resolve => { + window.PopupNotifications.panel.addEventListener( + "PanelUpdated", + function eventListener(e) { + // Skip notifications that are not the one that we are supposed to be looking for + if (!e.detail.includes(notificationId)) { + return; + } + window.PopupNotifications.panel.removeEventListener( + "PanelUpdated", + eventListener + ); + resolve(); + } + ); + }); + + await observerPromise; + await panelEventPromise; + await waitForTick(); + + info(`Saw a ${notificationId} notification`); + await SimpleTest.promiseFocus(window.PopupNotifications.window); + return window.PopupNotifications.panel; +} + +// This function is similar to the one in +// toolkit/mozapps/extensions/test/xpinstall/browser_doorhanger_installs.js, +// please keep both in sync! +function getObserverTopic(aNotificationId) { + let topic = aNotificationId; + if (topic == "xpinstall-disabled") { + topic = "addon-install-disabled"; + } else if (topic == "addon-progress") { + topic = "addon-install-started"; + } else if (topic == "addon-installed") { + topic = "webextension-install-notify"; + } + return topic; +} + +function waitForTick() { + return new Promise(resolve => executeSoon(resolve)); +} diff --git a/dom/midi/tests/browser_refresh_port_list.js b/dom/midi/tests/browser_refresh_port_list.js new file mode 100644 index 0000000000..152b067254 --- /dev/null +++ b/dom/midi/tests/browser_refresh_port_list.js @@ -0,0 +1,71 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +const EXAMPLE_ORG_URL = "https://example.org/browser/dom/midi/tests/"; +const PAGE = "refresh_port_list.html"; + +async function get_access(browser) { + return SpecialPowers.spawn(gBrowser.selectedBrowser, [], function () { + return content.wrappedJSObject.get_access(); + }); +} + +async function reset_access(browser) { + return SpecialPowers.spawn(gBrowser.selectedBrowser, [], function () { + return content.wrappedJSObject.reset_access(); + }); +} + +async function get_num_ports(browser) { + return SpecialPowers.spawn(gBrowser.selectedBrowser, [], function () { + return content.wrappedJSObject.get_num_ports(); + }); +} + +async function add_port(browser) { + return SpecialPowers.spawn(gBrowser.selectedBrowser, [], () => { + return content.wrappedJSObject.add_port(); + }); +} + +async function remove_port(browser) { + return SpecialPowers.spawn(gBrowser.selectedBrowser, [], () => { + return content.wrappedJSObject.remove_port(); + }); +} + +async function force_refresh(browser) { + return SpecialPowers.spawn(gBrowser.selectedBrowser, [], () => { + return content.wrappedJSObject.force_refresh(); + }); +} + +add_task(async function () { + gBrowser.selectedTab = BrowserTestUtils.addTab( + gBrowser, + EXAMPLE_ORG_URL + PAGE + ); + await BrowserTestUtils.browserLoaded(gBrowser.selectedBrowser); + + await get_access(gBrowser.selectedBrowser); + let ports_num = await get_num_ports(gBrowser.selectedBrowser); + Assert.equal(ports_num, 4, "We start with four ports"); + await add_port(gBrowser.selectedBrowser); + ports_num = await get_num_ports(gBrowser.selectedBrowser); + Assert.equal(ports_num, 5, "One port is added manually"); + // This causes the test service to refresh the ports the next time a refresh + // is requested, it will happen after we reload the tab later on and will add + // back the port that we're removing on the next line. + await force_refresh(gBrowser.selectedBrowser); + await remove_port(gBrowser.selectedBrowser); + ports_num = await get_num_ports(gBrowser.selectedBrowser); + Assert.equal(ports_num, 4, "One port is removed manually"); + + await BrowserTestUtils.reloadTab(gBrowser.selectedTab); + + await get_access(gBrowser.selectedBrowser); + let refreshed_ports_num = await get_num_ports(gBrowser.selectedBrowser); + Assert.equal(refreshed_ports_num, 5, "One port is added by the refresh"); + + gBrowser.removeTab(gBrowser.selectedTab); +}); diff --git a/dom/midi/tests/browser_stable_midi_port_ids.js b/dom/midi/tests/browser_stable_midi_port_ids.js new file mode 100644 index 0000000000..e7d3056160 --- /dev/null +++ b/dom/midi/tests/browser_stable_midi_port_ids.js @@ -0,0 +1,52 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +const EXAMPLE_COM_URL = "https://example.com/browser/dom/midi/tests/"; +const EXAMPLE_ORG_URL = "https://example.org/browser/dom/midi/tests/"; +const PAGE1 = "port_ids_page_1.html"; +const PAGE2 = "port_ids_page_2.html"; + +// Return the MIDI port id of the first input port for the given URL and page +function id_for_tab(url, page) { + return BrowserTestUtils.withNewTab( + { + gBrowser, + url: url + page, + waitForLoad: true, + }, + async function (browser) { + return SpecialPowers.spawn(browser, [""], function () { + return content.wrappedJSObject.get_first_input_id(); + }); + } + ); +} + +add_task(async function () { + let com_page1; + let com_page1_reload; + let org_page1; + let org_page2; + + [com_page1, com_page1_reload, org_page1, org_page2] = await Promise.all([ + id_for_tab(EXAMPLE_COM_URL, PAGE1), + id_for_tab(EXAMPLE_COM_URL, PAGE1), + id_for_tab(EXAMPLE_ORG_URL, PAGE1), + id_for_tab(EXAMPLE_ORG_URL, PAGE2), + ]); + Assert.equal( + com_page1, + com_page1_reload, + "MIDI port ids should be the same when reloading the same page" + ); + Assert.notEqual( + com_page1, + org_page1, + "MIDI port ids should be different in different origins" + ); + Assert.equal( + org_page1, + org_page2, + "MIDI port ids should be the same in the same origin" + ); +}); diff --git a/dom/midi/tests/file_midi_permission_gated.html b/dom/midi/tests/file_midi_permission_gated.html new file mode 100644 index 0000000000..8e3ed4d625 --- /dev/null +++ b/dom/midi/tests/file_midi_permission_gated.html @@ -0,0 +1,15 @@ + + + + + + diff --git a/dom/midi/tests/mochitest.toml b/dom/midi/tests/mochitest.toml new file mode 100644 index 0000000000..93a34003af --- /dev/null +++ b/dom/midi/tests/mochitest.toml @@ -0,0 +1,41 @@ +[DEFAULT] +support-files = [ + "MIDITestUtils.js", + "file_midi_permission_gated.html", +] +scheme = "https" + +["test_midi_device_connect_disconnect.html"] +disabled = "Bug 1437204" + +["test_midi_device_enumeration.html"] + +["test_midi_device_explicit_open_close.html"] + +["test_midi_device_implicit_open_close.html"] + +["test_midi_device_pending.html"] +disabled = "Bug 1437204" + +["test_midi_device_sysex.html"] + +["test_midi_device_system_rt.html"] + +["test_midi_message_event.html"] + +["test_midi_packet_timing_sorting.html"] + +["test_midi_permission_allow.html"] + +["test_midi_permission_deny.html"] + +["test_midi_permission_gated.html"] +skip-if = [ + "os == 'android'", #Bug 1747637 + "http3", + "http2", +] + +["test_midi_permission_prompt.html"] + +["test_midi_send_messages.html"] diff --git a/dom/midi/tests/port_ids_page_1.html b/dom/midi/tests/port_ids_page_1.html new file mode 100644 index 0000000000..31dadad4a5 --- /dev/null +++ b/dom/midi/tests/port_ids_page_1.html @@ -0,0 +1,17 @@ + + + +Stable MIDI port id test + + + + + + diff --git a/dom/midi/tests/port_ids_page_2.html b/dom/midi/tests/port_ids_page_2.html new file mode 100644 index 0000000000..8c313b04da --- /dev/null +++ b/dom/midi/tests/port_ids_page_2.html @@ -0,0 +1,17 @@ + + + +Stable MIDI port id test + + + + + + diff --git a/dom/midi/tests/refresh_port_list.html b/dom/midi/tests/refresh_port_list.html new file mode 100644 index 0000000000..96e4a7a309 --- /dev/null +++ b/dom/midi/tests/refresh_port_list.html @@ -0,0 +1,49 @@ + + + +Refresh MIDI port list test + + + + + + diff --git a/dom/midi/tests/test_midi_device_connect_disconnect.html b/dom/midi/tests/test_midi_device_connect_disconnect.html new file mode 100644 index 0000000000..338d1de55d --- /dev/null +++ b/dom/midi/tests/test_midi_device_connect_disconnect.html @@ -0,0 +1,54 @@ + + + WebMIDI Listener Test + + + + + + + + + + diff --git a/dom/midi/tests/test_midi_device_enumeration.html b/dom/midi/tests/test_midi_device_enumeration.html new file mode 100644 index 0000000000..1dab1c8cf7 --- /dev/null +++ b/dom/midi/tests/test_midi_device_enumeration.html @@ -0,0 +1,46 @@ + + + WebMIDI Listener Test + + + + + + + + + + diff --git a/dom/midi/tests/test_midi_device_explicit_open_close.html b/dom/midi/tests/test_midi_device_explicit_open_close.html new file mode 100644 index 0000000000..d3ed910a55 --- /dev/null +++ b/dom/midi/tests/test_midi_device_explicit_open_close.html @@ -0,0 +1,94 @@ + + + WebMIDI Device Open/Close Test + + + + + + + + + + diff --git a/dom/midi/tests/test_midi_device_implicit_open_close.html b/dom/midi/tests/test_midi_device_implicit_open_close.html new file mode 100644 index 0000000000..cddbaf26c2 --- /dev/null +++ b/dom/midi/tests/test_midi_device_implicit_open_close.html @@ -0,0 +1,54 @@ + + + WebMIDI Listener Test + + + + + + + + + + diff --git a/dom/midi/tests/test_midi_device_pending.html b/dom/midi/tests/test_midi_device_pending.html new file mode 100644 index 0000000000..2e6bd08420 --- /dev/null +++ b/dom/midi/tests/test_midi_device_pending.html @@ -0,0 +1,118 @@ + + + WebMIDI Listener Test + + + + + + + + + + diff --git a/dom/midi/tests/test_midi_device_sysex.html b/dom/midi/tests/test_midi_device_sysex.html new file mode 100644 index 0000000000..618f54ac8a --- /dev/null +++ b/dom/midi/tests/test_midi_device_sysex.html @@ -0,0 +1,57 @@ + + + WebMIDI Listener Test + + + + + + + + + + diff --git a/dom/midi/tests/test_midi_device_system_rt.html b/dom/midi/tests/test_midi_device_system_rt.html new file mode 100644 index 0000000000..81de0c3a94 --- /dev/null +++ b/dom/midi/tests/test_midi_device_system_rt.html @@ -0,0 +1,39 @@ + + + WebMIDI Listener Test + + + + + + + + + + diff --git a/dom/midi/tests/test_midi_message_event.html b/dom/midi/tests/test_midi_message_event.html new file mode 100644 index 0000000000..098b033008 --- /dev/null +++ b/dom/midi/tests/test_midi_message_event.html @@ -0,0 +1,45 @@ + + + + WebMIDI MIDIMessageEvent Test + + + + + + + + + + + diff --git a/dom/midi/tests/test_midi_packet_timing_sorting.html b/dom/midi/tests/test_midi_packet_timing_sorting.html new file mode 100644 index 0000000000..3c344066ac --- /dev/null +++ b/dom/midi/tests/test_midi_packet_timing_sorting.html @@ -0,0 +1,47 @@ + + + WebMIDI Listener Test + + + + + + + + + + diff --git a/dom/midi/tests/test_midi_permission_allow.html b/dom/midi/tests/test_midi_permission_allow.html new file mode 100644 index 0000000000..84578cfeae --- /dev/null +++ b/dom/midi/tests/test_midi_permission_allow.html @@ -0,0 +1,26 @@ + + + WebMIDI Listener Test + + + + + + + + diff --git a/dom/midi/tests/test_midi_permission_deny.html b/dom/midi/tests/test_midi_permission_deny.html new file mode 100644 index 0000000000..8e3043a49a --- /dev/null +++ b/dom/midi/tests/test_midi_permission_deny.html @@ -0,0 +1,26 @@ + + + WebMIDI Listener Test + + + + + + + + diff --git a/dom/midi/tests/test_midi_permission_gated.html b/dom/midi/tests/test_midi_permission_gated.html new file mode 100644 index 0000000000..0e85e99e9c --- /dev/null +++ b/dom/midi/tests/test_midi_permission_gated.html @@ -0,0 +1,181 @@ + + + WebMIDI Listener Test + + + + + + + + + + diff --git a/dom/midi/tests/test_midi_permission_prompt.html b/dom/midi/tests/test_midi_permission_prompt.html new file mode 100644 index 0000000000..26a6b3d789 --- /dev/null +++ b/dom/midi/tests/test_midi_permission_prompt.html @@ -0,0 +1,24 @@ + + + WebMIDI Listener Test + + + + + + + + diff --git a/dom/midi/tests/test_midi_send_messages.html b/dom/midi/tests/test_midi_send_messages.html new file mode 100644 index 0000000000..1d78709877 --- /dev/null +++ b/dom/midi/tests/test_midi_send_messages.html @@ -0,0 +1,112 @@ + + + + WebMIDI Send Test + + + + + + + + + + + -- cgit v1.2.3