summaryrefslogtreecommitdiffstats
path: root/testing/web-platform/tests/screen-capture
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-19 00:47:55 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-19 00:47:55 +0000
commit26a029d407be480d791972afb5975cf62c9360a6 (patch)
treef435a8308119effd964b339f76abb83a57c29483 /testing/web-platform/tests/screen-capture
parentInitial commit. (diff)
downloadfirefox-26a029d407be480d791972afb5975cf62c9360a6.tar.xz
firefox-26a029d407be480d791972afb5975cf62c9360a6.zip
Adding upstream version 124.0.1.upstream/124.0.1
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'testing/web-platform/tests/screen-capture')
-rw-r--r--testing/web-platform/tests/screen-capture/META.yml7
-rw-r--r--testing/web-platform/tests/screen-capture/capture-controller-event-target.https.window.js60
-rw-r--r--testing/web-platform/tests/screen-capture/delegate-request.https.sub.html80
-rw-r--r--testing/web-platform/tests/screen-capture/getdisplaymedia-after-discard.https.html45
-rw-r--r--testing/web-platform/tests/screen-capture/getdisplaymedia-capture-controller.https.window.js203
-rw-r--r--testing/web-platform/tests/screen-capture/getdisplaymedia-framerate.https.html42
-rw-r--r--testing/web-platform/tests/screen-capture/getdisplaymedia.https.html314
-rw-r--r--testing/web-platform/tests/screen-capture/historical.https.html10
-rw-r--r--testing/web-platform/tests/screen-capture/idlharness.https.window.js16
-rw-r--r--testing/web-platform/tests/screen-capture/permissions-policy-audio+video.https.sub.html48
-rw-r--r--testing/web-platform/tests/screen-capture/permissions-policy-audio.https.sub.html48
-rw-r--r--testing/web-platform/tests/screen-capture/permissions-policy-video.https.sub.html48
-rw-r--r--testing/web-platform/tests/screen-capture/resources/delegate-request-subframe.sub.html22
13 files changed, 943 insertions, 0 deletions
diff --git a/testing/web-platform/tests/screen-capture/META.yml b/testing/web-platform/tests/screen-capture/META.yml
new file mode 100644
index 0000000000..6fbb899ac3
--- /dev/null
+++ b/testing/web-platform/tests/screen-capture/META.yml
@@ -0,0 +1,7 @@
+spec: https://w3c.github.io/mediacapture-screen-share/
+suggested_reviewers:
+ - alvestrand
+ - martinthomson
+ - uysalere
+ - jan-ivar
+ - eladalon1983
diff --git a/testing/web-platform/tests/screen-capture/capture-controller-event-target.https.window.js b/testing/web-platform/tests/screen-capture/capture-controller-event-target.https.window.js
new file mode 100644
index 0000000000..379f359568
--- /dev/null
+++ b/testing/web-platform/tests/screen-capture/capture-controller-event-target.https.window.js
@@ -0,0 +1,60 @@
+'use strict';
+
+const controller = new CaptureController();
+const type = 'my-event-type';
+const listeners = {};
+const listener_count = 10;
+for (let i = 0; i < listener_count; i++) {
+ listeners[i] = {
+ callback: (event) => {
+ assert_equals(event.type, type, `Event type sent to listener ${i}`);
+ listeners[i].execution_count++;
+ }
+ };
+}
+
+test(() => {
+ for (const i in listeners) {
+ listeners[i].execution_count = 0;
+ controller.addEventListener(type, listeners[i].callback);
+ }
+ controller.dispatchEvent(new Event(type));
+ for (const i in listeners) {
+ assert_equals(
+ listeners[i].execution_count, 1,
+ `Callback execution count for listener ${i}`);
+ }
+}, 'Registering listeners on CaptureController and dispatching an event.');
+
+test(() => {
+ for (const i in listeners) {
+ listeners[i].execution_count = 0;
+ }
+ controller.dispatchEvent(new Event(type));
+ controller.dispatchEvent(new Event(type));
+ controller.dispatchEvent(new Event(type));
+ for (const i in listeners) {
+ assert_equals(
+ listeners[i].execution_count, 3,
+ `Callback execution count for listener ${i}`);
+ }
+}, 'Dispatching an multiple events to CaptureController.');
+
+test(() => {
+ for (const i in listeners) {
+ listeners[i].execution_count = 0;
+ if (i % 3) {
+ listeners[i].removed = false;
+ } else {
+ listeners[i].removed = true;
+ controller.removeEventListener(type, listeners[i].callback);
+ };
+ }
+ controller.dispatchEvent(new Event(type));
+ controller.dispatchEvent(new Event(type));
+ for (const i in listeners) {
+ assert_equals(
+ listeners[i].execution_count, listeners[i].removed ? 0 : 2,
+ `Callback execution count for listener ${i}`);
+ }
+}, 'Unregistering listeners from CaptureController and dispatching an event.');
diff --git a/testing/web-platform/tests/screen-capture/delegate-request.https.sub.html b/testing/web-platform/tests/screen-capture/delegate-request.https.sub.html
new file mode 100644
index 0000000000..8cc81c1383
--- /dev/null
+++ b/testing/web-platform/tests/screen-capture/delegate-request.https.sub.html
@@ -0,0 +1,80 @@
+<!DOCTYPE html>
+<title>Display-capture request delegation test</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/resources/testdriver.js"></script>
+<script src="/resources/testdriver-actions.js"></script>
+<script src="/resources/testdriver-vendor.js"></script>
+
+<div>
+ Verifies that getDisplayMedia() calls from a cross-origin subframe without user activation
+ works if and only if the top frame has user activation and it delegates the capability to the
+ subframe.
+</div>
+
+<iframe allow="display-capture" width="300px" height="50px"
+ src="https://{{hosts[alt][www]}}:{{ports[https][0]}}/screen-capture/resources/delegate-request-subframe.sub.html">
+</iframe>
+
+<script>
+ // Returns a |Promise| that gets resolved with |event.data| when |window|
+ // receives from |source| a "message" event whose |event.data.type| matches the string
+ // |message_data_type|.
+ function getMessageData(message_data_type, source) {
+ return new Promise(resolve => {
+ function waitAndRemove(e) {
+ if (e.source != source || !e.data || e.data.type != message_data_type)
+ return;
+ window.removeEventListener("message", waitAndRemove);
+ resolve(e.data);
+ }
+ window.addEventListener("message", waitAndRemove);
+ });
+ }
+
+ promise_setup(async () => {
+ // Make sure the iframe has loaded.
+ await getMessageData("subframe-loaded", frames[0]);
+ });
+
+ const target_origin = "https://{{hosts[alt][www]}}:{{ports[https][0]}}";
+ const request = {"type": "make-display-capture-request"};
+
+ promise_test(async () => {
+ let result_promise = getMessageData("result", frames[0]);
+ frames[0].postMessage(request, {targetOrigin: target_origin});
+ let data = await result_promise;
+
+ assert_equals(data.result, "failure");
+ }, "Display-capture request from a subframe fails without delegation when the top frame has no user activation");
+
+ promise_test(async () => {
+ let result_promise = getMessageData("result", frames[0]);
+ await test_driver.bless();
+ frames[0].postMessage(request, {targetOrigin: target_origin});
+ let data = await result_promise;
+
+ assert_equals(data.result, "failure");
+ }, "Display-capture request from a subframe fails without delegation when the top frame has user activation");
+
+ promise_test(async () => {
+ {
+ let result_promise = getMessageData("result", frames[0]);
+ await test_driver.bless();
+ frames[0].postMessage(request, {targetOrigin: target_origin,
+ delegate: "display-capture"});
+ let data = await result_promise;
+
+ assert_equals(data.result, "success");
+ }
+ {
+ // Check display-capture request can be consumed only once.
+ let result_promise = getMessageData("result", frames[0]);
+ frames[0].postMessage(request, {targetOrigin: target_origin});
+ let data = await result_promise;
+
+ assert_equals(data.result, "failure");
+ }
+ }, "Display-capture request from a subframe succeeds with delegation when the top frame has user activation");
+
+</script>
diff --git a/testing/web-platform/tests/screen-capture/getdisplaymedia-after-discard.https.html b/testing/web-platform/tests/screen-capture/getdisplaymedia-after-discard.https.html
new file mode 100644
index 0000000000..445120f8c2
--- /dev/null
+++ b/testing/web-platform/tests/screen-capture/getdisplaymedia-after-discard.https.html
@@ -0,0 +1,45 @@
+<!doctype html>
+<title>Test for rejected promise from getDisplayMedia() in a discarded browsing
+ context</title>
+<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>
+<body></body>
+<script>
+// https://w3c.github.io/mediacapture-screen-share/#dom-mediadevices-getdisplaymedia
+// If the current settings object's responsible document is NOT fully active,
+// return a promise rejected with a DOMException object whose name attribute
+// has the value "InvalidStateError".
+promise_test(async () => {
+ const frame = document.createElement('iframe');
+ document.body.appendChild(frame);
+ frame.srcdoc = '<html></html>';
+ await new Promise(resolve => frame.onload = resolve);
+ const child_window = frame.contentWindow;
+ const devices = child_window.navigator.mediaDevices;
+ const child_DOMException = child_window.DOMException;
+ // transient activation of iframe content
+ await test_driver.bless('getDisplayMedia()', undefined, child_window);
+ frame.remove();
+ // `catch()` is used rather than static Promise methods because microtasks
+ // for `PromiseResolve()` do not run when Promises in inactive Documents are
+ // involved. Whether microtasks for `catch()` run depends on the realm of
+ // the handler rather than the realm of the Promise.
+ // See https://github.com/whatwg/html/issues/5319.
+ let promise_already_rejected = false;
+ let rejected_reason;
+ devices.getDisplayMedia({video:true}).catch(reason => {
+ promise_already_rejected = true;
+ rejected_reason = reason;
+ });
+ // Race a settled promise to check that the returned promise is already
+ // rejected.
+ await Promise.reject().catch(() => {
+ assert_true(promise_already_rejected,
+ 'should have returned an already-rejected promise.');
+ assert_throws_dom('InvalidStateError', child_DOMException,
+ () => { throw rejected_reason });
+ });
+}, 'getDisplayMedia() in a discarded browsing context');
+</script>
diff --git a/testing/web-platform/tests/screen-capture/getdisplaymedia-capture-controller.https.window.js b/testing/web-platform/tests/screen-capture/getdisplaymedia-capture-controller.https.window.js
new file mode 100644
index 0000000000..0fe1a9fd26
--- /dev/null
+++ b/testing/web-platform/tests/screen-capture/getdisplaymedia-capture-controller.https.window.js
@@ -0,0 +1,203 @@
+// META: script=/resources/testdriver.js
+// META: script=/resources/testdriver-vendor.js
+// META: timeout=long
+
+'use strict';
+
+const validFocusBehaviors = [
+ 'focus-capturing-application', 'focus-captured-surface', 'no-focus-change'
+];
+const validDisplaySurfaces = ['window', 'browser'];
+
+test(() => {
+ assert_own_property(window, 'CaptureController');
+}, 'CaptureController in window');
+
+const stopTracks = stream => stream.getTracks().forEach(track => track.stop());
+
+validFocusBehaviors.forEach(
+ (focusBehavior) => test(
+ (t) => {
+ const controller = new CaptureController();
+ controller.setFocusBehavior(focusBehavior);
+ },
+ `setFocusBehavior("${
+ focusBehavior}") must succeed before capture starts`));
+
+['invalid', null, undefined, {}, true].forEach(
+ (focusBehavior) => test(
+ () => {
+ const controller = new CaptureController();
+ assert_throws_js(
+ TypeError, () => controller.setFocusBehavior(focusBehavior));
+ },
+ `setFocusBehavior("${
+ focusBehavior}") must throw TypeError if focusBehavior is invalid`));
+
+promise_test(async (t) => {
+ const controller = new CaptureController();
+ await test_driver.bless('getDisplayMedia()');
+ const stream = await navigator.mediaDevices.getDisplayMedia({controller});
+ t.add_cleanup(() => stopTracks(stream));
+ assert_equals(stream.getTracks().length, 1);
+ assert_equals(stream.getVideoTracks().length, 1);
+ assert_equals(stream.getAudioTracks().length, 0);
+}, 'getDisplayMedia({controller}) must succeed');
+
+['invalid', null, {}, true].forEach(
+ (controller) => promise_test(
+ async (t) => {
+ await test_driver.bless('getDisplayMedia()');
+ await promise_rejects_js(
+ t, TypeError,
+ navigator.mediaDevices.getDisplayMedia({controller}));
+ },
+ `getDisplayMedia({controller: ${
+ controller}}) must fail with TypeError`));
+
+promise_test(async (t) => {
+ const controller = new CaptureController();
+
+ await test_driver.bless('getDisplayMedia()');
+ const stream = await navigator.mediaDevices.getDisplayMedia({controller});
+ t.add_cleanup(() => stopTracks(stream));
+
+ await test_driver.bless('getDisplayMedia()');
+ const p = navigator.mediaDevices.getDisplayMedia({controller});
+ t.add_cleanup(async () => {
+ try {
+ stopTracks(await p);
+ } catch {
+ }
+ });
+ await promise_rejects_dom(
+ t, 'InvalidStateError', Promise.race([p, Promise.resolve()]),
+ 'getDisplayMedia should have returned an already-rejected promise.');
+}, 'getDisplayMedia({controller}) must fail with InvalidStateError if controller is bound');
+
+validDisplaySurfaces.forEach((displaySurface) => {
+ validFocusBehaviors.forEach(
+ (focusBehavior) => promise_test(
+ async (t) => {
+ const controller = new CaptureController();
+ await test_driver.bless('getDisplayMedia()');
+ const stream = await navigator.mediaDevices.getDisplayMedia(
+ {controller, video: {displaySurface}});
+ t.add_cleanup(() => stopTracks(stream));
+ controller.setFocusBehavior(focusBehavior);
+ },
+ `setFocusBehavior("${
+ focusBehavior}") must succeed when window of opportunity is opened if capturing a ${
+ displaySurface}`));
+});
+
+validDisplaySurfaces.forEach((displaySurface) => {
+ validFocusBehaviors.forEach(
+ (focusBehavior) => promise_test(
+ async (t) => {
+ const controller = new CaptureController();
+ await test_driver.bless('getDisplayMedia()');
+ const p = navigator.mediaDevices.getDisplayMedia(
+ {controller, video: {displaySurface}});
+ controller.setFocusBehavior(focusBehavior);
+ const stream = await p;
+ t.add_cleanup(() => stopTracks(stream));
+ },
+ `setFocusBehavior("${
+ focusBehavior}") must succeed when getDisplayMedia promise is pending if capturing a ${
+ displaySurface}`));
+});
+
+validDisplaySurfaces.forEach((displaySurface) => {
+ validFocusBehaviors.forEach(
+ (focusBehavior) => promise_test(
+ async (t) => {
+ const controller = new CaptureController();
+ await test_driver.bless('getDisplayMedia()');
+ const stream = await navigator.mediaDevices.getDisplayMedia(
+ {controller, video: {displaySurface}});
+ stopTracks(stream);
+ assert_throws_dom(
+ 'InvalidStateError',
+ () => controller.setFocusBehavior(focusBehavior));
+ },
+ `setFocusBehavior("${
+ focusBehavior}") must throw InvalidStateError when track is stopped if capturing a ${
+ displaySurface}`));
+});
+
+validFocusBehaviors.forEach(
+ (focusBehavior) => promise_test(
+ async (t) => {
+ const controller = new CaptureController();
+ await test_driver.bless('getDisplayMedia()');
+ const stream = await navigator.mediaDevices.getDisplayMedia(
+ {controller, video: {displaySurface: 'monitor'}});
+ t.add_cleanup(() => stopTracks(stream));
+ assert_throws_dom(
+ 'InvalidStateError',
+ () => controller.setFocusBehavior(focusBehavior));
+ },
+ `setFocusBehavior("${
+ focusBehavior}") must throw InvalidStateError if capturing a monitor`));
+
+validDisplaySurfaces.forEach((displaySurface) => {
+ validFocusBehaviors.forEach(
+ (focusBehavior) => promise_test(
+ async (t) => {
+ const controller = new CaptureController();
+ await test_driver.bless('getDisplayMedia()');
+ const stream = await navigator.mediaDevices.getDisplayMedia(
+ {controller, video: {displaySurface}});
+ t.add_cleanup(() => stopTracks(stream));
+ await new Promise((resolve) => step_timeout(resolve, 0));
+ assert_throws_dom(
+ 'InvalidStateError',
+ () => controller.setFocusBehavior(focusBehavior));
+ },
+ `setFocusBehavior("${
+ focusBehavior}") must throw InvalidStateError when window of opportunity is closed if capturing a ${
+ displaySurface}`));
+});
+
+validDisplaySurfaces.forEach((displaySurface) => {
+ validFocusBehaviors.forEach(
+ (focusBehavior) => promise_test(
+ async (t) => {
+ const controller = new CaptureController();
+ await test_driver.bless('getDisplayMedia()');
+ const stream = await navigator.mediaDevices.getDisplayMedia(
+ {controller, video: {displaySurface}});
+ t.add_cleanup(() => stopTracks(stream));
+ controller.setFocusBehavior(focusBehavior)
+ assert_throws_dom(
+ 'InvalidStateError',
+ () => controller.setFocusBehavior(focusBehavior));
+ },
+ `setFocusBehavior("${
+ focusBehavior}") must throw InvalidStateError the second time if capturing a ${
+ displaySurface}`));
+});
+
+validFocusBehaviors.forEach(
+ (focusBehavior) => promise_test(
+ async (t) => {
+ const controller = new CaptureController();
+ const options = {
+ controller: controller,
+ video: {width: {max: 0}},
+ }
+ try {
+ await test_driver.bless('getDisplayMedia()');
+ stopTracks(await navigator.mediaDevices.getDisplayMedia(options));
+ } catch (err) {
+ assert_equals(err.name, 'OverconstrainedError', err.message);
+ assert_throws_dom(
+ 'InvalidStateError',
+ () => controller.setFocusBehavior(focusBehavior));
+ return;
+ }
+ assert_unreached('getDisplayMedia should have failed');
+ },
+ `setFocusBehavior("${
+ focusBehavior}") must throw InvalidStateError if getDisplayMedia fails`));
diff --git a/testing/web-platform/tests/screen-capture/getdisplaymedia-framerate.https.html b/testing/web-platform/tests/screen-capture/getdisplaymedia-framerate.https.html
new file mode 100644
index 0000000000..c17b25d987
--- /dev/null
+++ b/testing/web-platform/tests/screen-capture/getdisplaymedia-framerate.https.html
@@ -0,0 +1,42 @@
+<!doctype html>
+<meta charset=utf-8>
+<title>getDisplayMedia</title>
+<meta name="timeout" content="long">
+<button id="button">User gesture</button>
+<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>
+<video id="display"></video>
+<script>
+ 'use strict';
+
+const stopTracks = stream => stream.getTracks().forEach(track => track.stop());
+
+async function getDisplayMedia(constraints) {
+ const p = new Promise(r => button.onclick = r);
+ await test_driver.click(button);
+ await p;
+ return navigator.mediaDevices.getDisplayMedia(constraints);
+}
+
+promise_test( async t => {
+ const v = document.getElementById('display');
+ v.autoplay = true;
+ // work around firefox bug 1586505, orthogonal to what's being tested
+ const frames = () => v.mozPaintedFrames ?? v.getVideoPlaybackQuality()?.totalVideoFrames;
+ const target_rate = 5;
+ const stream = await getDisplayMedia({video: {width:160, frameRate: target_rate}});
+ t.add_cleanup(() => stopTracks(stream));
+ v.srcObject = stream;
+ const intitial_time = v.currentTime;
+ const initial_frame_count = frames();
+ await new Promise(r => t.step_timeout(r, 10000));
+ const total_elapsed_frames = frames() - initial_frame_count;
+ const total_elapsed_time = v.currentTime - intitial_time;
+ const average_fps = total_elapsed_frames / total_elapsed_time;
+ assert_greater_than_equal(average_fps, target_rate * 0.50);
+ assert_less_than_equal(average_fps, target_rate * 1.75);
+}, "getDisplayMedia() must adhere to frameRate if set");
+
+</script>
diff --git a/testing/web-platform/tests/screen-capture/getdisplaymedia.https.html b/testing/web-platform/tests/screen-capture/getdisplaymedia.https.html
new file mode 100644
index 0000000000..4558786faa
--- /dev/null
+++ b/testing/web-platform/tests/screen-capture/getdisplaymedia.https.html
@@ -0,0 +1,314 @@
+<!doctype html>
+<meta charset=utf-8>
+<title>getDisplayMedia</title>
+<meta name="timeout" content="long">
+<button id="button">User gesture</button>
+<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';
+test(() => {
+ assert_idl_attribute(navigator.mediaDevices, 'getDisplayMedia');
+}, "getDisplayMedia in navigator.mediaDevices");
+
+const stopTracks = stream => stream.getTracks().forEach(track => track.stop());
+const j = obj => JSON.stringify(obj);
+
+async function getDisplayMedia(constraints) {
+ const p = new Promise(r => button.onclick = r);
+ await test_driver.click(button);
+ await p;
+ return navigator.mediaDevices.getDisplayMedia(constraints);
+}
+
+promise_test(t => {
+ const p = navigator.mediaDevices.getDisplayMedia({video: true});
+ t.add_cleanup(async () => {
+ try { stopTracks(await p) } catch {}
+ });
+ // Race a settled promise to check that the returned promise is already
+ // rejected.
+ return promise_rejects_dom(
+ t, 'InvalidStateError', Promise.race([p, Promise.resolve()]),
+ 'getDisplayMedia should have returned an already-rejected promise.');
+}, `getDisplayMedia() must require user activation`);
+
+[
+ {video: true},
+ {video: true, audio: false},
+ {video: {}},
+ {audio: false},
+ {},
+ undefined
+].forEach(constraints => promise_test(async t => {
+ const stream = await getDisplayMedia(constraints);
+ t.add_cleanup(() => stopTracks(stream));
+ assert_equals(stream.getTracks().length, 1);
+ assert_equals(stream.getVideoTracks().length, 1);
+ assert_equals(stream.getAudioTracks().length, 0);
+}, `getDisplayMedia(${j(constraints)}) must succeed with video`));
+
+[
+ {video: false},
+ {video: {advanced: [{width: 320}]}},
+ {video: {width: {min: 320}}},
+ {video: {width: {exact: 320}}},
+ {video: {height: {min: 240}}},
+ {video: {height: {exact: 240}}},
+ {video: {frameRate: {min: 4}}},
+ {video: {frameRate: {exact: 4}}},
+ {video: false, audio: true},
+].forEach(constraints => promise_test(async t => {
+ await test_driver.bless('getDisplayMedia()');
+ const p = navigator.mediaDevices.getDisplayMedia(constraints);
+ t.add_cleanup(async () => {
+ try { stopTracks(await p) } catch {}
+ });
+ await promise_rejects_js(
+ t, TypeError, Promise.race([p, Promise.resolve()]),
+ 'getDisplayMedia should have returned an already-rejected promise.');
+}, `getDisplayMedia(${j(constraints)}) must fail with TypeError`));
+
+[
+ {video: true, audio: true},
+ {audio: true},
+].forEach(constraints => promise_test(async t => {
+ const stream = await getDisplayMedia(constraints);
+ t.add_cleanup(() => stopTracks(stream));
+ assert_greater_than_equal(stream.getTracks().length, 1);
+ assert_less_than_equal(stream.getTracks().length, 2);
+ assert_equals(stream.getVideoTracks().length, 1);
+ assert_less_than_equal(stream.getAudioTracks().length, 1);
+}, `getDisplayMedia(${j(constraints)}) must succeed with video maybe audio`));
+
+[
+ {width: {max: 360}},
+ {height: {max: 240}},
+ {width: {max: 360}, height: {max: 240}},
+ {frameRate: {max: 4}},
+ {frameRate: {max: 4}, width: {max: 360}},
+ {frameRate: {max: 4}, height: {max: 240}},
+ {frameRate: {max: 4}, width: {max: 360}, height: {max: 240}},
+].forEach(constraints => promise_test(async t => {
+ const stream = await getDisplayMedia({video: constraints});
+ t.add_cleanup(() => stopTracks(stream));
+ const {width, height, frameRate} = stream.getTracks()[0].getSettings();
+ assert_greater_than_equal(width, 1);
+ assert_greater_than_equal(height, 1);
+ assert_greater_than_equal(frameRate, 1);
+ if (constraints.width) {
+ assert_less_than_equal(width, constraints.width.max);
+ }
+ if (constraints.height) {
+ assert_less_than_equal(height, constraints.height.max);
+ }
+ if (constraints.frameRate) {
+ assert_less_than_equal(frameRate, constraints.frameRate.max);
+ }
+}, `getDisplayMedia({video: ${j(constraints)}}) must be constrained`));
+
+const someSizes = [
+ {width: 160},
+ {height: 120},
+ {width: 80},
+ {height: 60},
+ {width: 158},
+ {height: 118},
+];
+
+someSizes.forEach(constraints => promise_test(async t => {
+ const stream = await getDisplayMedia({video: constraints});
+ t.add_cleanup(() => stopTracks(stream));
+ const {width, height, frameRate} = stream.getTracks()[0].getSettings();
+ if (constraints.width) {
+ assert_equals(width, constraints.width);
+ } else {
+ assert_equals(height, constraints.height);
+ }
+ assert_greater_than_equal(frameRate, 1);
+}, `getDisplayMedia({video: ${j(constraints)}}) must be downscaled precisely`));
+
+promise_test(async t => {
+ const video = {height: 240};
+ const stream = await getDisplayMedia({video});
+ t.add_cleanup(() => stopTracks(stream));
+ const [track] = stream.getVideoTracks();
+ const {height} = track.getSettings();
+ assert_equals(height, video.height);
+ for (const constraints of someSizes) {
+ await track.applyConstraints(constraints);
+ const {width, height} = track.getSettings();
+ if (constraints.width) {
+ assert_equals(width, constraints.width);
+ } else {
+ assert_equals(height, constraints.height);
+ }
+ }
+}, `applyConstraints(width or height) must downscale precisely`);
+
+[
+ {video: {width: {max: 0}}},
+ {video: {height: {max: 0}}},
+ {video: {frameRate: {max: 0}}},
+ {video: {width: {max: -1}}},
+ {video: {height: {max: -1}}},
+ {video: {frameRate: {max: -1}}},
+].forEach(constraints => promise_test(async t => {
+ try {
+ stopTracks(await getDisplayMedia(constraints));
+ } catch (err) {
+ assert_equals(err.name, 'OverconstrainedError', err.message);
+ return;
+ }
+ assert_unreached('getDisplayMedia should have failed');
+}, `getDisplayMedia(${j(constraints)}) must fail with OverconstrainedError`));
+
+// Content shell picks a fake desktop device by default.
+promise_test(async t => {
+ const stream = await getDisplayMedia({video: true});
+ t.add_cleanup(() => stopTracks(stream));
+ assert_equals(stream.getVideoTracks().length, 1);
+ const track = stream.getVideoTracks()[0];
+ assert_equals(track.kind, "video");
+ assert_equals(track.enabled, true);
+ assert_equals(track.readyState, "live");
+ track.stop();
+ assert_equals(track.readyState, "ended");
+}, 'getDisplayMedia() resolves with stream with video track');
+
+{
+ const displaySurfaces = ['monitor', 'window', 'browser'];
+ displaySurfaces.forEach((displaySurface) => {
+ promise_test(async t => {
+ const stream = await getDisplayMedia({video: {displaySurface}});
+ t.add_cleanup(() => stopTracks(stream));
+ const settings = stream.getVideoTracks()[0].getSettings();
+ assert_equals(settings.displaySurface, displaySurface);
+ assert_any(assert_equals, settings.logicalSurface, [true, false]);
+ assert_any(assert_equals, settings.cursor, ['never', 'always', 'motion']);
+ assert_false("suppressLocalAudioPlayback" in settings);
+ }, `getDisplayMedia({"video":{"displaySurface":"${displaySurface}"}}) with getSettings`);
+ })
+}
+
+{
+ const properties = ["displaySurface"];
+ properties.forEach((property) => {
+ test(() => {
+ const supportedConstraints =
+ navigator.mediaDevices.getSupportedConstraints();
+ assert_true(supportedConstraints[property]);
+ }, property + " is supported");
+ });
+}
+
+[
+ {video: {displaySurface: "monitor"}},
+ {video: {displaySurface: "window"}},
+ {video: {displaySurface: "browser"}},
+ {selfBrowserSurface: "include"},
+ {selfBrowserSurface: "exclude"},
+ {surfaceSwitching: "include"},
+ {surfaceSwitching: "exclude"},
+ {systemAudio: "include"},
+ {systemAudio: "exclude"},
+].forEach(constraints => promise_test(async t => {
+ const stream = await getDisplayMedia(constraints);
+ t.add_cleanup(() => stopTracks(stream));
+}, `getDisplayMedia(${j(constraints)}) must succeed`));
+
+[
+ {selfBrowserSurface: "invalid"},
+ {surfaceSwitching: "invalid"},
+ {systemAudio: "invalid"},
+ {monitorTypeSurfaces: "invalid"},
+].forEach(constraints => promise_test(async t => {
+ await test_driver.bless('getDisplayMedia()');
+ const p = navigator.mediaDevices.getDisplayMedia(constraints);
+ t.add_cleanup(async () => {
+ try { stopTracks(await p) } catch {}
+ });
+ await promise_rejects_js(
+ t, TypeError, Promise.race([p, Promise.resolve()]),
+ 'getDisplayMedia should have returned an already-rejected promise.');
+}, `getDisplayMedia(${j(constraints)}) must fail with TypeError`));
+
+test(() => {
+ const supportedConstraints =
+ navigator.mediaDevices.getSupportedConstraints();
+ assert_true(supportedConstraints.suppressLocalAudioPlayback);
+}, "suppressLocalAudioPlayback is supported");
+
+{
+ const suppressLocalAudioPlaybacks = [true, false];
+ suppressLocalAudioPlaybacks.forEach((suppressLocalAudioPlayback) => {
+ promise_test(async (t) => {
+ const stream = await getDisplayMedia({
+ audio: { suppressLocalAudioPlayback },
+ });
+ t.add_cleanup(() => stopTracks(stream));
+ const [videoTrack] = stream.getVideoTracks();
+ assert_false("suppressLocalAudioPlayback" in videoTrack.getSettings());
+ const [audioTrack] = stream.getAudioTracks();
+ const audioTrackSettings = audioTrack.getSettings();
+ assert_true("suppressLocalAudioPlayback" in audioTrackSettings);
+ assert_equals(
+ audioTrackSettings.suppressLocalAudioPlayback,
+ suppressLocalAudioPlayback
+ );
+ await audioTrack.applyConstraints();
+ assert_true("suppressLocalAudioPlayback" in audioTrackSettings);
+ assert_equals(
+ audioTrackSettings.suppressLocalAudioPlayback,
+ suppressLocalAudioPlayback
+ );
+ }, `getDisplayMedia({"audio":{"suppressLocalAudioPlayback":${suppressLocalAudioPlayback}}}) with getSettings`);
+ });
+}
+
+promise_test(async t => {
+ const stream = await getDisplayMedia({video: true});
+ t.add_cleanup(() => stopTracks(stream));
+ const capabilities = stream.getVideoTracks()[0].getCapabilities();
+ assert_any(
+ assert_equals, capabilities.displaySurface,
+ ['monitor', 'window', 'browser']);
+}, 'getDisplayMedia() with getCapabilities');
+
+promise_test(async (t) => {
+ const constraints = {
+ video: { displaySurface: "monitor" },
+ monitorTypeSurfaces: "exclude",
+ };
+ await test_driver.bless('getDisplayMedia()');
+ const p = navigator.mediaDevices.getDisplayMedia(constraints);
+ t.add_cleanup(async () => {
+ try { stopTracks(await p) } catch {}
+ });
+ await promise_rejects_js(
+ t, TypeError, Promise.race([p, Promise.resolve()]),
+ 'getDisplayMedia should have returned an already-rejected promise.');
+ }, `getDisplayMedia({"video":{"displaySurface":"monitor"},"monitorTypeSurfaces":"exclude"}) rejects with TypeError`);
+
+promise_test(async (t) => {
+ const stream = await getDisplayMedia({
+ video: { displaySurface: "monitor" },
+ monitorTypeSurfaces: "include",
+ });
+ t.add_cleanup(() => stopTracks(stream));
+ const { displaySurface } = stream.getTracks()[0].getSettings();
+ assert_equals(displaySurface, "monitor");
+}, `getDisplayMedia({"video":{"displaySurface":"monitor"},"monitorTypeSurfaces":"include"}) resolves with a monitor track`);
+
+promise_test(async (t) => {
+ const stream = await getDisplayMedia({
+ monitorTypeSurfaces: "exclude",
+ });
+ t.add_cleanup(() => stopTracks(stream));
+ const { displaySurface } = stream.getTracks()[0].getSettings();
+ assert_any(assert_equals, displaySurface, ["window", "browser"]);
+}, `getDisplayMedia({"monitorTypeSurfaces":"exclude"}) resolves with a non monitor track`);
+
+</script>
diff --git a/testing/web-platform/tests/screen-capture/historical.https.html b/testing/web-platform/tests/screen-capture/historical.https.html
new file mode 100644
index 0000000000..d510bc4208
--- /dev/null
+++ b/testing/web-platform/tests/screen-capture/historical.https.html
@@ -0,0 +1,10 @@
+<!doctype html>
+<meta charset=utf-8>
+<title>getDisplayMedia historical tests</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script>
+test(function() {
+ assert_false('getDisplayMedia' in navigator);
+}, 'navigator.getDisplayMedia should not exist');
+</script>
diff --git a/testing/web-platform/tests/screen-capture/idlharness.https.window.js b/testing/web-platform/tests/screen-capture/idlharness.https.window.js
new file mode 100644
index 0000000000..527565ea96
--- /dev/null
+++ b/testing/web-platform/tests/screen-capture/idlharness.https.window.js
@@ -0,0 +1,16 @@
+// META: script=/resources/WebIDLParser.js
+// META: script=/resources/idlharness.js
+
+'use strict';
+
+// https://w3c.github.io/mediacapture-screen-share/
+
+idl_test(
+ ['screen-capture'],
+ ['mediacapture-streams', 'html', 'dom'],
+ idl_array => {
+ idl_array.add_objects({
+ MediaDevices: ['navigator.mediaDevices'],
+ });
+ }
+);
diff --git a/testing/web-platform/tests/screen-capture/permissions-policy-audio+video.https.sub.html b/testing/web-platform/tests/screen-capture/permissions-policy-audio+video.https.sub.html
new file mode 100644
index 0000000000..2e7df39125
--- /dev/null
+++ b/testing/web-platform/tests/screen-capture/permissions-policy-audio+video.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 getDisplayMedia(constraints) {
+ await test_driver.bless('transient activation for getDisplayMedia()');
+ return navigator.mediaDevices.getDisplayMedia(constraints);
+ }
+
+ async function testGDM({audio, video}) {
+ let stream;
+ try {
+ stream = await getDisplayMedia({audio, video});
+ if (stream.getVideoTracks().length == 0) {
+ throw Error(`requested video track must be present with ` +
+ `audio ${audio} and video ${video}, or fail`);
+ }
+ } finally {
+ if (stream) {
+ stream.getTracks().forEach(track => track.stop());
+ }
+ }
+ }
+
+ 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,
+ 'display-capture',
+ 'NotAllowedError',
+ async () => {
+ await testGDM({audio: true, video: true});
+ }
+ );
+ </script>
+</body>
diff --git a/testing/web-platform/tests/screen-capture/permissions-policy-audio.https.sub.html b/testing/web-platform/tests/screen-capture/permissions-policy-audio.https.sub.html
new file mode 100644
index 0000000000..7bfc33f861
--- /dev/null
+++ b/testing/web-platform/tests/screen-capture/permissions-policy-audio.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 getDisplayMedia(constraints) {
+ await test_driver.bless('transient activation for getDisplayMedia()');
+ return navigator.mediaDevices.getDisplayMedia(constraints);
+ }
+
+ async function testGDM({audio, video}) {
+ let stream;
+ try {
+ stream = await getDisplayMedia({audio, video});
+ if (stream.getVideoTracks().length == 0) {
+ throw Error(`requested video track must be present with ` +
+ `audio ${audio} and video ${video}, or fail`);
+ }
+ } finally {
+ if (stream) {
+ stream.getTracks().forEach(track => track.stop());
+ }
+ }
+ }
+
+ 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,
+ 'display-capture',
+ 'NotAllowedError',
+ async () => {
+ await testGDM({audio: true});
+ }
+ );
+ </script>
+</body>
diff --git a/testing/web-platform/tests/screen-capture/permissions-policy-video.https.sub.html b/testing/web-platform/tests/screen-capture/permissions-policy-video.https.sub.html
new file mode 100644
index 0000000000..6740466ef2
--- /dev/null
+++ b/testing/web-platform/tests/screen-capture/permissions-policy-video.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 getDisplayMedia(constraints) {
+ await test_driver.bless('transient activation for getDisplayMedia()');
+ return navigator.mediaDevices.getDisplayMedia(constraints);
+ }
+
+ async function testGDM({audio, video}) {
+ let stream;
+ try {
+ stream = await getDisplayMedia({audio, video});
+ if (stream.getVideoTracks().length == 0) {
+ throw Error(`requested video track must be present with ` +
+ `audio ${audio} and video ${video}, or fail`);
+ }
+ } finally {
+ if (stream) {
+ stream.getTracks().forEach(track => track.stop());
+ }
+ }
+ }
+
+ 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,
+ 'display-capture',
+ 'NotAllowedError',
+ async () => {
+ await testGDM({video: true});
+ }
+ );
+ </script>
+</body>
diff --git a/testing/web-platform/tests/screen-capture/resources/delegate-request-subframe.sub.html b/testing/web-platform/tests/screen-capture/resources/delegate-request-subframe.sub.html
new file mode 100644
index 0000000000..2b3295bc20
--- /dev/null
+++ b/testing/web-platform/tests/screen-capture/resources/delegate-request-subframe.sub.html
@@ -0,0 +1,22 @@
+<!DOCTYPE html>
+<title>Display-capture request delegation test: subframe</title>
+
+<script>
+ function reportResult(msg) {
+ window.top.postMessage({"type": "result", "result": msg}, "*");
+ }
+
+ window.addEventListener("message", async e => {
+ if (e.data.type == "make-display-capture-request") {
+ try {
+ const stream = await navigator.mediaDevices.getDisplayMedia();
+ stream.getTracks()[0].stop();
+ reportResult("success");
+ } catch(e) {
+ reportResult("failure");
+ }
+ }
+ });
+
+ window.top.postMessage({"type": "subframe-loaded"}, "*");
+</script>