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') }