'use strict'; // These tests rely on the User Agent providing an implementation of the // WebXR Testing API (https://github.com/immersive-web/webxr-test-api). // // In Chromium-based browsers, this implementation is provided by a JavaScript // shim in order to reduce the amount of test-only code shipped to users. To // enable these tests the browser must be run with these options: // // --enable-blink-features=MojoJS,MojoJSTest // Debugging message helper, by default does nothing. Implementations can // override this. var xr_debug = function(name, msg) {}; function xr_promise_test(name, func, properties, glContextType, glContextProperties) { promise_test(async (t) => { if (glContextType === 'webgl2') { // Fast fail on platforms not supporting WebGL2. assert_implements('WebGL2RenderingContext' in window, 'webgl2 not supported.'); } // Perform any required test setup: xr_debug(name, 'setup'); assert_implements(navigator.xr, 'missing navigator.xr - ensure test is run in a secure context.'); // Only set up once. if (!navigator.xr.test) { // Load test-only API helpers. const script = document.createElement('script'); script.src = '/resources/test-only-api.js'; script.async = false; const p = new Promise((resolve, reject) => { script.onload = () => { resolve(); }; script.onerror = e => { reject(e); }; }) document.head.appendChild(script); await p; if (isChromiumBased) { // Chrome setup await loadChromiumResources(); } else if (isWebKitBased) { // WebKit setup await setupWebKitWebXRTestAPI(); } } // Either the test api needs to be polyfilled and it's not set up above, or // something happened to one of the known polyfills and it failed to be // setup properly. Either way, the fact that xr_promise_test is being used // means that the tests expect navigator.xr.test to be set. By rejecting now // we can hopefully provide a clearer indication of what went wrong. assert_implements(navigator.xr.test, 'missing navigator.xr.test, even after attempted load'); let gl = null; let canvas = null; if (glContextType) { canvas = document.createElement('canvas'); document.body.appendChild(canvas); gl = canvas.getContext(glContextType, glContextProperties); } // Ensure that any devices are disconnected when done. If this were done in // a .then() for the success case, a test that expected failure would // already be marked done at the time that runs and the shutdown would // interfere with the next test. t.add_cleanup(async () => { // Ensure system state is cleaned up. xr_debug(name, 'cleanup'); await navigator.xr.test.disconnectAllDevices(); }); xr_debug(name, 'main'); return func(t, gl); }, name, properties); } // A utility function for waiting one animation frame before running the callback // // This is only needed after calling FakeXRDevice methods outside of an animation frame // // This is so that we can paper over the potential race allowed by the "next animation frame" // concept https://immersive-web.github.io/webxr-test-api/#xrsession-next-animation-frame function requestSkipAnimationFrame(session, callback) { session.requestAnimationFrame(() => { session.requestAnimationFrame(callback); }); } // A test function which runs through the common steps of requesting a session. // Calls the passed in test function with the session, the controller for the // device, and the test object. function xr_session_promise_test( name, func, fakeDeviceInit, sessionMode, sessionInit, properties, glcontextPropertiesParam, gllayerPropertiesParam) { const glcontextProperties = (glcontextPropertiesParam) ? glcontextPropertiesParam : {}; const gllayerProperties = (gllayerPropertiesParam) ? gllayerPropertiesParam : {}; function runTest(t, glContext) { let testSession; let testDeviceController; let sessionObjects = {gl: glContext}; // Ensure that any pending sessions are ended when done. This needs to // use a cleanup function to ensure proper sequencing. If this were // done in a .then() for the success case, a test that expected // failure would already be marked done at the time that runs, and the // shutdown would interfere with the next test which may have started. t.add_cleanup(async () => { // If a session was created, end it. if (testSession) { await testSession.end().catch(() => {}); } }); return navigator.xr.test.simulateDeviceConnection(fakeDeviceInit) .then((controller) => { testDeviceController = controller; return sessionObjects.gl.makeXRCompatible(); }) .then(() => new Promise((resolve, reject) => { // Perform the session request in a user gesture. xr_debug(name, 'simulateUserActivation'); navigator.xr.test.simulateUserActivation(() => { xr_debug(name, 'document.hasFocus()=' + document.hasFocus()); navigator.xr.requestSession(sessionMode, sessionInit || {}) .then((session) => { xr_debug(name, 'session start'); testSession = session; session.mode = sessionMode; session.sessionInit = sessionInit; let glLayer = new XRWebGLLayer(session, sessionObjects.gl, gllayerProperties); glLayer.context = sessionObjects.gl; // Session must have a baseLayer or frame requests // will be ignored. session.updateRenderState({ baseLayer: glLayer }); sessionObjects.glLayer = glLayer; xr_debug(name, 'session.visibilityState=' + session.visibilityState); try { resolve(func(session, testDeviceController, t, sessionObjects)); } catch(err) { reject("Test function failed with: " + err); } }) .catch((err) => { xr_debug(name, 'error: ' + err); reject( 'Session with params ' + JSON.stringify(sessionMode) + ' was rejected on device ' + JSON.stringify(fakeDeviceInit) + ' with error: ' + err); }); }); })); } xr_promise_test( name + ' - webgl', runTest, properties, 'webgl', {alpha: false, antialias: false, ...glcontextProperties} ); xr_promise_test( name + ' - webgl2', runTest, properties, 'webgl2', {alpha: false, antialias: false, ...glcontextProperties}); } // This function wraps the provided function in a // simulateUserActivation() call, and resolves the promise with the // result of func(), or an error if one is thrown function promise_simulate_user_activation(func) { return new Promise((resolve, reject) => { navigator.xr.test.simulateUserActivation(() => { try { let a = func(); resolve(a); } catch(e) { reject(e); } }); }); } // This functions calls a callback with each API object as specified // by https://immersive-web.github.io/webxr/spec/latest/, allowing // checks to be made on all ojects. // Arguements: // callback: A callback function with two arguements, the first // being the API object, the second being the name of // that API object. function forEachWebxrObject(callback) { callback(window.navigator.xr, 'navigator.xr'); callback(window.XRSession, 'XRSession'); callback(window.XRSessionCreationOptions, 'XRSessionCreationOptions'); callback(window.XRFrameRequestCallback, 'XRFrameRequestCallback'); callback(window.XRPresentationContext, 'XRPresentationContext'); callback(window.XRFrame, 'XRFrame'); callback(window.XRLayer, 'XRLayer'); callback(window.XRView, 'XRView'); callback(window.XRViewport, 'XRViewport'); callback(window.XRViewerPose, 'XRViewerPose'); callback(window.XRWebGLLayer, 'XRWebGLLayer'); callback(window.XRWebGLLayerInit, 'XRWebGLLayerInit'); callback(window.XRCoordinateSystem, 'XRCoordinateSystem'); callback(window.XRFrameOfReference, 'XRFrameOfReference'); callback(window.XRStageBounds, 'XRStageBounds'); callback(window.XRSessionEvent, 'XRSessionEvent'); callback(window.XRCoordinateSystemEvent, 'XRCoordinateSystemEvent'); } // Code for loading test API in Chromium. async function loadChromiumResources() { await loadScript('/resources/chromium/webxr-test-math-helper.js'); await import('/resources/chromium/webxr-test.js'); await loadScript('/resources/testdriver.js'); await loadScript('/resources/testdriver-vendor.js'); // This infrastructure is also used by Chromium-specific internal tests that // may need additional resources (e.g. internal API extensions), this allows // those tests to rely on this infrastructure while ensuring that no tests // make it into public WPTs that rely on APIs outside of the webxr test API. if (typeof(additionalChromiumResources) !== 'undefined') { for (const path of additionalChromiumResources) { await loadScript(path); } } xr_debug = navigator.xr.test.Debug; } function setupWebKitWebXRTestAPI() { // WebKit setup. The internals object is used by the WebKit test runner // to provide JS access to internal APIs. In this case it's used to // ensure that XRTest is only exposed to wpt tests. navigator.xr.test = internals.xrTest; return Promise.resolve(); }