Reject getDisplayMedia calls without user activation

This CL makes sure getDisplayMedia() returned promise reject with
InvalidStateError when called without user activation. This is now gated
by an experimental blink feature as it was revered several times before.

Note that speculation rules tests are updated as well[1].

[1] https://github.com/WICG/nav-speculation/issues/225

Intent to remove: https://groups.google.com/a/chromium.org/g/blink-dev/c/YGmuAVOqftI

Bug: 1198918
Change-Id: I9517012e76478b108d74bff432b71095ac7ba4d2
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/4219093
Commit-Queue: Fr <beaufort.francois@gmail.com>
Reviewed-by: Elad Alon <eladalon@chromium.org>
Reviewed-by: Yoav Weiss <yoavweiss@chromium.org>
Reviewed-by: Dale Curtis <dalecurtis@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1101098}
diff --git a/mediacapture-streams/MediaStreamTrack-iframe-audio-transfer.https.html b/mediacapture-streams/MediaStreamTrack-iframe-audio-transfer.https.html
index dca9c07..3f132e1 100644
--- a/mediacapture-streams/MediaStreamTrack-iframe-audio-transfer.https.html
+++ b/mediacapture-streams/MediaStreamTrack-iframe-audio-transfer.https.html
@@ -2,9 +2,12 @@
 <title>MediaStreamTrack transfer to iframe</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>
 <script>
 promise_test(async () => {
   const iframe = document.createElement("iframe");
+  await test_driver.bless('getDisplayMedia');
   const stream = await navigator.mediaDevices.getDisplayMedia({audio:true, video: true});
   const track = stream.getAudioTracks()[0];
   const result = new Promise((resolve, reject) => {
diff --git a/mediacapture-streams/MediaStreamTrack-iframe-transfer.https.html b/mediacapture-streams/MediaStreamTrack-iframe-transfer.https.html
index abff374..2215954 100644
--- a/mediacapture-streams/MediaStreamTrack-iframe-transfer.https.html
+++ b/mediacapture-streams/MediaStreamTrack-iframe-transfer.https.html
@@ -2,9 +2,12 @@
 <title>MediaStreamTrack transfer to iframe</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>
 <script>
 promise_test(async () => {
   const iframe = document.createElement("iframe");
+  await test_driver.bless('getDisplayMedia');
   const stream = await navigator.mediaDevices.getDisplayMedia({video: true});
   const track = stream.getVideoTracks()[0];
   const iframeLoaded = new Promise((resolve) => {iframe.onload = resolve});
diff --git a/permissions-policy/payment-extension-allowed-by-permissions-policy-attribute.https.sub.html b/permissions-policy/payment-extension-allowed-by-permissions-policy-attribute.https.sub.html
index de55531..ef36bf9 100644
--- a/permissions-policy/payment-extension-allowed-by-permissions-policy-attribute.https.sub.html
+++ b/permissions-policy/payment-extension-allowed-by-permissions-policy-attribute.https.sub.html
@@ -11,15 +11,15 @@
     var feature_name = 'permissions policy "payment"';
     var header = 'allow="payment" attribute';
 
-    async_test(t => {
-      test_feature_availability_with_post_message_result(
+    promise_test(t => {
+      return test_feature_availability_with_post_message_result(
           t, cross_origin_src, "NotSupportedError#The 'payment' feature is not " +
           "enabled in this document. Permissions Policy may be used to " +
           "delegate Web Payment capabilities to cross-origin child frames.");
     }, feature_name + ' is not supported in cross-origin iframe without ' + header);
 
-    async_test(t => {
-      test_feature_availability_with_post_message_result(
+    promise_test(t => {
+      return test_feature_availability_with_post_message_result(
           t, cross_origin_src, 'OK', 'payment');
     }, feature_name + ' can be enabled in cross-origin iframe using ' + header);
   </script>
diff --git a/permissions-policy/resources/permissions-policy.js b/permissions-policy/resources/permissions-policy.js
index 00c0bf2..62f8dcd 100644
--- a/permissions-policy/resources/permissions-policy.js
+++ b/permissions-policy/resources/permissions-policy.js
@@ -26,9 +26,11 @@
 //      https://github.com/w3c/webappsec-permissions-policy/blob/main/features.md
 //    allow_attribute: Optional argument, only used for testing fullscreen or
 //      payment: either "allowfullscreen" or "allowpaymentrequest" is passed.
+//    is_promise_test: Optional argument, true if this call should return a
+//    promise. Used by test_feature_availability_with_post_message_result()
 function test_feature_availability(
     feature_description, test, src, expect_feature_available, feature_name,
-    allow_attribute) {
+    allow_attribute, is_promise_test = false) {
   let frame = document.createElement('iframe');
   frame.src = src;
 
@@ -40,16 +42,26 @@
     frame.setAttribute(allow_attribute, true);
   }
 
-  window.addEventListener('message', test.step_func(evt => {
+  function expectFeatureAvailable(evt) {
     if (evt.source === frame.contentWindow &&
         evt.data.type === 'availability-result') {
       expect_feature_available(evt.data, feature_description);
       document.body.removeChild(frame);
       test.done();
     }
-  }));
+  }
 
+  if (!is_promise_test) {
+    window.addEventListener('message', test.step_func(expectFeatureAvailable));
+    document.body.appendChild(frame);
+    return;
+  }
+
+  const promise = new Promise((resolve) => {
+                    window.addEventListener('message', resolve);
+                  }).then(expectFeatureAvailable);
   document.body.appendChild(frame);
+  return promise;
 }
 
 // Default helper functions to test a feature's availability:
@@ -76,7 +88,8 @@
   const test_result = ({ name, message }, feature_description) => {
     assert_equals(name, expected_result, message + '.');
   };
-  test_feature_availability(null, test, src, test_result, allow_attribute);
+  return test_feature_availability(
+      null, test, src, test_result, allow_attribute, undefined, true);
 }
 
 // If this page is intended to test the named feature (according to the URL),
@@ -163,9 +176,9 @@
 
   // 2. Allowed in same-origin iframe.
   const same_origin_frame_pathname = same_origin_url(feature_name);
-  async_test(
+  promise_test(
       t => {
-        test_feature_availability_with_post_message_result(
+        return test_feature_availability_with_post_message_result(
             t, same_origin_frame_pathname, '#OK');
       },
       'Default "' + feature_name +
@@ -173,29 +186,29 @@
 
   // 3. Blocked in cross-origin iframe.
   const cross_origin_frame_url = cross_origin_url(cross_origin, feature_name);
-  async_test(
+  promise_test(
       t => {
-        test_feature_availability_with_post_message_result(
+        return test_feature_availability_with_post_message_result(
             t, cross_origin_frame_url, error_name);
       },
       'Default "' + feature_name +
           '" permissions policy ["self"] disallows cross-origin iframes.');
 
   // 4. Allowed in cross-origin iframe with "allow" attribute.
-  async_test(
+  promise_test(
       t => {
-        test_feature_availability_with_post_message_result(
+        return test_feature_availability_with_post_message_result(
             t, cross_origin_frame_url, '#OK', feature_name);
       },
       'permissions policy "' + feature_name +
           '" can be enabled in cross-origin iframes using "allow" attribute.');
 
   // 5. Blocked in same-origin iframe with "allow" attribute set to 'none'.
-  async_test(
+  promise_test(
       t => {
-        test_feature_availability_with_post_message_result(
+        return test_feature_availability_with_post_message_result(
             t, same_origin_frame_pathname, error_name,
-            feature_name + " 'none'");
+            feature_name + ' \'none\'');
       },
       'permissions policy "' + feature_name +
           '" can be disabled in same-origin iframes using "allow" attribute.');
@@ -246,9 +259,9 @@
 
   // 2. Allowed in same-origin iframe.
   const same_origin_frame_pathname = same_origin_url(feature_name);
-  async_test(
+  promise_test(
       t => {
-        test_feature_availability_with_post_message_result(
+        return test_feature_availability_with_post_message_result(
             t, same_origin_frame_pathname, '#OK');
       },
       'Default "' + feature_name +
@@ -256,30 +269,29 @@
 
   // 3. Allowed in cross-origin iframe.
   const cross_origin_frame_url = cross_origin_url(cross_origin, feature_name);
-  async_test(
+  promise_test(
       t => {
-        test_feature_availability_with_post_message_result(
+        return test_feature_availability_with_post_message_result(
             t, cross_origin_frame_url, '#OK');
       },
       'Default "' + feature_name +
           '" permissions policy ["*"] allows cross-origin iframes.');
 
   // 4. Blocked in cross-origin iframe with "allow" attribute set to 'none'.
-  async_test(
+  promise_test(
       t => {
-        test_feature_availability_with_post_message_result(
-            t, cross_origin_frame_url, error_name,
-            feature_name + " 'none'");
+        return test_feature_availability_with_post_message_result(
+            t, cross_origin_frame_url, error_name, feature_name + ' \'none\'');
       },
       'permissions policy "' + feature_name +
           '" can be disabled in cross-origin iframes using "allow" attribute.');
 
   // 5. Blocked in same-origin iframe with "allow" attribute set to 'none'.
-  async_test(
+  promise_test(
       t => {
-        test_feature_availability_with_post_message_result(
+        return test_feature_availability_with_post_message_result(
             t, same_origin_frame_pathname, error_name,
-            feature_name + " 'none'");
+            feature_name + ' \'none\'');
       },
       'permissions policy "' + feature_name +
           '" can be disabled in same-origin iframes using "allow" attribute.');
diff --git a/speculation-rules/prerender/resources/screen-capture.https.html b/speculation-rules/prerender/resources/screen-capture.https.html
index 55f1995..1304b9d 100644
--- a/speculation-rules/prerender/resources/screen-capture.https.html
+++ b/speculation-rules/prerender/resources/screen-capture.https.html
@@ -2,30 +2,23 @@
 <script src="/resources/testharness.js"></script>
 <script src="/resources/testharnessreport.js"></script>
 <script src="/speculation-rules/prerender/resources/utils.js"></script>
-<script src="/speculation-rules/prerender/resources/deferred-promise-utils.js"></script>
 <script>
 
-const params = new URLSearchParams(location.search);
+assert_true(document.prerendering);
 
-// The main test page (restriction-screen-capture.https.html) loads the
-// initiator page, then the initiator page will prerender itself with the
-// `prerendering` parameter.
-const isPrerendering = params.has('prerendering');
+async function invokeScreenCaptureAPI(){
+  const bc = new PrerenderChannel('prerender-channel');
 
-if (!isPrerendering) {
-  loadInitiatorPage();
-} else {
-  const prerenderEventCollector = new PrerenderEventCollector();
-  const promise = new Promise(async (resolve, reject) => {
-    try {
-      await navigator.mediaDevices.getDisplayMedia({video: true});
-      resolve();
-    } catch (e) {
-      reject(`navigator.mediaDevices.getDisplayMedia error: ${e.toString()}`);
-    }
-  });
-  prerenderEventCollector.start(
-      promise, 'navigator.mediaDevices.getDisplayMedia');
+  try {
+    await navigator.mediaDevices.getDisplayMedia();
+    bc.postMessage('unexpected success');
+  } catch (err){
+    bc.postMessage(err.name);
+  } finally {
+    bc.close();
+  }
 }
 
+invokeScreenCaptureAPI();
+
 </script>
diff --git a/speculation-rules/prerender/restriction-screen-capture.https.html b/speculation-rules/prerender/restriction-screen-capture.https.html
index e4b958d..2cd7fb6 100644
--- a/speculation-rules/prerender/restriction-screen-capture.https.html
+++ b/speculation-rules/prerender/restriction-screen-capture.https.html
@@ -1,5 +1,5 @@
 <!DOCTYPE html>
-<title>Access to the Screen Capture API is deferred</title>
+<title>Prerendering cannot invoke the Screen Capture API</title>
 <meta name="timeout" content="long">
 <script src="/resources/testharness.js"></script>
 <script src="/resources/testharnessreport.js"></script>
@@ -12,8 +12,7 @@
 
 promise_test(async t => {
   const uid = token();
-  const bc = new PrerenderChannel('test-channel', uid);
-  t.add_cleanup(_ => bc.close());
+  const bc = new PrerenderChannel('prerender-channel', uid);
 
   const gotMessage = new Promise(resolve => {
     bc.addEventListener('message', e => {
@@ -22,35 +21,15 @@
       once: true
     });
   });
-  const url = `resources/screen-capture.https.html?uid=${uid}`;
-  window.open(url, '_blank', 'noopener');
 
+  // Start prerendering a page that attempts to invoke the Screen Capture API.
+  // This API is activated-gated so it's expected to fail:
+  // https://wicg.github.io/nav-speculation/prerendering.html#implicitly-restricted
+  startPrerendering(`resources/screen-capture.https.html?uid=${uid}`);
   const result = await gotMessage;
-  const expected = [
-    {
-      event: 'started waiting navigator.mediaDevices.getDisplayMedia',
-      prerendering: true
-    },
-    {
-      event: 'prerendering change',
-      prerendering: false
-    },
-    {
-      event: 'finished waiting navigator.mediaDevices.getDisplayMedia',
-      prerendering: false
-    },
-  ];
-  assert_equals(result.length, expected.length);
-  for (let i = 0; i < result.length; i++) {
-    assert_equals(result[i].event, expected[i].event, `event[${i}]`);
-    assert_equals(result[i].prerendering, expected[i].prerendering,
-      `prerendering[${i}]`);
-  }
-
-  // Send a close signal to PrerenderEventCollector on the prerendered page.
-  new PrerenderChannel('close', uid).postMessage('');
-}, `The access to the Screen Capture API should be deferred until the
-    prerendered page is activated`);
+  assert_equals(result, 'InvalidStateError');
+  bc.close();
+}, `prerendering pages should not be able to invoke the Screen Capture API`);
 
 </script>
 </body>