Update WebXR DOM Overlay to match spec change requests

Instead of depending on Fullscreen API's styling, use a separate
:xr-dom-overlay pseudoclass with its own copy of the relevant styles.
Lazy-load this stylesheet when DOM Overlay mode is active.

Update the Fullscreen API integration to specifically allow XR session
setup to configure the fullscreen element, while blocking app-side
element changes to avoid inconsistent behavior.

Update the WPT test to cover more scenarios and improve compatibility
with potential implementations that aren't screen space.

Bug: 991747
Change-Id: I2b578570f695f72019c7efccb4c797cdb90e87f7
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/2057120
Reviewed-by: Philip Jägenstedt <foolip@chromium.org>
Reviewed-by: Lan Wei <lanwei@chromium.org>
Reviewed-by: Piotr Bialecki <bialpio@chromium.org>
Commit-Queue: Klaus Weidner <klausw@chromium.org>
Cr-Commit-Position: refs/heads/master@{#743218}
diff --git a/resources/chromium/webxr-test.js b/resources/chromium/webxr-test.js
index be8be80..fe4d8bbe 100644
--- a/resources/chromium/webxr-test.js
+++ b/resources/chromium/webxr-test.js
@@ -1233,6 +1233,7 @@
     // Pointer data for DOM Overlay, set by setOverlayPointerPosition()
     if (this.overlay_pointer_position_) {
       input_state.overlayPointerPosition = this.overlay_pointer_position_;
+      this.overlay_pointer_position_ = null;
     }
 
     return input_state;
diff --git a/webxr/dom-overlay/ar_dom_overlay.https.html b/webxr/dom-overlay/ar_dom_overlay.https.html
index d63197c..15eac78 100644
--- a/webxr/dom-overlay/ar_dom_overlay.https.html
+++ b/webxr/dom-overlay/ar_dom_overlay.https.html
@@ -11,13 +11,22 @@
       min-width: 10px;
       min-height: 10px;
   }
+  iframe {
+    border: 0;
+    width: 20px;
+    height: 20px;
+  }
 </style>
 <div id="div_overlay">
   <div id="inner_a">
   </div>
   <div id="inner_b">
   </div>
-  <canvas />
+  <!-- This SVG iframe is treated as cross-origin content. -->
+  <iframe id="iframe" src='data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"><rect height="20" width="20" fill="red" fill-opacity="0.3"/></svg>'>
+  </iframe>
+  <canvas>
+  </canvas>
 </div>
 <div id="div_other">
   <p>test text</p>
@@ -32,44 +41,95 @@
   supportedFeatures: ALL_FEATURES,
 };
 
