webxr_util.js (10182B)
1 'use strict'; 2 3 // These tests rely on the User Agent providing an implementation of the 4 // WebXR Testing API (https://github.com/immersive-web/webxr-test-api). 5 // 6 // In Chromium-based browsers, this implementation is provided by a JavaScript 7 // shim in order to reduce the amount of test-only code shipped to users. To 8 // enable these tests the browser must be run with these options: 9 // 10 // --enable-blink-features=MojoJS,MojoJSTest 11 12 // Debugging message helper, by default does nothing. Implementations can 13 // override this. 14 var xr_debug = function(name, msg) {}; 15 16 let loaded = new Promise(resolve => document.addEventListener('DOMContentLoaded', resolve)); 17 18 function xr_promise_test(name, func, properties, glContextType, glContextProperties) { 19 promise_test(async (t) => { 20 if (glContextType === 'webgl2') { 21 // Fast fail on platforms not supporting WebGL2. 22 assert_implements('WebGL2RenderingContext' in window, 'webgl2 not supported.'); 23 } 24 // Perform any required test setup: 25 xr_debug(name, 'setup'); 26 27 assert_implements(navigator.xr, 'missing navigator.xr - ensure test is run in a secure context.'); 28 29 // Only set up once. 30 if (!navigator.xr.test) { 31 32 if (typeof isChromiumBased === 'undefined' || typeof isWebKitBased === 'undefined') { 33 // Load test-only API helpers. 34 const script = document.createElement('script'); 35 script.src = '/resources/test-only-api.js'; 36 script.async = false; 37 const p = new Promise((resolve, reject) => { 38 script.onload = () => { resolve(); }; 39 script.onerror = e => { reject(e); }; 40 }); 41 document.head.appendChild(script); 42 await p; 43 } 44 45 if (isChromiumBased) { 46 // Chrome setup 47 await loadChromiumResources(); 48 } else if (isWebKitBased) { 49 // WebKit setup 50 await setupWebKitWebXRTestAPI(); 51 } 52 } 53 54 // Either the test api needs to be polyfilled and it's not set up above, or 55 // something happened to one of the known polyfills and it failed to be 56 // setup properly. Either way, the fact that xr_promise_test is being used 57 // means that the tests expect navigator.xr.test to be set. By rejecting now 58 // we can hopefully provide a clearer indication of what went wrong. 59 assert_implements(navigator.xr.test, 'missing navigator.xr.test, even after attempted load'); 60 61 let gl = null; 62 let canvas = null; 63 if (glContextType) { 64 canvas = document.createElement('canvas'); 65 await loaded; 66 document.body.appendChild(canvas); 67 gl = canvas.getContext(glContextType, glContextProperties); 68 } 69 70 // Ensure that any devices are disconnected when done. If this were done in 71 // a .then() for the success case, a test that expected failure would 72 // already be marked done at the time that runs and the shutdown would 73 // interfere with the next test. 74 t.add_cleanup(async () => { 75 // Ensure system state is cleaned up. 76 xr_debug(name, 'cleanup'); 77 await navigator.xr.test.disconnectAllDevices(); 78 }); 79 80 xr_debug(name, 'main'); 81 return func(t, gl); 82 }, name, properties); 83 } 84 85 // A utility function for waiting one animation frame before running the callback 86 // 87 // This is only needed after calling FakeXRDevice methods outside of an animation frame 88 // 89 // This is so that we can paper over the potential race allowed by the "next animation frame" 90 // concept https://immersive-web.github.io/webxr-test-api/#xrsession-next-animation-frame 91 function requestSkipAnimationFrame(session, callback) { 92 session.requestAnimationFrame(() => { 93 session.requestAnimationFrame(callback); 94 }); 95 } 96 97 // A test function which runs through the common steps of requesting a session. 98 // Calls the passed in test function with the session, the controller for the 99 // device, and the test object. 100 function xr_session_promise_test( 101 name, func, fakeDeviceInit, sessionMode, sessionInit, properties, 102 glcontextPropertiesParam, gllayerPropertiesParam) { 103 const glcontextProperties = (glcontextPropertiesParam) ? glcontextPropertiesParam : {}; 104 const gllayerProperties = (gllayerPropertiesParam) ? gllayerPropertiesParam : {}; 105 106 function runTest(t, glContext) { 107 let testSession; 108 let testDeviceController; 109 let sessionObjects = {gl: glContext}; 110 111 // Ensure that any pending sessions are ended when done. This needs to 112 // use a cleanup function to ensure proper sequencing. If this were 113 // done in a .then() for the success case, a test that expected 114 // failure would already be marked done at the time that runs, and the 115 // shutdown would interfere with the next test which may have started. 116 t.add_cleanup(async () => { 117 // If a session was created, end it. 118 if (testSession) { 119 await testSession.end().catch(() => {}); 120 } 121 }); 122 123 return navigator.xr.test.simulateDeviceConnection(fakeDeviceInit) 124 .then((controller) => { 125 testDeviceController = controller; 126 return sessionObjects.gl.makeXRCompatible(); 127 }) 128 .then(() => new Promise((resolve, reject) => { 129 // Perform the session request in a user gesture. 130 xr_debug(name, 'simulateUserActivation'); 131 navigator.xr.test.simulateUserActivation(() => { 132 xr_debug(name, 'document.hasFocus()=' + document.hasFocus()); 133 navigator.xr.requestSession(sessionMode, sessionInit || {}) 134 .then((session) => { 135 xr_debug(name, 'session start'); 136 testSession = session; 137 session.mode = sessionMode; 138 session.sessionInit = sessionInit; 139 let glLayer = new XRWebGLLayer(session, sessionObjects.gl, gllayerProperties); 140 glLayer.context = sessionObjects.gl; 141 // Session must have a baseLayer or frame requests 142 // will be ignored. 143 session.updateRenderState({ 144 baseLayer: glLayer 145 }); 146 sessionObjects.glLayer = glLayer; 147 xr_debug(name, 'session.visibilityState=' + session.visibilityState); 148 try { 149 resolve(func(session, testDeviceController, t, sessionObjects)); 150 } catch(err) { 151 reject("Test function failed with: " + err); 152 } 153 }) 154 .catch((err) => { 155 xr_debug(name, 'error: ' + err); 156 reject( 157 'Session with params ' + 158 JSON.stringify(sessionMode) + 159 ' was rejected on device ' + 160 JSON.stringify(fakeDeviceInit) + 161 ' with error: ' + err); 162 }); 163 }); 164 })); 165 } 166 167 xr_promise_test( 168 name + ' - webgl', 169 runTest, 170 properties, 171 'webgl', 172 {alpha: false, antialias: false, ...glcontextProperties} 173 ); 174 xr_promise_test( 175 name + ' - webgl2', 176 runTest, 177 properties, 178 'webgl2', 179 {alpha: false, antialias: false, ...glcontextProperties} 180 ); 181 } 182 183 184 // This function wraps the provided function in a 185 // simulateUserActivation() call, and resolves the promise with the 186 // result of func(), or an error if one is thrown 187 function promise_simulate_user_activation(func) { 188 return new Promise((resolve, reject) => { 189 navigator.xr.test.simulateUserActivation(() => { 190 try { let a = func(); resolve(a); } catch(e) { reject(e); } 191 }); 192 }); 193 } 194 195 // This functions calls a callback with each API object as specified 196 // by https://immersive-web.github.io/webxr/spec/latest/, allowing 197 // checks to be made on all ojects. 198 // Arguements: 199 // callback: A callback function with two arguements, the first 200 // being the API object, the second being the name of 201 // that API object. 202 function forEachWebxrObject(callback) { 203 callback(window.navigator.xr, 'navigator.xr'); 204 callback(window.XRSession, 'XRSession'); 205 callback(window.XRSessionCreationOptions, 'XRSessionCreationOptions'); 206 callback(window.XRFrameRequestCallback, 'XRFrameRequestCallback'); 207 callback(window.XRPresentationContext, 'XRPresentationContext'); 208 callback(window.XRFrame, 'XRFrame'); 209 callback(window.XRLayer, 'XRLayer'); 210 callback(window.XRView, 'XRView'); 211 callback(window.XRViewport, 'XRViewport'); 212 callback(window.XRViewerPose, 'XRViewerPose'); 213 callback(window.XRWebGLLayer, 'XRWebGLLayer'); 214 callback(window.XRWebGLLayerInit, 'XRWebGLLayerInit'); 215 callback(window.XRCoordinateSystem, 'XRCoordinateSystem'); 216 callback(window.XRFrameOfReference, 'XRFrameOfReference'); 217 callback(window.XRStageBounds, 'XRStageBounds'); 218 callback(window.XRSessionEvent, 'XRSessionEvent'); 219 callback(window.XRCoordinateSystemEvent, 'XRCoordinateSystemEvent'); 220 } 221 222 // Code for loading test API in Chromium. 223 async function loadChromiumResources() { 224 await loadScript('/resources/chromium/webxr-test-math-helper.js'); 225 await import('/resources/chromium/webxr-test.js'); 226 await loadScript('/resources/testdriver.js'); 227 await loadScript('/resources/testdriver-vendor.js'); 228 229 // This infrastructure is also used by Chromium-specific internal tests that 230 // may need additional resources (e.g. internal API extensions), this allows 231 // those tests to rely on this infrastructure while ensuring that no tests 232 // make it into public WPTs that rely on APIs outside of the webxr test API. 233 if (typeof(additionalChromiumResources) !== 'undefined') { 234 for (const path of additionalChromiumResources) { 235 await loadScript(path); 236 } 237 } 238 239 xr_debug = navigator.xr.test.Debug; 240 } 241 242 function setupWebKitWebXRTestAPI() { 243 // WebKit setup. The internals object is used by the WebKit test runner 244 // to provide JS access to internal APIs. In this case it's used to 245 // ensure that XRTest is only exposed to wpt tests. 246 navigator.xr.test = internals.xrTest; 247 return Promise.resolve(); 248 }