summaryrefslogtreecommitdiffstats
path: root/testing/web-platform/tests/audio-output
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-19 01:47:29 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-19 01:47:29 +0000
commit0ebf5bdf043a27fd3dfb7f92e0cb63d88954c44d (patch)
treea31f07c9bcca9d56ce61e9a1ffd30ef350d513aa /testing/web-platform/tests/audio-output
parentInitial commit. (diff)
downloadfirefox-esr-0ebf5bdf043a27fd3dfb7f92e0cb63d88954c44d.tar.xz
firefox-esr-0ebf5bdf043a27fd3dfb7f92e0cb63d88954c44d.zip
Adding upstream version 115.8.0esr.upstream/115.8.0esr
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'testing/web-platform/tests/audio-output')
-rw-r--r--testing/web-platform/tests/audio-output/META.yml4
-rw-r--r--testing/web-platform/tests/audio-output/enumerateDevices-permissions-policy.https.html32
-rw-r--r--testing/web-platform/tests/audio-output/enumerateDevices-with-selectAudioOutput.https.html48
-rw-r--r--testing/web-platform/tests/audio-output/idlharness.https.window.js20
-rw-r--r--testing/web-platform/tests/audio-output/removeTrack-after-setSinkId.https.html27
-rw-r--r--testing/web-platform/tests/audio-output/secure-context.html23
-rw-r--r--testing/web-platform/tests/audio-output/selectAudioOutput-permissions-policy.https.sub.html48
-rw-r--r--testing/web-platform/tests/audio-output/selectAudioOutput-sans-user-activation.https.html18
-rw-r--r--testing/web-platform/tests/audio-output/setSinkId-manual.https.html132
-rw-r--r--testing/web-platform/tests/audio-output/setSinkId-permissions-policy.https.sub.html32
-rw-r--r--testing/web-platform/tests/audio-output/setSinkId-with-selectAudioOutput.https.html85
-rw-r--r--testing/web-platform/tests/audio-output/setSinkId.https.html62
12 files changed, 531 insertions, 0 deletions
diff --git a/testing/web-platform/tests/audio-output/META.yml b/testing/web-platform/tests/audio-output/META.yml
new file mode 100644
index 0000000000..b6a7d4d062
--- /dev/null
+++ b/testing/web-platform/tests/audio-output/META.yml
@@ -0,0 +1,4 @@
+spec: https://w3c.github.io/mediacapture-output/
+suggested_reviewers:
+ - guidou
+ - jan-ivar
diff --git a/testing/web-platform/tests/audio-output/enumerateDevices-permissions-policy.https.html b/testing/web-platform/tests/audio-output/enumerateDevices-permissions-policy.https.html
new file mode 100644
index 0000000000..4e5652e68f
--- /dev/null
+++ b/testing/web-platform/tests/audio-output/enumerateDevices-permissions-policy.https.html
@@ -0,0 +1,32 @@
+<!DOCTYPE html>
+<head>
+ <title>
+ Test permissions policy on enumerateDevices() after getUserMedia()
+ </title>
+ <link rel="help" href="https://w3c.github.io/mediacapture-output/#privacy-obtaining-consent">
+ <meta charset=utf-8>
+<body>
+ <p class="instructions">If prompted, <strong>please allow</strong> access to
+ a microphone device.</p>
+ <script src="/resources/testharness.js"></script>
+ <script src="/resources/testharnessreport.js"></script>
+ <script src="/common/get-host-info.sub.js"></script>
+ <script src="/permissions-policy/resources/permissions-policy.js"></script>
+ <script>
+'use strict';
+
+promise_test(async () => {
+ const frame = document.createElement('iframe');
+ frame.allow = "speaker-selection 'none'"
+ const promise_load = new Promise(r => frame.onload = r);
+ document.body.appendChild(frame);
+ await promise_load;
+
+ const fDevices = frame.contentWindow.navigator.mediaDevices;
+ await fDevices.getUserMedia({ audio: true });
+ const list = await fDevices.enumerateDevices();
+ const outputDevicesList = list.filter(({kind}) => kind == "audiooutput");
+ assert_equals(outputDevicesList.length, 0, "number of output devices.");
+}, "permissions policy on enumerateDevices() after getUserMedia()");
+ </script>
+</body>
diff --git a/testing/web-platform/tests/audio-output/enumerateDevices-with-selectAudioOutput.https.html b/testing/web-platform/tests/audio-output/enumerateDevices-with-selectAudioOutput.https.html
new file mode 100644
index 0000000000..c1a825592a
--- /dev/null
+++ b/testing/web-platform/tests/audio-output/enumerateDevices-with-selectAudioOutput.https.html
@@ -0,0 +1,48 @@
+<!doctype html>
+<head>
+<title>Test effect of selectAudioOutput() on audiooutput devices from enumerateDevices()</title>
+<link rel="help" href="https://w3c.github.io/mediacapture-output/#dom-mediadevices-selectaudiooutput">
+</head>
+<body>
+ <p class="instructions">If prompted, <strong>please allow</strong> access to
+ an audio output device.</p>
+</body>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/resources/testdriver.js"></script>
+<script src="/resources/testdriver-vendor.js"></script>
+<script>
+'use strict';
+
+promise_test(async () => {
+ const devices = await navigator.mediaDevices.enumerateDevices();
+ const outputDevices = devices.filter(info => info.kind == "audiooutput");
+ assert_equals(outputDevices.length, 0, "number of audiooutput devices.");
+}, "enumerateDevices() returns no audiooutput devices before permission grant");
+
+let selected;
+
+promise_test(async t => {
+ await test_driver.bless('transient activation for selectAudioOutput()');
+ selected = await navigator.mediaDevices.selectAudioOutput();
+ assert_true(selected instanceof MediaDeviceInfo,
+ "resolves with a MediaDeviceInfo.");
+ assert_equals(selected.kind, "audiooutput", "selected.kind");
+ assert_greater_than(selected.deviceId.length, 0, "selected.deviceId.length");
+ assert_greater_than(selected.groupId.length, 0, "selected.groupId.length");
+ assert_not_equals(selected.label, undefined, "selected.label");
+}, "selectAudioOutput()");
+
+promise_test(async () => {
+ // "Once a device is exposed after a call to selectAudioOutput, it MUST be
+ // listed by enumerateDevices() for the current browsing context."
+ const devices = await navigator.mediaDevices.enumerateDevices();
+ const outputDevices = devices.filter(info => info.kind == "audiooutput");
+ assert_equals(outputDevices.length, 1, "number of audiooutput devices.");
+ assert_not_equals(selected, undefined);
+ const [info] = outputDevices;
+ assert_equals(info.deviceId, selected.deviceId, "deviceId exposed");
+ assert_equals(info.groupId, selected.groupId, "groupId exposed");
+ assert_equals(info.label, selected.label, "label exposed");
+}, "enumerateDevices() after selectAudioOutput()");
+</script>
diff --git a/testing/web-platform/tests/audio-output/idlharness.https.window.js b/testing/web-platform/tests/audio-output/idlharness.https.window.js
new file mode 100644
index 0000000000..d7cdbd0768
--- /dev/null
+++ b/testing/web-platform/tests/audio-output/idlharness.https.window.js
@@ -0,0 +1,20 @@
+// META: script=/resources/WebIDLParser.js
+// META: script=/resources/idlharness.js
+
+// https://w3c.github.io/mediacapture-output/
+
+'use strict';
+
+idl_test(
+ ['audio-output'],
+ ['mediacapture-streams', 'html', 'dom'],
+ idl_array => {
+ self.audio = document.createElement('audio');
+ self.video = document.createElement('video');
+ idl_array.add_objects({
+ HTMLAudioElement: ['audio'],
+ HTMLVideoElement: ['video'],
+ MediaDevices: ['navigator.mediaDevices'],
+ });
+ }
+);
diff --git a/testing/web-platform/tests/audio-output/removeTrack-after-setSinkId.https.html b/testing/web-platform/tests/audio-output/removeTrack-after-setSinkId.https.html
new file mode 100644
index 0000000000..97db5f81e2
--- /dev/null
+++ b/testing/web-platform/tests/audio-output/removeTrack-after-setSinkId.https.html
@@ -0,0 +1,27 @@
+<!DOCTYPE html>
+<html>
+<head>
+ <title>Test source track removal after setSinkId does not crash</title>
+</head>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/resources/testdriver.js"></script>
+<script src="/resources/testdriver-vendor.js"></script>
+<script src="/common/gc.js"></script>
+<script>
+'use strict';
+// This could be a crashtest, except that testdriver.bless() is not functional
+// in crashtests. promise_test() is more elegant than class="test-wait" anyway.
+promise_test(async t => {
+ await test_driver.bless('transient activation');
+ const {deviceId, label} = await navigator.mediaDevices.selectAudioOutput();
+ const audio = new Audio();
+ await audio.setSinkId(deviceId);
+ audio.srcObject = new AudioContext().createMediaStreamDestination().stream;
+ audio.play();
+ await new Promise(r => t.step_timeout(r, 0));
+ audio.srcObject.removeTrack(audio.srcObject.getTracks()[0]);
+ await garbageCollect();
+});
+</script>
+</html>
diff --git a/testing/web-platform/tests/audio-output/secure-context.html b/testing/web-platform/tests/audio-output/secure-context.html
new file mode 100644
index 0000000000..7b4dd229a6
--- /dev/null
+++ b/testing/web-platform/tests/audio-output/secure-context.html
@@ -0,0 +1,23 @@
+<!doctype html>
+<html>
+<head>
+<title>setSinkId and sinkId are SecureContext</title>
+<script src=/resources/testharness.js></script>
+<script src=/resources/testharnessreport.js></script>
+</head>
+<body>
+<script>
+let audio;
+setup(() => {
+ assert_false(window.isSecureContext,
+ "This test must be run in a non secure context");
+ audio = document.createElement('audio');
+});
+for (const prop of ['sinkId', 'setSinkId']) {
+ test(() => {
+ assert_false(prop in audio);
+ }, `${prop} exposure`);
+}
+</script>
+</body>
+</html>
diff --git a/testing/web-platform/tests/audio-output/selectAudioOutput-permissions-policy.https.sub.html b/testing/web-platform/tests/audio-output/selectAudioOutput-permissions-policy.https.sub.html
new file mode 100644
index 0000000000..41917bb1a6
--- /dev/null
+++ b/testing/web-platform/tests/audio-output/selectAudioOutput-permissions-policy.https.sub.html
@@ -0,0 +1,48 @@
+<!DOCTYPE html>
+<meta charset=utf-8>
+<body>
+ <script src="/resources/testharness.js"
+ type="text/javascript{{GET[in-iframe]}}"></script>
+ <script src="/resources/testharnessreport.js"
+ type="text/javascript{{GET[in-iframe]}}"></script>
+ <script src="/resources/testdriver.js"></script>
+ <script src="/resources/testdriver-vendor.js"></script>
+ <script src="/common/get-host-info.sub.js"></script>
+ <script src="/permissions-policy/resources/permissions-policy.js"></script>
+ <script>
+ 'use strict';
+
+ async function testSelectAudioOutput() {
+ await test_driver.bless('transient activation for selectAudioOutput()');
+ const selected = await navigator.mediaDevices.selectAudioOutput();
+
+ let devices;
+ try {
+ devices = await navigator.mediaDevices.enumerateDevices();
+ } catch (e) {
+ // Throw a unique error to avoid risk of false-pass if e should match
+ // an expected error from selectAudioOutput().
+ throw Error(`enumerateDevices() failed with ${e}`);
+ }
+ const selected_devices =
+ devices.filter(info => info.deviceId == selected.deviceId);
+ // Don't use assert_not_equals() because testharness.js cannot be
+ // loaded in the iframe with testdriver.js.
+ if (selected_devices.length != 1) {
+ throw Error('Device count matching selected is ' +
+ selected_devices.length);
+ }
+ }
+
+ if (page_loaded_in_iframe()) {
+ test_driver.set_test_context(window.parent);
+ }
+ const cross_domain = get_host_info().HTTPS_REMOTE_ORIGIN;
+ run_all_fp_tests_allow_self(
+ cross_domain,
+ 'speaker-selection',
+ 'NotAllowedError',
+ testSelectAudioOutput
+ );
+ </script>
+</body>
diff --git a/testing/web-platform/tests/audio-output/selectAudioOutput-sans-user-activation.https.html b/testing/web-platform/tests/audio-output/selectAudioOutput-sans-user-activation.https.html
new file mode 100644
index 0000000000..9f014476d2
--- /dev/null
+++ b/testing/web-platform/tests/audio-output/selectAudioOutput-sans-user-activation.https.html
@@ -0,0 +1,18 @@
+<!doctype html>
+<head>
+<title>Test selectAudioOutput() without user activation</title>
+<link rel="help" href="https://w3c.github.io/mediacapture-output/#dom-mediadevices-selectaudiooutput">
+</head>
+<script src=/resources/testharness.js></script>
+<script src=/resources/testharnessreport.js></script>
+<script>
+'use strict';
+promise_test(t => {
+ const p = navigator.mediaDevices.selectAudioOutput();
+ // Race a settled promise to check that the returned promise is already
+ // rejected.
+ return promise_rejects_dom(
+ t, 'InvalidStateError', Promise.race([p, Promise.resolve()]),
+ 'selectAudioOutput should have returned an already-rejected promise.');
+}, `selectAudioOutput() before user activation`);
+</script>
diff --git a/testing/web-platform/tests/audio-output/setSinkId-manual.https.html b/testing/web-platform/tests/audio-output/setSinkId-manual.https.html
new file mode 100644
index 0000000000..a083cdf092
--- /dev/null
+++ b/testing/web-platform/tests/audio-output/setSinkId-manual.https.html
@@ -0,0 +1,132 @@
+<!doctype html>
+<html>
+<head>
+<title>Test setSinkId behavior with permissions / device changes</title>
+<link rel="author" title="Dominique Hazael-Massieux" href="mailto:dom@w3.org"/>
+<link rel="help" href="https://www.w3.org/TR/audio-output/#dom-htmlmediaelement-setsinkid">
+<meta name="timeout" content="long">
+
+</head>
+<body>
+<h1 class="instructions">Description</h1>
+<p class="instructions">This test checks that <code>setSinkId</code> follows the algorithm, this includes manually checking the proper rendering on new output devices.</p>
+<p class="instructions">When prompted to access microphones, please accept as this is the only current way to get permissions for associated output devices.</p>
+<p class="instructions">For each authorized output device, check that selecting it makes the audio beat rendered on the corresponding device.</p>
+<p>Available but unauthorized devices (only those for which we can gain permission can be selected):</p>
+<ul id="available"></ul>
+<p>Authorized devices:</p>
+<ul id="authorized"></ul>
+<audio controls id="beat" src="/media/sound_5.mp3" loop></audio>
+
+<div id='log'></div>
+<script src=/resources/testharness.js></script>
+<script src=/resources/testharnessreport.js></script>
+<script>
+"use strict";
+
+const is_output = d => d.kind === "audiooutput";
+const by_id = (id) => d => d.groupId === id;
+const is_input = d => d.kind === "audioinput";
+const audio = document.getElementById("beat");
+const available = document.getElementById("available");
+const authorized = document.getElementById("authorized");
+
+let outputList;
+
+const selectDeviceTester = (t) => (e) => {
+ const groupId = e.target.dataset["groupid"];
+ const device = outputList.find(by_id(groupId));
+ if (audio.paused) audio.play();
+ promise_test(pt => audio.setSinkId(device.deviceId).then(() => {
+ assert_equals(device.deviceId, audio.sinkId);
+
+ const pass = document.createElement("button");
+ const fail = document.createElement("button");
+
+ const result = (bool) => () => {
+ assert_true(bool, "Sound rendered on right device");
+ fail.remove();
+ pass.remove();
+ audio.pause();
+ e.target.checked = false;
+ e.target.disabled = true;
+ t.done();
+ };
+
+ pass.style.backgroundColor = "#0f0";
+ pass.textContent = "\u2713 Sound plays on " + device.label;
+ pass.addEventListener("click", result(true));
+
+ fail.style.backgroundColor = "#f00";
+ fail.textContent = "\u274C Sound doesn't play on " + device.label;
+ fail.addEventListener("click", result(true));
+
+ const container = e.target.parentNode.parentNode;
+ container.appendChild(pass);
+ container.appendChild(fail);
+ }), "setSinkId for " + device.label + " resolves");
+};
+
+const addAuthorizedDevice = (groupId) => {
+ const device = outputList.find(by_id(groupId));
+ const async_t = async_test("Selecting output device " + device.label + " makes the audio rendered on the proper device");
+ const li = document.createElement("li");
+ const label = document.createElement("label");
+ const input = document.createElement("input");
+ input.type = "radio";
+ input.name = "device";
+ input.dataset["groupid"] = device.groupId;
+ input.addEventListener("change", selectDeviceTester(async_t));
+ const span = document.createElement("span");
+ span.textContent = device.label;
+ label.appendChild(input);
+ label.appendChild(span);
+ li.appendChild(label);
+ authorized.appendChild(li);
+};
+
+const authorizeDeviceTester = (t) => (e) => {
+ const groupId = e.target.dataset["groupid"];
+ navigator.mediaDevices.getUserMedia({audio: {groupId}})
+ .then( () => {
+ addAuthorizedDevice(groupId);
+ t.done();
+ });
+};
+
+promise_test(gum =>
+ navigator.mediaDevices.getUserMedia({audio: true}).then(
+ () => {
+ promise_test(t =>
+ navigator.mediaDevices.enumerateDevices().then(list => {
+ assert_not_equals(list.find(is_output), undefined, "media device list includes at least one audio output device");
+ outputList = list.filter(is_output);
+ outputList.forEach(d => {
+ const li = document.createElement("li");
+ assert_not_equals(d.label, "", "Audio Output Device Labels are available after permission grant");
+ li.textContent = d.label;
+ // Check permission
+ promise_test(perm => navigator.permissions.query({name: "speaker", deviceId: d.deviceId}).then(({state}) => {
+ if (state === "granted") {
+ addAuthorizedDevice(d.groupId);
+ } else if (state === "prompt") {
+ const inp = list.find(inp => inp.kind === "audioinput" && inp.groupId === d.groupId);
+ if (inp || true) {
+ const async_t = async_test("Authorizing output devices via permission requests for microphones works");
+ const button = document.createElement("button");
+ button.textContent = "Authorize access";
+ button.dataset["groupid"] = d.groupId;
+ button.addEventListener("click", async_t.step_func_done(authorizeDeviceTester(async_t)));
+ li.appendChild(button);
+ }
+ available.appendChild(li);
+ }
+ }, () => {
+ // if we can't query the permission, we assume it's granted :/
+ addAuthorizedDevice(d.groupId);
+ })
+ , "Query permission to use " + d.label);
+ });
+ }), "List media devices");
+ }), "Authorize mike access");
+</script>
diff --git a/testing/web-platform/tests/audio-output/setSinkId-permissions-policy.https.sub.html b/testing/web-platform/tests/audio-output/setSinkId-permissions-policy.https.sub.html
new file mode 100644
index 0000000000..8e26fc44a8
--- /dev/null
+++ b/testing/web-platform/tests/audio-output/setSinkId-permissions-policy.https.sub.html
@@ -0,0 +1,32 @@
+<!DOCTYPE html>
+<meta charset=utf-8>
+<body>
+ <script src="/resources/testharness.js"
+ type="text/javascript{{GET[in-iframe]}}"></script>
+ <script src="/resources/testharnessreport.js"
+ type="text/javascript{{GET[in-iframe]}}"></script>
+ <script src="/common/get-host-info.sub.js"></script>
+ <script src="/permissions-policy/resources/permissions-policy.js"></script>
+ <script>
+ 'use strict';
+
+ async function testSetSinkId() {
+ const audio = new Audio();
+ const p = audio.setSinkId('');
+ // Race a settled promise to check that the returned promise is already
+ // settled.
+ return Promise.race([
+ p,
+ Promise.reject(Error('setSinkId() promise not already settled')),
+ ]);
+ }
+
+ const cross_domain = get_host_info().HTTPS_REMOTE_ORIGIN;
+ run_all_fp_tests_allow_self(
+ cross_domain,
+ 'speaker-selection',
+ 'NotAllowedError',
+ testSetSinkId
+ );
+ </script>
+</body>
diff --git a/testing/web-platform/tests/audio-output/setSinkId-with-selectAudioOutput.https.html b/testing/web-platform/tests/audio-output/setSinkId-with-selectAudioOutput.https.html
new file mode 100644
index 0000000000..dbe32e2606
--- /dev/null
+++ b/testing/web-platform/tests/audio-output/setSinkId-with-selectAudioOutput.https.html
@@ -0,0 +1,85 @@
+<!doctype html>
+<head>
+<title>Test setSinkId() before and after selectAudioOutput()</title>
+<link rel="help" href="https://www.w3.org/TR/audio-output/#dom-htmlmediaelement-setsinkid">
+</head>
+<script src=/resources/testharness.js></script>
+<script src=/resources/testharnessreport.js></script>
+<script src="/resources/testdriver.js"></script>
+<script src="/resources/testdriver-vendor.js"></script>
+<script>
+"use strict";
+
+let deviceId;
+promise_test(async t => {
+ await test_driver.bless('transient activation for selectAudioOutput()');
+ ({deviceId} = await navigator.mediaDevices.selectAudioOutput());
+
+ const audio = new Audio();
+ const p1 = audio.setSinkId(deviceId);
+ assert_equals(audio.sinkId, "", "before it resolves, setSinkId is unchanged");
+ let r = await p1;
+ assert_equals(r, undefined, "setSinkId resolves with undefined");
+ assert_equals(audio.sinkId, deviceId, "when it resolves, setSinkId updates sinkId to the requested deviceId");
+
+ r = await audio.setSinkId(deviceId);
+ assert_equals(r, undefined, "resetting sinkid on same current value should always work");
+
+ r = await audio.setSinkId("");
+ assert_equals(r, undefined, "resetting sinkid on default audio output should always work");
+}, "setSinkId() after selectAudioOutput()");
+
+const src = "/media/movie_5.ogv";
+
+promise_test(async t => {
+ assert_not_equals(deviceId, undefined, "selectAudioOutput() resolved");
+ const video = document.createElement('video');
+ try {
+ document.body.appendChild(video);
+ video.src = src;
+ await new Promise(r => video.onloadeddata = r);
+ await video.setSinkId(deviceId);
+ assert_equals(video.sinkId, deviceId, "sinkId after setSinkId()");
+ } finally {
+ video.remove();
+ }
+}, "setSinkId() on video after loadeddata");
+
+promise_test(async t => {
+ assert_not_equals(deviceId, undefined, "selectAudioOutput() resolved");
+ const video = document.createElement('video');
+ try {
+ video.src = src;
+ video.autoplay = true;
+ video.loop = true;
+ await new Promise(r => video.onplay = r);
+ await video.setSinkId(deviceId);
+ assert_equals(video.sinkId, deviceId, "sinkId after setSinkId()");
+ } finally {
+ video.pause();
+ }
+}, "setSinkId() on video after play");
+
+// Use the same sinkId in another same-origin top-level browsing context.
+// "the identifier MUST be the same in documents of the same origin in
+// top-level browsing contexts."
+// https://w3c.github.io/mediacapture-main/#dom-mediadeviceinfo-deviceid
+promise_test(async t => {
+ assert_not_equals(deviceId, undefined, "selectAudioOutput() resolved");
+ const proxy = window.open('/common/blank.html');
+ t.add_cleanup(() => proxy.close());
+ await new Promise(r => proxy.onload = r);
+ const pAudio = new proxy.Audio();
+ await promise_rejects_dom(t, "NotFoundError", proxy.DOMException,
+ pAudio.setSinkId(deviceId),
+ "before selectAudioOutput()");
+ await test_driver.bless('transient activation for selectAudioOutput()',
+ null, proxy);
+ const { deviceId: pDeviceId } =
+ await proxy.navigator.mediaDevices.selectAudioOutput({deviceId});
+ assert_equals(pDeviceId, deviceId,
+ "deviceIds should be same in each browsing context");
+ await pAudio.setSinkId(deviceId);
+ assert_equals(pAudio.sinkId, deviceId, "sinkId after setSinkId()");
+}, "setSinkId() with deviceID from another window");
+</script>
diff --git a/testing/web-platform/tests/audio-output/setSinkId.https.html b/testing/web-platform/tests/audio-output/setSinkId.https.html
new file mode 100644
index 0000000000..be65f0ac81
--- /dev/null
+++ b/testing/web-platform/tests/audio-output/setSinkId.https.html
@@ -0,0 +1,62 @@
+<!doctype html>
+<head>
+<title>Test setSinkId behavior </title>
+<link rel="author" title="Dominique Hazael-Massieux" href="mailto:dom@w3.org"/>
+<link rel="help" href="https://www.w3.org/TR/audio-output/#dom-htmlmediaelement-setsinkid">
+</head>
+<script src=/resources/testharness.js></script>
+<script src=/resources/testharnessreport.js></script>
+<script src=/resources/testdriver.js></script>
+<script src=/resources/testdriver-vendor.js></script>
+<script src='../mediacapture-streams/permission-helper.js'></script>
+<script>
+"use strict";
+
+const audio = new Audio();
+
+promise_test(t => audio.setSinkId(""), "setSinkId on default audio output should always work");
+
+promise_test(t => promise_rejects_dom(t, "NotFoundError", audio.setSinkId("nonexistent_device_id")),
+ "setSinkId fails with NotFoundError on made up deviceid");
+
+promise_test(async t => {
+ // Attempt to expose some audiooutput devices.
+ await setMediaPermission("granted", ["microphone"]);
+ await navigator.mediaDevices.getUserMedia({ audio: true });
+ const list = await navigator.mediaDevices.enumerateDevices();
+ assert_greater_than(list.length, 0,
+ "media device list includes at least one device");
+ const audioInputList = list.filter(({kind}) => kind == "audioinput");
+ const outputDevicesList = list.filter(({kind}) => kind == "audiooutput");
+ // List of exposed microphone groupIds
+ const exposedGroupIds = new Set(audioInputList.map(device => device.groupId));
+
+ for (const { deviceId, groupId } of outputDevicesList) {
+ assert_true(exposedGroupIds.has(groupId),
+ "audiooutput device groupId must match an exposed microphone");
+ assert_greater_than(deviceId.length, 0, "deviceId.length");
+
+ const p1 = audio.setSinkId(deviceId);
+ assert_equals(audio.sinkId, "", "before it resolves, setSinkId is unchanged");
+ let r;
+ try {
+ r = await p1;
+ } catch (e) {
+ // "If sinkId is not the empty string, and the application would not be
+ // permitted to play audio through the device identified by sinkId if it
+ // weren't the current user agent default device, reject p with a new
+ // DOMException whose name is NotAllowedError and abort these
+ // substeps."
+ assert_equals(e.name, "NotAllowedError", "Non-permitted devices are failing with NotAllowed error");
+ continue;
+ }
+ assert_equals(r, undefined, "setSinkId resolves with undefined");
+ assert_equals(audio.sinkId, deviceId, "when it resolves, setSinkId updates sinkId to the requested deviceId");
+ r = await audio.setSinkId(deviceId);
+ assert_equals(r, undefined, "resetting sinkid on same current value should always work");
+ r = await audio.setSinkId("");
+ assert_equals(r, undefined, "resetting sinkid on default audio output should always work");
+ }
+}, "setSinkId() with output device IDs exposed by getUserMedia() should either reject with NotAllowedError or resolve");
+
+</script>