-let watcherStep = new Event("watcherstep");
-let watcherDone = new Event("watcherdone");
-
-let testFunction = function(overlayElement, session, fakeDeviceController, t) {
+let testBasicProperties = function(overlayElement, session, fakeDeviceController, t) {
   assert_equals(session.mode, 'immersive-ar');
   assert_not_equals(session.environmentBlendMode, 'opaque');
 
   assert_true(overlayElement != null);
   assert_true(overlayElement instanceof Element);
 
-  assert_equals(session.domOverlayState.type, "screen");
+  // Verify that the DOM overlay type is one of the known types.
+  assert_in_array(session.domOverlayState.type,
+                  ["screen", "floating", "head-locked"]);
 
   // Verify SameObject property for domOverlayState
   assert_true(session.domOverlayState === session.domOverlayState);
 
-  // add: "select", "no_event",
-  let eventWatcher = new EventWatcher(
-    t, session, ["watcherstep", "select", "watcherdone"]);
-  let eventPromise = eventWatcher.wait_for(
-    ["watcherstep", "select", "watcherdone"]);
+  // The overlay element should have a transparent background.
+  assert_equals(window.getComputedStyle(overlayElement).backgroundColor,
+                'rgba(0, 0, 0, 0)');
+
+  // Check that the pseudostyle is set.
+  assert_equals(document.querySelector(':xr-overlay'), overlayElement);
+
+  return new Promise((resolve) => {
+    session.requestAnimationFrame((time, xrFrame) => {
+      resolve();
+    });
+  });
+};
+
+let testFullscreen = function(overlayElement, session, fakeDeviceController, t) {
+  // If the browser implements DOM Overlay using Fullscreen API,
+  // it must not be possible to change the DOM Overlay element by using
+  // Fullscreen API, and attempts to do so must be rejected.
+  // Since this is up to the UA, this test also passes if the fullscreen
+  // element is different from the overlay element.
+
+  let rafPromise = new Promise((resolve) => {
+    session.requestAnimationFrame((time, xrFrame) => {
+      resolve();
+    });
+  });
+  let promises = [rafPromise];
+
+  if (document.fullscreenElement == overlayElement) {
+    let elem = document.getElementById('div_other');
+    assert_true(elem != null);
+    assert_not_equals(elem, overlayElement);
+
+    let fullscreenPromise = new Promise((resolve, reject) => {
+      elem.requestFullscreen().then(() => {
+        assert_unreached("fullscreen change should be blocked");
+        reject();
+      }).catch(() => {
+        resolve();
+      });
+    });
+    promises.push(fullscreenPromise);
+  }
+
+  return Promise.all(promises);
+};
+
+let watcherStep = new Event("watcherstep");
+let watcherDone = new Event("watcherdone");
+
+let testInput = function(overlayElement, session, fakeDeviceController, t) {
+
+  // Use two DIVs for this test. "inner_a" uses a "beforexrselect" handler
+  // that uses preventDefault(). Controller interactions with it should trigger
+  // that event, and not generate an XR select event.
 
   let inner_a = document.getElementById('inner_a');
   assert_true(inner_a != null);
   let inner_b = document.getElementById('inner_b');
   assert_true(inner_b != null);
 
+  let got_beforexrselect = false;
   inner_a.addEventListener('beforexrselect', (ev) => {
     ev.preventDefault();
+    got_beforexrselect = true;
   });
 
-  // The overlay element should have a transparent background.
-  assert_equals(window.getComputedStyle(overlayElement).backgroundColor,
-                'rgba(0, 0, 0, 0)');
+  let eventWatcher = new EventWatcher(
+    t, session, ["watcherstep", "select", "watcherdone"]);
 
-  // Try fullscreening a different element, this should fail.
-  let elem = document.getElementById('div_other');
-  assert_true(elem != null);
-  assert_not_equals(elem, overlayElement);
+  // Set up the expected sequence of events. The test triggers two select
+  // actions, but only the second one should generate a "select" event.
+  // Use a "watcherstep" in between to verify this.
+  let eventPromise = eventWatcher.wait_for(
+    ["watcherstep", "select", "watcherdone"]);
 
   let input_source =
       fakeDeviceController.simulateInputSourceConnection(SCREEN_CONTROLLER);
@@ -88,6 +148,62 @@
           session.requestAnimationFrame((time, xrFrame) => {
             session.dispatchEvent(watcherStep);
 
+            assert_true(got_beforexrselect);
+
+            session.requestAnimationFrame((time, xrFrame) => {
+              input_source.setOverlayPointerPosition(inner_b.offsetLeft + 1,
+                                                     inner_b.offsetTop + 1);
+              input_source.startSelection();
+
+              session.requestAnimationFrame((time, xrFrame) => {
+                input_source.endSelection();
+
+                session.requestAnimationFrame((time, xrFrame) => {
+                  // Need to process one more frame to allow select to propagate.
+                  session.dispatchEvent(watcherDone);
+                });
+              });
+            });
+          });
+        });
+      });
+    });
+  });
+  return eventPromise;
+};
+
+let testCrossOriginContent = function(overlayElement, session, fakeDeviceController, t) {
+  let iframe = document.getElementById('iframe');
+  assert_true(iframe != null);
+  let inner_b = document.getElementById('inner_b');
+  assert_true(inner_b != null);
+
+  let eventWatcher = new EventWatcher(
+    t, session, ["watcherstep", "select", "watcherdone"]);
+
+  // Set up the expected sequence of events. The test triggers two select
+  // actions, but only the second one should generate a "select" event.
+  // Use a "watcherstep" in between to verify this.
+  let eventPromise = eventWatcher.wait_for(
+    ["watcherstep", "select", "watcherdone"]);
+
+  let input_source =
+      fakeDeviceController.simulateInputSourceConnection(SCREEN_CONTROLLER);
+  session.requestReferenceSpace('viewer').then(function(viewerSpace) {
+    // Press the primary input button and then release it a short time later.
+    session.requestAnimationFrame((time, xrFrame) => {
+      input_source.setOverlayPointerPosition(iframe.offsetLeft + 1,
+                                             iframe.offsetTop + 1);
+      input_source.startSelection();
+
+      session.requestAnimationFrame((time, xrFrame) => {
+        input_source.endSelection();
+
+        session.requestAnimationFrame((time, xrFrame) => {
+          // Need to process one more frame to allow select to propagate.
+          session.requestAnimationFrame((time, xrFrame) => {
+            session.dispatchEvent(watcherStep);
+
             session.requestAnimationFrame((time, xrFrame) => {
               input_source.setOverlayPointerPosition(inner_b.offsetLeft + 1,
                                                      inner_b.offsetTop + 1);
@@ -110,16 +226,58 @@
   return eventPromise;
 };
 
+xr_promise_test(
+"Ensures DOM Overlay rejected without root element",
+(t) => {
+  return navigator.xr.test.simulateDeviceConnection(fakeDeviceInitParams)
+  .then(() => {
+    return new Promise((resolve, reject) => {
+      navigator.xr.test.simulateUserActivation(() => {
+        resolve(
+          promise_rejects_dom(t, "NotSupportedError",
+            navigator.xr.requestSession('immersive-ar',
+              {requiredFeatures: ['dom-overlay']})
+            .then(session => session.end()),
+            "Should reject when not specifying DOM overlay root")
+        );
+      });
+    });
+  });
+});
+
 xr_session_promise_test(
-  "Ensures DOM Overlay feature works for immersive-ar",
-  testFunction.bind(this, document.body),
+  "Ensures DOM Overlay feature works for immersive-ar, body element",
+  testBasicProperties.bind(this, document.body),
   fakeDeviceInitParams, 'immersive-ar',
     {requiredFeatures: ['dom-overlay'],
      domOverlay: { root: document.body } });
 
 xr_session_promise_test(
-  "Ensures DOM Overlay element selection works",
-  testFunction.bind(this, document.getElementById('div_overlay')),
+  "Ensures DOM Overlay feature works for immersive-ar, div element",
+  testBasicProperties.bind(this, document.getElementById('div_overlay')),
+  fakeDeviceInitParams, 'immersive-ar',
+    {requiredFeatures: ['dom-overlay'],
+     domOverlay: { root: document.getElementById('div_overlay') } });
+
+xr_session_promise_test(
+  "Ensures DOM Overlay input deduplication works",
+  testInput.bind(this, document.getElementById('div_overlay')),
+  fakeDeviceInitParams, 'immersive-ar', {
+    requiredFeatures: ['dom-overlay'],
+    domOverlay: { root: document.getElementById('div_overlay') }
+  });
+
+xr_session_promise_test(
+  "Ensures DOM Overlay Fullscreen API doesn't change DOM overlay",
+  testFullscreen.bind(this, document.getElementById('div_overlay')),
+  fakeDeviceInitParams, 'immersive-ar', {
+    requiredFeatures: ['dom-overlay'],
+    domOverlay: { root: document.getElementById('div_overlay') }
+  });
+
+xr_session_promise_test(
+  "Ensures DOM Overlay interactions on cross origin iframe are ignored",
+  testCrossOriginContent.bind(this, document.getElementById('div_overlay')),
   fakeDeviceInitParams, 'immersive-ar', {
     requiredFeatures: ['dom-overlay'],
     domOverlay: { root: document.getElementById('div_overlay') }