From 4b205ad3caed14a80991601143c5b60d0acd3358 Mon Sep 17 00:00:00 2001 From: Klaus Weidner Date: Thu, 20 Feb 2020 12:34:00 -0800 Subject: [PATCH] Update WebXR DOM Overlay to match spec change requests MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 Reviewed-by: Lan Wei Reviewed-by: Piotr Bialecki Commit-Queue: Klaus Weidner Cr-Commit-Position: refs/heads/master@{#743218} --- resources/chromium/webxr-test.js | 1 + webxr/dom-overlay/ar_dom_overlay.https.html | 202 +++++++++++++++++--- 2 files changed, 181 insertions(+), 22 deletions(-) diff --git a/resources/chromium/webxr-test.js b/resources/chromium/webxr-test.js index be8be8067e94bf..fe4d8bbeff0721 100644 --- a/resources/chromium/webxr-test.js +++ b/resources/chromium/webxr-test.js @@ -1233,6 +1233,7 @@ class MockXRInputSource { // 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 d63197c46a56f6..15eac78d9288b5 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; + }
- + + + +

test text

@@ -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); @@ -80,6 +140,62 @@ inner_a.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); + + 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(); @@ -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') }